2026-04-18開發日誌
- 日期:2026-04-18
- 專案:Cá xấu Duckduck
今天的重點是修 iOS 上圖層對不準的問題,並繼續擴充場景的互動物件。
iOS overlay 對不準修正
在電腦的 2560×1080 windowed 模式下,RefrigeratorOverlay 跟背景完全對齊,但在 iPhone 上冰箱位置會偏掉。
原因:stretch_mode 不一致。
| 節點 | stretch_mode |
|---|---|
背景 TextureRect | 6(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,而是直接跳場景:
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:
if SaveManager.get_prop("sugar_state") >= 1:
_background.texture = _TEXTURE_BG_SUGERLESS跟糖罐場景的做法一致,用 preload 在編譯期載入,runtime 不會有找不到檔案的問題。
包包物件拖拉系統(bag.gd)
實作了第一版 drag-and-drop,讓包包裡的物件可以拖出來使用。
流程:
- 包包打開後,點擊 item → 包包關閉,ghost 出現在手指位置
- 拖動 → ghost 跟著走
- 放開 → emit
item_dropped(item_id, drop_position)信號 - 如果沒有人消費(沒有呼叫
consume_drag())→ ghost 飛回包包,包包自動重新打開
技術細節:
- Items 用
Button(button_downsignal),而非TextureRect(gui_input 在 ScrollContainer 裡不可靠) - Ghost 用
Sprite2D(不受 Control layout 影響,scale直接有效) - Ghost 大小統一由
const GHOST_SCALE控制,改一個地方全部生效 _input統一處理拖動中的移動與放開事件
之後加事件的方式:
# 在場景的 _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決定行為:0(cabinet_inside.png,螞蟻在):顯示警語「糖罐旁邊太多螞蟻,無法打開」1(cabinet_after.png,螞蟻走了):取出鑰匙,key 飛行動畫進包包,背景換cabinet_inside_after_key_out.png2(已取鑰匙):按鈕 disabled
拖糖觸發螞蟻消失: 將包包裡的糖拖進 chapter1_cabinet_inside 場景任意位置 → 淡出換背景為 cabinet_after.png,cabinet_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 >= 1chapter1_cabinet_inside.gd:拖糖觸發時設sugar_state = 2
SugerBtn 永遠可以點進糖罐場景;進去後由場景內部根據 sugar_state 決定顯示內容。
InventoryManager 新增 remove_item
func remove_item(item_id: String) -> void:
for i in range(_items.size()):
if _items[i].id == item_id:
_items.remove_at(i)
return小結
今天確立了幾個規範:
- 所有 overlay 的
stretch_mode必須跟背景一致(= 6),否則在非 21:9 裝置上會跑位 - 開關狀態要即時寫入存檔,不能只放在記憶體 bool 裡
- 進場時統一在
_ready()恢復所有開關狀態,不做淡入動畫 - 需要進入子場景的按鈕用
change_scene_to_file,不需要進子場景的用 overlay 模式 - Drag ghost 用
Sprite2D而非TextureRect,避免 layout 系統覆蓋 scale/size - 物件狀態用多態 int prop 管理,不用 bool,方便之後擴充中間狀態
