遊戲開發

用 Godot 做出我的第一款完整遊戲
// 從卡關到 Release 的完整紀錄

Godot 4.x Editor 截圖

在這篇文章寫出來之前,我已經開始了五款遊戲——其中四款爛尾。 第五款終於完成了,而且真的發布出去了。這篇文章是對整個過程的記錄, 包含那些讓我真正學到東西的失敗。

我使用的引擎是 Godot 4.2,語言是 GDScript。 遊戲類型是文字冒險加上簡單的場景切換,如果你在找動作遊戲或 3D 的相關心得, 這篇文章可能只有部分適用。

為什麼前四款都沒做完

回頭看那些失敗的專案,原因其實都一樣:範圍失控。 我在還沒有確定核心玩法的情況下,就開始規劃地圖、NPC 對話、支線任務、 不同結局……每個功能都很帥,但合在一起就是做不完。

WARN 在確定核心循環(Core Loop)可以正常運作並且好玩之前, 不要開始做任何「加分項目」。美術、音效、多結局都可以之後再加, 但核心玩法如果從一開始就不對,加再多東西也救不了。

第五款遊戲之所以完成,是因為我強迫自己在開發前先回答一個問題: 「如果這個遊戲只有一個房間、一個機制,它有沒有辦法是好玩的?」 當我能夠回答「有」的時候,才開始寫第一行程式碼。

專案架構與我踩過的坑

Godot 的場景系統一開始讓我很困惑,我花了大約一個禮拜才搞清楚應該如何組織節點。 以下是我最後採用的基本架構:

# 場景樹基本結構
Game (Node)
  ├── GameManager (Node)        # 全域狀態管理
  ├── UILayer (CanvasLayer)     # HUD、對話框
  │   ├── DialogBox
  │   └── StatusBar
  └── World (Node2D)
      ├── CurrentRoom           # 動態載入當前房間
      └── Player (CharacterBody2D)

GameManager 是我用來管理跨場景狀態的節點, 設定為 Autoload(自動載入)之後,不管切換到哪個場景都可以存取它的變數。 這個設計救了我非常多次,特別是在處理存檔系統的時候。

# GameManager.gd — 簡化版本
extends Node

var player_flags: Dictionary = {}
var current_chapter: int = 0

func set_flag(key: String, value: bool) -> void:
    player_flags[key] = value

func get_flag(key: String) -> bool:
    return player_flags.get(key, false)

func save_game() -> void:
    var save_data = {
        "flags": player_flags,
        "chapter": current_chapter
    }
    var file = FileAccess.open("user://save.json", FileAccess.WRITE)
    file.store_string(JSON.stringify(save_data))

對話系統的設計

這款遊戲大量依賴文字和對話,所以對話系統的設計是整個專案裡花時間最多的部分。 我最後採用的方案是把所有對話內容存在 JSON 檔裡,然後用一個通用的 DialogBox 場景來渲染。

定義對話資料格式
每段對話是一個陣列,每個元素包含說話者名稱、文字內容,以及可選的選項。選項本身也可以觸發 flag 的變更。
建立通用 DialogBox 場景
用 RichTextLabel 顯示文字,支援 BBCode 讓特定文字變色或加粗。打字機效果用 Timer + 逐字插入實現。
信號驅動的對話結束事件
對話結束後,DialogBox 發送 dialogue_finished 信號,讓各個場景可以自由決定後續要觸發什麼事件,而不是把邏輯寫死在對話系統裡。
TIP Godot 的信號(Signal)系統是整個引擎最好用的功能之一。 善用信號可以讓各個系統之間保持低耦合,要修改某個系統的時候不會連帶弄壞其他東西。

發布與事後反思

遊戲最後發布在 itch.io 上,上線後的第一個禮拜沒什麼人玩, 後來在 X 上有人分享才稍微有了些流量。 最常收到的回饋是「文字太多了」——這讓我意識到, 我習慣的敘事節奏和一般玩家期待的體驗之間存在一段距離。

但最重要的收穫不是這些外部的回饋,而是:我終於做完了一款遊戲。 完成品哪怕只有六十分,也比一百個精心規劃但從未完成的企畫書更有價值。 下一款我還是會繼續用 Godot,但會更早設定 scope 的上限。