Skip to content

2026-04-18開發日誌

  • 日期:2026-04-18
  • 專案:Cá xấu Duckduck

今天的重點是修 iOS 上圖層對不準的問題,並繼續擴充場景的互動物件。

iOS overlay 對不準修正

在電腦的 2560×1080 windowed 模式下,RefrigeratorOverlay 跟背景完全對齊,但在 iPhone 上冰箱位置會偏掉。

原因stretch_mode 不一致。

節點stretch_mode
背景 TextureRect6(KEEP_ASPECT_COVERED,保持比例裁切)
RefrigeratorOverlay未設定,預設 0(SCALE,直接拉伸)

iPhone 的螢幕比例不是 21:9,兩種縮放邏輯在非標準比例的裝置上結果不同,造成位移。

修正:所有 overlay 的 stretch_mode 統一設成 6,跟背景一致。之後新增的 overlay 都要確認這一點。


開關狀態寫入存檔

原本冰箱和洗手台的開關只在記憶體中用 bool 記錄,離開場景再回來就會重置。

現在改成:

  • 按下按鈕時 → SaveManager.set_prop("key", 1 or 0) 立即寫入 JSON
  • 進場 _ready() 時 → SaveManager.get_prop("key") 讀取,如果是 1 就直接顯示 overlay(不做淡入,因為是恢復狀態)

目前存在 props 裡的開關 key:

refrigerator_open
washbasin_cabinet_open
suger_under_cabinet_open

洗手台下方櫃子(washbasin_cabinet

新增 WashbasinCabinetOverlay + WashbasinCabinetBtn,使用與冰箱相同的 overlay 模式:

  • 全螢幕透明圖(washbasin_cabinet.png
  • mouse_filter = 2(不攔截點擊)
  • stretch_mode = 6
  • 開關狀態寫入存檔

糖下方的櫃子(suger_under_cabinet

新增 SugerUnderCabinetOverlay + SugerUnderCabinetBtn,邏輯相同。


烤箱旁的櫃子 → 進入新場景

OvenCabinetBtn 按下後不是 overlay,而是直接跳場景:

gdscript
func _on_oven_cabinet_pressed() -> void:
    get_tree().change_scene_to_file("res://Scene/chapter1_cabinet_inside.tscn")

新建了 chapter1_cabinet_inside.tscn + chapter1_cabinet_inside.gd

  • 背景:cabinet_inside.png(棚子內部,很多道具)
  • 有返回按鈕(回 chapter1_background
  • 有包包 UI
  • 淡入動畫

背景圖根據糖的狀態切換

chapter1_background 進場時讀 sugar_state,如果 >= 1 就把背景換成 chap1_background_sugerless.png

gdscript
if SaveManager.get_prop("sugar_state") >= 1:
    _background.texture = _TEXTURE_BG_SUGERLESS

跟糖罐場景的做法一致,用 preload 在編譯期載入,runtime 不會有找不到檔案的問題。


包包物件拖拉系統(bag.gd

實作了第一版 drag-and-drop,讓包包裡的物件可以拖出來使用。

流程:

  1. 包包打開後,點擊 item → 包包關閉,ghost 出現在手指位置
  2. 拖動 → ghost 跟著走
  3. 放開 → emit item_dropped(item_id, drop_position) 信號
  4. 如果沒有人消費(沒有呼叫 consume_drag())→ ghost 飛回包包,包包自動重新打開

技術細節:

  • Items 用 Buttonbutton_down signal),而非 TextureRect(gui_input 在 ScrollContainer 裡不可靠)
  • Ghost 用 Sprite2D(不受 Control layout 影響,scale 直接有效)
  • Ghost 大小統一由 const GHOST_SCALE 控制,改一個地方全部生效
  • _input 統一處理拖動中的移動與放開事件

之後加事件的方式:

gdscript
# 在場景的 _ready() 連接信號
_bag_ui.item_dropped.connect(_on_item_dropped)

func _on_item_dropped(item_id: String, drop_pos: Vector2) -> void:
    if item_id == "sugar" and _drop_zone.get_global_rect().has_point(drop_pos):
        _bag_ui.consume_drag()  # ghost 消失,觸發事件
    # 不呼叫 consume_drag() → ghost 自動飛回

糖罐場景互動(chapter1_cabinet_inside

chapter1_cabinet_inside 新增糖罐按鈕與事件邏輯:

  • SugarJarBtn:覆蓋在糖罐位置的透明按鈕
  • 根據 cabinet_inside_state 決定行為:
    • 0cabinet_inside.png,螞蟻在):顯示警語「糖罐旁邊太多螞蟻,無法打開」
    • 1cabinet_after.png,螞蟻走了):取出鑰匙,key 飛行動畫進包包,背景換 cabinet_inside_after_key_out.png
    • 2(已取鑰匙):按鈕 disabled

拖糖觸發螞蟻消失: 將包包裡的糖拖進 chapter1_cabinet_inside 場景任意位置 → 淡出換背景為 cabinet_after.pngcabinet_inside_state 設為 1,糖從包包移除。

key 飛行動畫:Sprite2D 從糖罐中心飛到包包位置,縮小淡出,動畫結束後加入 inventory(同糖的收集動畫做法)。


sugar_state 參數統一管理

廢棄舊的 sugar_collected,改用三態 sugar_state

意義
0糖還在場景上,可撿起
1糖已撿進包包
2糖已用掉(拖進棚子引走螞蟻)

影響範圍:

  • chapter1_sugar_prop.gd:判斷改為 sugar_state >= 1(不再依賴 has_item,避免 inventory 移除後狀態重置)
  • chapter1_background.gd:背景切換條件改為 sugar_state >= 1
  • chapter1_cabinet_inside.gd:拖糖觸發時設 sugar_state = 2

SugerBtn 永遠可以點進糖罐場景;進去後由場景內部根據 sugar_state 決定顯示內容。


InventoryManager 新增 remove_item

gdscript
func remove_item(item_id: String) -> void:
    for i in range(_items.size()):
        if _items[i].id == item_id:
            _items.remove_at(i)
            return

小結

今天確立了幾個規範:

  1. 所有 overlay 的 stretch_mode 必須跟背景一致(= 6),否則在非 21:9 裝置上會跑位
  2. 開關狀態要即時寫入存檔,不能只放在記憶體 bool 裡
  3. 進場時統一在 _ready() 恢復所有開關狀態,不做淡入動畫
  4. 需要進入子場景的按鈕用 change_scene_to_file,不需要進子場景的用 overlay 模式
  5. Drag ghost 用 Sprite2D 而非 TextureRect,避免 layout 系統覆蓋 scale/size
  6. 物件狀態用多態 int prop 管理,不用 bool,方便之後擴充中間狀態