[{"data":1,"prerenderedAt":15},["ShallowReactive",2],{"workshop-jetpack-joyride":3},{"contents":4,"totalCount":13,"offset":14,"limit":13},[5],{"id":6,"createdAt":7,"updatedAt":8,"publishedAt":8,"revisedAt":8,"title":9,"author":10,"content":11,"slug":12},"zbj0dlhc8lx5","2026-04-26T09:03:51.655Z","2026-04-26T09:04:10.416Z","GDC 20 Games Workshop | Jetpack Joyride","Bogay","\u003Ch1 id=\"h7c9cf73555\">用 Godot 4 製作 Jetpack Joyride\u003C/h1>\u003Ch2 id=\"h31fc5cbc4a\">事前準備\u003C/h2>\u003Cp>從 \u003Ca href=\"https://godotengine.org/download\" rel=\"nofollow\">https://godotengine.org/download\u003C/a> 下載 \u003Cstrong>Godot 4\u003C/strong>\u003C/p>\u003Ch2 id=\"h82e8465a1e\">Jetpack Joyride 簡介\u003C/h2>\u003Cblockquote>\u003Cp>Jetpack Joyride is a side-scrolling endless mobile game from 2011. It only requires a single input button to control the player. The game is fairly complex overall, though the basic premise is very simple. The game came from the same studio that made Fruit Ninja.\u003C/p>\u003Cp>The game features a character with a machine-gun jetpack. When holding the input, the player will rise (and destroy everything below!) When the input is released, the character will fall. The character can run on the ground if they reach the bottom of the screen.\u003C/p>\u003Cp>\u003Ca href=\"https://20_games_challenge.gitlab.io/games/jetpack/\" rel=\"nofollow\">Jetpack Joyride | 20 Games Challenge\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>YT 介紹（二代）：\u003Ca href=\"https://www.youtube.com/watch?v=1myElNV4MG0\" rel=\"nofollow\">https://www.youtube.com/watch?v=1myElNV4MG0\u003C/a>\u003C/p>\u003Ch2 id=\"hcb4c21d118\">Godot 簡介\u003C/h2>\u003Cblockquote>\u003Cp>關於更完整的介紹，建議參考官方文件：\u003Ca href=\"https://docs.godotengine.org/en/stable/getting_started/introduction/introduction_to_godot.html\" rel=\"nofollow\">Intorduction to Godot\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>Godot 是一款開源的遊戲引擎，可以做 2D 或 3D 遊戲。編輯器本身支援許多平台，包含 PC / android / web / VR，並且相當輕量（相較於 Unity / UE），對於老舊裝置也很友善。 在 Godot 中，大部分腳本使用 \u003Cstrong>GDScript\u003C/strong> 撰寫，語法與 Python 相似，專為 Godot 設計。但如果熟悉 Unity 的話，Godot 也支援使用 C# 當做腳本語言，甚至透過 GDExtension 的機制，你也可以使用其他的程式語言（門檻較高）。 比較多是獨立遊戲在使用，但也有一些大作，可參考\u003Ca href=\"https://godotengine.org/showcase/\" rel=\"nofollow\">官方 showcase\u003C/a> 上面的列表。台灣近幾年比較有名的案例是文字遊戲，他們在 TGDF 有分享過相關敬驗：\u003Ca href=\"https://www.youtube.com/watch?v=VX7i7TNSm88\" rel=\"nofollow\">[2022 TGDF] 2D 遊戲開發，除了 Unity 你還可以考慮試試 Godot！以《文字遊戲》為例 (張文瀚)\u003C/a>。\u003C/p>\u003Ch2 id=\"hb2dd78ca85\">建立第一個場景\u003C/h2>\u003Ch3 id=\"hb743f44db7\">建立新專案\u003C/h3>\u003Col>\u003Cli>開啟 Godot -&gt; 點選左上角的 \u003Cstrong>Create\u003C/strong>\u003C/li>\u003Cli>Project Name 輸入 \u003Ccode>jetpack-joyride\u003C/code>，然後將 Project Path 修改成自己習慣的位置\u003C/li>\u003Cli>Rendere 選擇 \u003Cstrong>Compatibility\u003C/strong>（適用於 2D 遊戲，相容性最廣，可以將遊戲發佈到網頁上）\u003C/li>\u003Cli>點選 \u003Cstrong>Create\u003C/strong>\u003C/li>\u003Cli>前往 \u003Cstrong>Project → Project Settings → Display → Window\u003C/strong>，設定：\u003Cul>\u003Cli>Width: \u003Ccode>1280\u003C/code>，Height: \u003Ccode>720\u003C/code>\u003C/li>\u003C/ul>\u003C/li>\u003C/ol>\u003Ch3 id=\"hb122743400\">編輯器介面\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/getting_started/introduction/first_look_at_the_editor.html\" rel=\"nofollow\">Godot 編輯器介面總覽\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>開啟 Godot 後，你會看到四個主要面板：\u003C/p>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>面板\u003C/p>\u003C/th>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>用途\u003C/p>\u003C/th>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Cstrong>Scene\u003C/strong>（左上）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>目前場景的節點樹狀結構\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Cstrong>Inspector\u003C/strong>（右側）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>被選取節點的屬性\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Cstrong>FileSystem\u003C/strong>（左下）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>專案中的所有檔案\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Cstrong>Viewport\u003C/strong>（中央）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>場景的視覺預覽\u003C/p>\u003C/td>\u003C/tr>\u003C/tbody>\u003C/table>\u003Ch3 id=\"hbe5bccbdd0\">節點（Node）與場景（Scene）\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/getting_started/step_by_step/nodes_and_scenes.html\" rel=\"nofollow\">節點與場景\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>Godot 中的一切都是\u003Cstrong>節點 (Node)\u003C/strong>。節點是遊戲的基本積木 --- 每個節點只負責一件事（e.g. 顯示圖片、播放音效、偵測碰撞...）。 我們可以將節點掛到另一個節點底下成為它的子節點，以該專案為例，玩家就會有顯示圖片 (\u003Ccode>Sprite2D\u003C/code>) 跟碰撞 (\u003Ccode>Area2D\u003C/code>, \u003Ccode>CollisionShape2D\u003C/code>) 等功能，這些節點全部組裝在一起，就可以提供更複雜的功能。 這樣由多個節點組成的集合就可以被視為一顆樹，而\u003Cstrong>場景 (Scene)\u003C/strong> 在 Godot 中就是一棵儲存起來的節點樹，可以重複使用，類似 Unity 的 prefab，而節點就類似 Unity 中的 component，負責提供特定功能。\u003C/p>\u003Cp>在剛建立的 Godot 專案中，我們並沒有任何場景存在，因此我們可以先尋找編輯器左側的 Scene 面板，點選 &quot;2D Scene&quot; 創建一個空的 2D 場景，然後按下 Ctrl + s，將它另存為 \u003Ccode>main.tscn\u003C/code>，做為我們遊戲的主要場景。 儲存後可以按下 F5，或是點選編輯器右上角的 &quot;Run Project&quot; 按鈕測試專案，Godot 會跳出視窗要求設定 main scene，也就是專案預設要載入的場景。選擇 Select Current 後就可以看到遊戲預設的空白畫面了。 如果之後有需要更改 main scene 的話，也可以在 FileSystem 面板中尋找想要修改的場景右鍵後選擇 &quot;Set as Main Scene&quot;。\u003C/p>\u003Ch2 id=\"hf22e5a1cd9\">背景與場景設定\u003C/h2>\u003Ch3 id=\"hd6465611d2\">加入捲動背景\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/2d/2d_parallax.html\" rel=\"nofollow\">Parallax2D 自動捲動背景\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>背景使用 \u003Ccode>Parallax2D\u003C/code> 節點實現自動橫向捲動效果。\u003C/p>\u003Col>\u003Cli>右鍵點擊 \u003Ccode>main\u003C/code> -&gt; \u003Cstrong>Add Child Node\u003C/strong> -&gt; \u003Ccode>Parallax2D\u003C/code>，重新命名為 \u003Ccode>background\u003C/code>\u003C/li>\u003Cli>在 Inspector 設定：\u003Cul>\u003Cli>\u003Cstrong>Repeat Size\u003C/strong>：\u003Ccode>Vector2(1024, 0)\u003C/code>（水平方向重複拼接）\u003C/li>\u003Cli>\u003Cstrong>Autoscroll\u003C/strong>：\u003Ccode>Vector2(-64, 0)\u003C/code>（每秒向左捲動 64 px）\u003C/li>\u003Cli>\u003Cstrong>Repeat Times\u003C/strong>：\u003Ccode>2\u003C/code>\u003C/li>\u003C/ul>\u003C/li>\u003Cli>在 \u003Ccode>background\u003C/code> 下新增子節點 \u003Ccode>Sprite2D\u003C/code>\u003C/li>\u003Cli>將背景貼圖拖入 \u003Cstrong>Texture\u003C/strong> 欄位；若暫時沒有素材，可先使用專案內建的 \u003Ccode>icon.svg\u003C/code> 做為佔位符\u003C/li>\u003Cli>調整 \u003Ccode>Sprite2D\u003C/code> 的 \u003Cstrong>Position\u003C/strong> 與 \u003Cstrong>Scale\u003C/strong>，使其覆蓋整個畫面（約 \u003Ccode>scale = (9.85, 7.15)\u003C/code>）\u003C/li>\u003C/ol>\u003Cp>\u003Ccode>Parallax2D\u003C/code> 會讓背景圖無縫循環捲動，只需設定 \u003Ccode>Autoscroll\u003C/code> 速度，Godot 便自動處理重複拼接，不需要任何程式碼。\u003C/p>\u003Ch3 id=\"hbff91801fd\">加入 UI 層\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/2d/canvas_layers.html#canvaslayers\" rel=\"nofollow\">CanvasLayer 與 UI 渲染層\u003C/a>、\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/ui/size_and_anchors.html\" rel=\"nofollow\">Control 節點的 Anchor 系統\u003C/a>\u003C/p>\u003C/blockquote>\u003Col>\u003Cli>在 \u003Ccode>main\u003C/code> 下新增子節點 → \u003Ccode>CanvasLayer\u003C/code>，重新命名為 \u003Ccode>UI\u003C/code>\u003C/li>\u003Cli>在 \u003Ccode>UI\u003C/code> 內新增子節點 \u003Ccode>Label\u003C/code>\u003C/li>\u003Cli>在 Inspector 將 Label 的 \u003Cstrong>Anchors Preset\u003C/strong> 設為 \u003Cstrong>Top Right\u003C/strong>（錨點在右上角）\u003C/li>\u003Cli>調整 \u003Cstrong>Offset\u003C/strong> 讓文字略微離開邊緣\u003C/li>\u003Cli>\u003Cstrong>Theme Overrides → Font Sizes → Font Size\u003C/strong>：\u003Ccode>32\u003C/code>\u003C/li>\u003C/ol>\u003Cp>\u003Ccode>CanvasLayer\u003C/code> 會讓 UI 固定在畫面上，即使遊戲世界的攝影機移動，UI 也不會跟著偏移。\u003C/p>\u003Ch3 id=\"h6a5ba9dfef\">加入地板與天花板\u003C/h3>\u003Cp>遊戲場地需要上下兩道邊界，防止玩家飛出畫面：\u003C/p>\u003Cp>\u003Cstrong>地板（ground）\u003C/strong>\u003C/p>\u003Col>\u003Cli>在 \u003Ccode>main.tscn\u003C/code> 新增 \u003Ccode>StaticBody2D\u003C/code> 節點，命名為 \u003Ccode>ground\u003C/code>\u003C/li>\u003Cli>移動到畫面底部\u003C/li>\u003Cli>在底下新增 \u003Ccode>ColorRect\u003C/code>，用來顯示地板外觀。新增後設定成喜歡的顏色並調整 Anchors Preset 為 &quot;Center&quot;，調整大小讓它可以覆蓋畫面左右兩側 Note: 調整大小時先按住 Alt 再拖動可以維持中心點\u003C/li>\u003Cli>在底下新增 \u003Ccode>CollisionShape2D\u003C/code>，並在 \u003Cstrong>Shape\u003C/strong> 欄位新增一個 \u003Cstrong>RectangleShape2D\u003C/strong>，將大小調整到跟剛剛新增的 ColorRect 差不多\u003C/li>\u003C/ol>\u003Cp>\u003Cstrong>天花板（ceil）\u003C/strong>\u003C/p>\u003Col>\u003Cli>將 \u003Ccode>ground\u003C/code> 複製一份，並重新命名為 \u003Ccode>ceil\u003C/code>\u003C/li>\u003Cli>往上移動到畫面上方\u003C/li>\u003C/ol>\u003Cp>這時候兩個 node 裡面的 RectangleShape2D 其實是共用的，所以調整其中一邊的話也會影響到另外一個，如果希望可以讓兩邊的設定分開，可以選擇其中一邊 Shape 欄位的下拉選單，點選 &quot;Make Unique&quot;，\u003C/p>\u003Ch2 id=\"hdd05e05de7\">設定玩家節點\u003C/h2>\u003Cblockquote>\u003Cp>\u003Cstrong>官方文件參考\u003C/strong>\u003C/p>\u003Cul>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/physics/using_character_body_2d.html\" rel=\"nofollow\">使用 CharacterBody2D\u003C/a>\u003C/li>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html\" rel=\"nofollow\">InputEvent 與輸入處理\u003C/a>\u003C/li>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html\" rel=\"nofollow\">Area2D 碰撞偵測\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003Ch3 id=\"h1985fade3c\">在主場景中新增玩家\u003C/h3>\u003Cp>本專案已提供主角的美術素材，位於 \u003Ccode>sprite/\u003C/code> 資料夾，包含三種狀態：\u003C/p>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>檔案\u003C/p>\u003C/th>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>用途\u003C/p>\u003C/th>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>sprite/meatball_walk.png\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>站在地面時\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>sprite/meatball_fly.png\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>噴射飛行時\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>sprite/meatball_hurt.png\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>碰到障礙物時\u003C/p>\u003C/td>\u003C/tr>\u003C/tbody>\u003C/table>\u003Cp>玩家直接在 \u003Ccode>main.tscn\u003C/code> 內建立，不需要獨立的場景檔案：\u003C/p>\u003Col>\u003Cli>開啟 \u003Ccode>main.tscn\u003C/code>，右鍵點擊 \u003Ccode>main\u003C/code> → \u003Cstrong>Add Child Node\u003C/strong> → \u003Ccode>CharacterBody2D\u003C/code>，命名為 \u003Ccode>player\u003C/code>\u003C/li>\u003Cli>在 \u003Ccode>player\u003C/code> 下新增兩組子節點：\u003Cul>\u003Cli>\u003Ccode>Sprite2D\u003C/code> — 顯示角色圖片，\u003Cstrong>Texture\u003C/strong> 拖入 \u003Ccode>sprite/meatball_walk.png\u003C/code>\u003C/li>\u003Cli>\u003Ccode>CollisionShape2D\u003C/code> — 賦予玩家物理碰撞形狀，在 \u003Cstrong>Shape\u003C/strong> 欄位新增 \u003Cstrong>RectangleShape2D\u003C/strong> 並調整為合適的大小\u003C/li>\u003C/ul>\u003C/li>\u003Cli>將 \u003Ccode>player\u003C/code> 移動到合適的起始位置（畫面左下角，略高於地板）\u003C/li>\u003C/ol>\u003Cblockquote>\u003Cp>\u003Cstrong>CharacterBody2D\u003C/strong> 是 Godot 用來製作可控制角色的節點。它處理沿地板與牆壁移動的邏輯，但物理計算由你的程式碼負責。\u003C/p>\u003C/blockquote>\u003Ch3 id=\"h3d243516f3\">撰寫 player.gd\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html\" rel=\"nofollow\">GDScript 基礎\u003C/a>、\u003Ca href=\"https://gdquest.github.io/learn-gdscript/\" rel=\"nofollow\">Learn GDScript from Zero\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>以下程式碼是一個簡單的 GDScript 範例：\u003C/p>\u003Cpre>\u003Ccode>extends Node2D          # 這個腳本掛載到哪種節點上\n\nvar score := 0.0        # 宣告變數（自動推斷型別）\nvar name: String = &quot;Barry&quot;  # 明確標注型別\n\n# _ready 在節點第一次出現於場景時執行一次\nfunc _ready() -&gt; void:\n    print(&quot;Hello, world!&quot;)\n\n# _process 每個畫格執行一次；delta = 距離上一畫格的秒數\nfunc _process(delta: float) -&gt; void:\n    score += delta      # 將經過時間累加到分數\u003C/code>\u003C/pre>\u003Cp>現在，我們要替玩家掛上腳本。右鍵點擊 \u003Ccode>player\u003C/code> → \u003Cstrong>Attach Script\u003C/strong> → 儲存為 \u003Ccode>player.gd\u003C/code>。 我們會分幾個階段逐步完成這個腳本。\u003C/p>\u003Ch4 id=\"h4a66855547\">階段一：讓玩家受到重力下墜\u003C/h4>\u003Cp>第一步先實作最基礎的物理：重力。玩家應該要自然往下掉，並且不能穿透地板。\u003C/p>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\n@export var gravity: float = 600   # 每秒向下加速的量（px/s²）\n\nfunc _physics_process(delta: float) -&gt; void:\n    velocity.y += gravity * delta   # 重力每幀累積速度\n    move_and_slide()                # 移動並自動處理與地板的碰撞\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Ccode>_physics_process\u003C/code> vs \u003Ccode>_process\u003C/code> 物理相關的更新要放在 \u003Ccode>_physics_process\u003C/code>，它的呼叫頻率固定（預設 60 Hz），不受畫面更新率影響，讓物理模擬更穩定。\u003C/p>\u003Cp>\u003Ccode>move_and_slide()\u003C/code> 這是 \u003Ccode>CharacterBody2D\u003C/code> 提供的方法，會依照 \u003Ccode>velocity\u003C/code> 移動角色，並自動處理與靜態物體（如地板）的碰撞，不需要自己寫碰撞解析。\u003C/p>\u003C/blockquote>\u003Cp>按下 \u003Cstrong>F5\u003C/strong> 執行遊戲，此時角色應該會從起始位置自由落下，落到地板上停住。\u003C/p>\u003Ch4 id=\"h34cdc9afc1\">階段二：設定輸入，讓玩家可以噴射飛行\u003C/h4>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html\" rel=\"nofollow\">Input Map 設定\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>在 Godot 中，我們會使用 \u003Cstrong>Input Map\u003C/strong> 替輸入做抽象化——先替動作命名，再綁定實體按鍵，程式碼只需要詢問「動作有沒有被觸發」，不需要直接寫死按鍵名稱。 這樣的作法讓跨平台輸入變得容易，類似於 Unity 較新的 Input System。\u003C/p>\u003Col>\u003Cli>\u003Cstrong>Project → Project Settings → Input Map\u003C/strong>\u003C/li>\u003Cli>在「Add New Action」欄位輸入 \u003Ccode>fly\u003C/code> → \u003Cstrong>Add\u003C/strong>\u003C/li>\u003Cli>點選 \u003Ccode>fly\u003C/code> 旁的 \u003Ccode>+\u003C/code> → 按下\u003Cstrong>空白鍵\u003C/strong> → \u003Cstrong>OK\u003C/strong>\u003C/li>\u003C/ol>\u003Cp>接著在腳本中加入輸入判斷：\u003C/p>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\n@export var gravity: float = 600\n@export var acceleration: float = 800   # 噴射時的向上加速力\n\nfunc _physics_process(delta: float) -&gt; void:\n    if Input.is_action_pressed(&quot;fly&quot;):\n        velocity.y -= acceleration * delta   # 向上加速（y 軸向下為正）\n    else:\n        velocity.y += gravity * delta\n\n    move_and_slide()\u003C/code>\u003C/pre>\u003Cp>執行遊戲，按住空白鍵應該可以讓角色上升，放開後下墜。\u003C/p>\u003Ch4 id=\"hf49fe8eb9a\">階段三：限制最大上升速度\u003C/h4>\u003Cp>目前持續按住空白鍵的話，角色速度會無限增加，飛出畫面。我們加入速度上限，讓手感更可控：\u003C/p>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\nconst MAX_FLY_SPEED = 600   # 向上的速度上限（px/s）\n\n@export var gravity: float = 600\n@export var acceleration: float = 800\n\nfunc _physics_process(delta: float) -&gt; void:\n    if Input.is_action_pressed(&quot;fly&quot;):\n        # move_toward(當前值, 目標值, 最大步進量)：平滑趨近目標，不會超過\n        velocity.y = move_toward(velocity.y, -MAX_FLY_SPEED, acceleration * delta)\n    else:\n        velocity.y += gravity * delta\n\n    move_and_slide()\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Cstrong>為什麼速度上限是負數？\u003C/strong> 在 Godot 的 2D 座標系中，y 軸向下為正。向上飛行的速度是負值，所以上限用 \u003Ccode>-MAX_FLY_SPEED\u003C/code> 表示「向上最多這麼快」。\u003C/p>\u003Cp>\u003Ccode>move_toward(from, to, delta)\u003C/code> 讓數值從 \u003Ccode>from\u003C/code> 平滑趨向 \u003Ccode>to\u003C/code>，每次最多移動 \u003Ccode>delta\u003C/code>，永遠不會超過目標值。這裡用來讓加速過程更流暢，而不是瞬間跳到上限。\u003C/p>\u003C/blockquote>\u003Ch4 id=\"h222ae4f585\">階段四：依狀態切換貼圖\u003C/h4>\u003Cp>玩家有三種素材對應不同狀態。我們用 \u003Ccode>@export\u003C/code> 讓素材可以在 Inspector 中直接指定，再根據目前的輸入切換 \u003Ccode>Sprite2D\u003C/code> 的貼圖：\u003C/p>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\nconst MAX_FLY_SPEED = 600\n\n@export var gravity: float = 600\n@export var acceleration: float = 800\n\n@export var walk_tex: Texture2D   # 一般行走貼圖\n@export var fly_tex: Texture2D    # 噴射飛行貼圖\n@export var hurt_tex: Texture2D   # 碰到障礙物貼圖\n\n@onready var sprite_2d = $Sprite2D   # 取得子節點的參考\n\nfunc _physics_process(delta: float) -&gt; void:\n    if Input.is_action_pressed(&quot;fly&quot;):\n        velocity.y = move_toward(velocity.y, -MAX_FLY_SPEED, acceleration * delta)\n        sprite_2d.texture = fly_tex\n    else:\n        velocity.y += gravity * delta\n        sprite_2d.texture = walk_tex\n\n    move_and_slide()\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Ccode>@export\u003C/code> 加上這個標記的變數會出現在 Inspector 面板，可以直接在編輯器中拖入素材，不需要寫死路徑在程式碼裡。\u003C/p>\u003Cp>\u003Ccode>@onready\u003C/code> 確保在節點完全加入場景後才取得子節點的參考。如果省略，在 \u003Ccode>_ready()\u003C/code> 執行之前存取子節點可能會得到 \u003Ccode>null\u003C/code>。\u003C/p>\u003C/blockquote>\u003Cp>設定完成後，在 Inspector 中將 \u003Ccode>walk_tex\u003C/code>、\u003Ccode>fly_tex\u003C/code>、\u003Ccode>hurt_tex\u003C/code> 分別拖入對應的 png 檔案，執行遊戲確認貼圖會跟著狀態切換。\u003C/p>\u003Cblockquote>\u003Cp>遊戲的主角是一顆貢丸，這是因為我在準備美術素材的時候玩到竹梅賽做的\u003Ca href=\"https://galgame-5c440.web.app/\" rel=\"nofollow\">嘎拉給木\u003C/a>\u003C/p>\u003C/blockquote>\u003Ch4 id=\"h0ff3caa92e\">階段五：縮放動畫（Squash &amp; Stretch）\u003C/h4>\u003Cp>只切換貼圖已經有基本的視覺回饋，但我們可以再加一點「果凍感」——用縮放讓角色看起來更有重量與彈性：\u003C/p>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>狀態\u003C/p>\u003C/th>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>效果\u003C/p>\u003C/th>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>縮放值\u003C/p>\u003C/th>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>噴射飛行\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>垂直拉伸（Stretch）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>Vector2(0.9, 1.1)\u003C/code>\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>站在地板\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>水平方向週期性擠壓（Squash）\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>隨時間正弦波動\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>空中下墜\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>恢復原始比例\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>Vector2(1.0, 1.0)\u003C/code>\u003C/p>\u003C/td>\u003C/tr>\u003C/tbody>\u003C/table>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\nconst MAX_FLY_SPEED = 600\n\n@export var gravity: float = 600\n@export var acceleration: float = 800\n\n@export var walk_tex: Texture2D\n@export var fly_tex: Texture2D\n@export var hurt_tex: Texture2D\n\n@onready var sprite_2d = $Sprite2D\n\nfunc _physics_process(delta: float) -&gt; void:\n    if Input.is_action_pressed(&quot;fly&quot;):\n        velocity.y = move_toward(velocity.y, -MAX_FLY_SPEED, acceleration * delta)\n        sprite_2d.texture = fly_tex\n        sprite_2d.scale = Vector2(0.9, 1.1)   # 飛行時：橫壓縱拉\n    else:\n        velocity.y += gravity * delta\n        sprite_2d.texture = walk_tex\n        if is_on_floor():\n            # 站在地板時：用正弦波產生輕微的呼吸感擠壓\n            var squash = sin(Time.get_ticks_msec() * 0.005) * 0.05\n            sprite_2d.scale = Vector2(1.0 + squash, 1.0 - squash)\n        else:\n            sprite_2d.scale = Vector2(1.0, 1.0)   # 下墜時：恢復原始比例\n\n    move_and_slide()\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Cstrong>Squash &amp; Stretch\u003C/strong> 這是傳統動畫的\u003Ca href=\"https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation\" rel=\"nofollow\">十二原則\u003C/a>之一，由迪士尼動畫師 Ollie Johnston 與 Frank Thomas 在 1981 年的著作《The Illusion of Life》中提出。壓縮（squash）和拉伸（stretch）能讓物體看起來有質量與彈性，即使只是改變縮放比例，也能大幅提升遊戲的「手感」。\u003C/p>\u003Cp>\u003Ccode>sin(Time.get_ticks_msec() * 0.005) * 0.05\u003C/code> \u003Ccode>Time.get_ticks_msec()\u003C/code> 回傳遊戲啟動至今的毫秒數，乘上一個小係數後傳入 \u003Ccode>sin()\u003C/code>，就得到一個在 -1 到 1 之間平滑往復的值。再乘上 \u003Ccode>0.05\u003C/code> 縮小振幅，讓縮放變化維持在 ±5% 以內，細膩而不誇張。\u003C/p>\u003Cp>\u003Ccode>is_on_floor()\u003C/code> \u003Ccode>CharacterBody2D\u003C/code> 在呼叫 \u003Ccode>move_and_slide()\u003C/code> 後會自動更新這個狀態，可以直接判斷角色是否落地。\u003C/p>\u003C/blockquote>\u003Cp>執行遊戲，觀察三種狀態下角色縮放的細微變化。\u003C/p>\u003Ch4 id=\"h8e27764ced\">階段六：偵測與障礙物的碰撞\u003C/h4>\u003Cp>最後，我們要讓玩家能夠感知到自己碰到了障礙物。\u003Ccode>CharacterBody2D\u003C/code> 的碰撞只能偵測到靜態物體（如地板），對於需要「重疊感知」的障礙物，我們改用 \u003Ccode>Area2D\u003C/code>。\u003C/p>\u003Cp>先在 \u003Ccode>player\u003C/code> 節點下新增子節點：\u003C/p>\u003Cul>\u003Cli>\u003Ccode>Area2D\u003C/code>（內含一個 \u003Ccode>CollisionShape2D\u003C/code>）— 形狀同樣用 RectangleShape2D，略小於外層的碰撞框即可\u003C/li>\u003C/ul>\u003Cp>接著更新腳本，加入訊號宣告與碰撞處理：\u003C/p>\u003Cpre>\u003Ccode>class_name Player\nextends CharacterBody2D\n\nconst MAX_FLY_SPEED = 600\n\n@export var gravity: float = 600\n@export var acceleration: float = 800\n\n@export var walk_tex: Texture2D\n@export var fly_tex: Texture2D\n@export var hurt_tex: Texture2D\n\n@onready var sprite_2d = $Sprite2D\n\nsignal player_died   # 玩家碰到障礙物時發出此訊號\n\nfunc _ready() -&gt; void:\n    # 將 Area2D 的 area_entered 訊號連接到我們的處理函式\n    $Area2D.area_entered.connect(_on_area_entered)\n\nfunc _physics_process(delta: float) -&gt; void:\n    if Input.is_action_pressed(&quot;fly&quot;):\n        velocity.y = move_toward(velocity.y, -MAX_FLY_SPEED, acceleration * delta)\n        sprite_2d.texture = fly_tex\n    else:\n        velocity.y += gravity * delta\n        sprite_2d.texture = walk_tex\n\n    move_and_slide()\n\nfunc _on_area_entered(area: Area2D) -&gt; void:\n    if area.owner.is_in_group(&quot;obstacle&quot;):\n        player_died.emit()\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Cstrong>訊號（Signal）\u003C/strong> \u003Ccode>signal player_died\u003C/code> 宣告一個自訂事件。當玩家碰到障礙物時，用 \u003Ccode>player_died.emit()\u003C/code> 發出訊號；其他節點（例如 \u003Ccode>main.gd\u003C/code>）只需連接這個訊號，就能在適當時機做出回應（例如切換到遊戲結束畫面）。這讓 \u003Ccode>player.gd\u003C/code> 不需要知道遊戲其他部分的存在。\u003C/p>\u003C/blockquote>\u003Cp>以上都設定完成後，應該就可以看到角色正常移動了。\u003C/p>\u003Ch2 id=\"hf19c9ab69c\">障礙物\u003C/h2>\u003Cblockquote>\u003Cp>\u003Cstrong>官方文件參考\u003C/strong>\u003C/p>\u003Cul>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html\" rel=\"nofollow\">Area2D 碰撞偵測\u003C/a>\u003C/li>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/tutorials/scripting/groups.html\" rel=\"nofollow\">使用群組（Groups）\u003C/a>\u003C/li>\u003Cli>\u003Ca href=\"https://docs.godotengine.org/en/stable/classes/class_visibleonscreennotifier2d.html\" rel=\"nofollow\">VisibleOnScreenNotifier2D 類別說明\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003Cp>本遊戲有兩種障礙物：公車（\u003Ccode>obstacle.tscn\u003C/code>）與辣椒醬（\u003Ccode>rocket.tscn\u003C/code>）。兩者共用同一份腳本 \u003Ccode>obstacle.gd\u003C/code>，僅速度與外觀不同。\u003C/p>\u003Ch3 id=\"ha3a5650e9c\">訊號（Signals）是什麼？\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html\" rel=\"nofollow\">訊號（Signals）使用教學\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>在開始撰寫腳本之前，先介紹一個即將用到的核心概念。\u003C/p>\u003Cp>訊號是 Godot 內建的事件系統。節點在某事發生時**發出（emit）\u003Cstrong>訊號；任何\u003C/strong>已連接（connected）**到該訊號的節點都會執行對應的函式。\u003C/p>\u003Cpre>\u003Ccode>VisibleOnScreenNotifier2D 偵測到節點離開畫面\n        │ 發出 &quot;screen_exited&quot;\n        └──► obstacle.gd 接收到 → 呼叫 queue_free 刪除自己\n\u003C/code>\u003C/pre>\u003Cp>這讓每個節點只負責自己的事，不需要在外部寫輪詢邏輯來判斷「這個東西有沒有跑出畫面」。\u003C/p>\u003Ch3 id=\"h6f9930995e\">撰寫 obstacle.gd（共用腳本）\u003C/h3>\u003Cp>先建立腳本，兩個場景都會使用它。右鍵根節點 → \u003Cstrong>Attach Script\u003C/strong> → 儲存為 \u003Ccode>obstacle.gd\u003C/code>。\u003C/p>\u003Ch4 id=\"h430a7f4fc4\">階段一：讓障礙物往左移動\u003C/h4>\u003Cpre>\u003Ccode>extends Node2D\n\n@export var speed: float = 64   # 每秒向左移動的像素數\n\nfunc _process(delta: float) -&gt; void:\n    position += Vector2.LEFT * (speed * delta)\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Ccode>Vector2.LEFT\u003C/code> 等同於 \u003Ccode>Vector2(-1, 0)\u003C/code>。在 Godot 的 2D 座標系中，x 軸向右為正，所以向左移動要使用負 x。\u003Ccode>Vector2.LEFT\u003C/code> / \u003Ccode>Vector2.RIGHT\u003C/code> / \u003Ccode>Vector2.UP\u003C/code> / \u003Ccode>Vector2.DOWN\u003C/code> 是 Godot 提供的常數，讓方向語意更清晰。\u003C/p>\u003C/blockquote>\u003Cp>此時可以將障礙物場景暫時拖入 \u003Ccode>main.tscn\u003C/code> 測試，應該可以看到它緩緩往左移動。但有個問題：它會一直移動下去，\u003Cstrong>永遠不會消失\u003C/strong>，長期下來會佔用越來越多記憶體。\u003C/p>\u003Ch4 id=\"h07a11e3672\">階段二：離開畫面後自動刪除\u003C/h4>\u003Cp>先在場景中加入 \u003Ccode>VisibleOnScreenNotifier2D\u003C/code> 子節點：\u003C/p>\u003Cul>\u003Cli>選取它後，在 Viewport 中調整橘色的 \u003Cstrong>Rect\u003C/strong> 邊框，使其略大於障礙物的 Sprite，確保障礙物完全移出畫面後才觸發。\u003C/li>\u003C/ul>\u003Cp>接著更新腳本，在 \u003Ccode>_ready()\u003C/code> 中連接訊號：\u003C/p>\u003Cpre>\u003Ccode>extends Node2D\n\n@export var speed: float = 64\n\nfunc _ready() -&gt; void:\n    # 障礙物完全離開畫面時，發出 screen_exited 訊號，觸發 queue_free 刪除自己\n    $VisibleOnScreenNotifier2D.screen_exited.connect(queue_free)\n\nfunc _process(delta: float) -&gt; void:\n    position += Vector2.LEFT * (speed * delta)\u003C/code>\u003C/pre>\u003Cblockquote>\u003Cp>\u003Ccode>queue_free\u003C/code> 將節點排入「待刪除」佇列，在當前幀結束後安全地從場景樹移除並釋放記憶體。不能直接呼叫 \u003Ccode>free()\u003C/code> 是因為在物理或訊號處理期間節點可能仍在被參考，強制刪除會造成錯誤；\u003Ccode>queue_free\u003C/code> 會等到安全時機再執行。\u003C/p>\u003C/blockquote>\u003Ch3 id=\"h45185960ea\">建立公車場景\u003C/h3>\u003Cp>本專案已提供公車的美術素材 \u003Ccode>sprite/bus.png\u003C/code>，可直接使用。\u003C/p>\u003Col>\u003Cli>\u003Cstrong>Scene → New Scene\u003C/strong>，根節點選 \u003Ccode>Node2D\u003C/code>，命名為 \u003Ccode>Obstacle\u003C/code>，儲存為 \u003Ccode>obstacle.tscn\u003C/code>\u003C/li>\u003Cli>在根節點設定 \u003Ccode>Scale = (0.6, 0.6)\u003C/code> 縮小整體大小\u003C/li>\u003Cli>新增以下子節點：\u003Cul>\u003Cli>\u003Ccode>Sprite2D\u003C/code> — \u003Cstrong>Texture\u003C/strong> 拖入 \u003Ccode>sprite/bus.png\u003C/code>\u003C/li>\u003Cli>\u003Ccode>Area2D\u003C/code> — 偵測與玩家的重疊\u003Cul>\u003Cli>在 \u003Ccode>Area2D\u003C/code> 內新增 \u003Ccode>CollisionPolygon2D\u003C/code>，用來描繪不規則的公車外型碰撞區域\u003C/li>\u003C/ul>\u003C/li>\u003Cli>\u003Ccode>VisibleOnScreenNotifier2D\u003C/code> — 偵測節點何時離開畫面\u003C/li>\u003C/ul>\u003C/li>\u003Cli>掛上 \u003Ccode>obstacle.gd\u003C/code> 腳本（根節點右鍵 → \u003Cstrong>Attach Script\u003C/strong> → 選擇現有的 \u003Ccode>obstacle.gd\u003C/code>）\u003C/li>\u003Cli>選取根節點 \u003Ccode>Obstacle\u003C/code> → Inspector → \u003Cstrong>Node → Groups\u003C/strong> → 輸入 \u003Ccode>obstacle\u003C/code> → \u003Cstrong>Add\u003C/strong>\u003C/li>\u003C/ol>\u003Cp>\u003Cstrong>如何編輯 CollisionPolygon2D\u003C/strong>\u003C/p>\u003Cp>選取 \u003Ccode>CollisionPolygon2D\u003C/code> 節點後，Viewport 上方工具列會出現多邊形編輯模式：\u003C/p>\u003Cul>\u003Cli>\u003Cstrong>左鍵點擊\u003C/strong>畫面上的空白處：新增頂點\u003C/li>\u003Cli>\u003Cstrong>左鍵拖曳\u003C/strong>已存在的頂點：移動位置\u003C/li>\u003Cli>\u003Cstrong>右鍵點擊\u003C/strong>已存在的頂點：刪除該點\u003C/li>\u003C/ul>\u003Cp>沿著公車的外輪廓依序點下頂點，圍成一個貼合車身的多邊形即可。不需要非常精確，略微內縮一點反而能讓遊戲判定更寬鬆、體驗更好。\u003C/p>\u003Cblockquote>\u003Cp>\u003Cstrong>為什麼要加入 \u003C/strong>\u003Ccode>obstacle\u003C/code> 群組？ \u003Ccode>player.gd\u003C/code> 在偵測到 \u003Ccode>Area2D\u003C/code> 重疊時，會用 \u003Ccode>area.owner.is_in_group(&quot;obstacle&quot;)\u003C/code> 來判斷碰到的是不是障礙物——因為未來場景中可能有其他 \u003Ccode>Area2D\u003C/code>（如金幣），不能把所有重疊都當成死亡事件。群組（Groups）就是 Godot 裡最簡單的「標籤」系統。\u003C/p>\u003C/blockquote>\u003Ch3 id=\"h27f43a480b\">建立東泉辣椒醬場景\u003C/h3>\u003Cp>辣椒醬是快速橫飛的第二種障礙物，使用 \u003Ccode>sprite/chili_sauce.png\u003C/code>。\u003C/p>\u003Col>\u003Cli>\u003Cstrong>Scene → New Scene\u003C/strong>，根節點選 \u003Ccode>Node2D\u003C/code>，命名為 \u003Ccode>Rocket\u003C/code>，儲存為 \u003Ccode>rocket.tscn\u003C/code>\u003C/li>\u003Cli>新增以下子節點：\u003Cul>\u003Cli>\u003Ccode>Sprite2D\u003C/code> — \u003Cstrong>Texture\u003C/strong> 拖入 \u003Ccode>sprite/chili_sauce.png\u003C/code>；設定根節點 \u003Ccode>Scale = (0.3, 0.3)\u003C/code>\u003C/li>\u003Cli>\u003Ccode>Area2D\u003C/code> — 偵測與玩家的重疊\u003Cul>\u003Cli>在 \u003Ccode>Area2D\u003C/code> 內新增 \u003Ccode>CollisionShape2D\u003C/code>，形狀選 \u003Cstrong>New RectangleShape2D\u003C/strong>；調整覆蓋辣椒醬的主體（約 \u003Ccode>734×124\u003C/code>，略向左偏移 \u003Ccode>x = -69\u003C/code>）\u003C/li>\u003C/ul>\u003C/li>\u003Cli>\u003Ccode>VisibleOnScreenNotifier2D\u003C/code> — 偵測節點何時離開畫面，同樣調整 Rect 略大於 Sprite\u003C/li>\u003C/ul>\u003C/li>\u003Cli>掛上 \u003Ccode>obstacle.gd\u003C/code> 腳本\u003C/li>\u003Cli>在 Inspector 將 \u003Ccode>speed\u003C/code> 設為 \u003Ccode>400\u003C/code>（比公車快很多，製造緊迫感）\u003C/li>\u003Cli>同樣加入 \u003Ccode>obstacle\u003C/code> 群組\u003C/li>\u003C/ol>\u003Cblockquote>\u003Cp>辣椒醬形狀規則，直接用 \u003Ccode>CollisionShape2D\u003C/code> + \u003Ccode>RectangleShape2D\u003C/code> 就夠了，不需要像公車一樣用多邊形。\u003C/p>\u003C/blockquote>\u003Cp>上述兩個場景建立完成後，可以將它們拖入 \u003Ccode>main.tscn\u003C/code> 測試，應該可以看到公車緩慢往左、辣椒醬快速橫掃，並在離開畫面後自動消失。\u003C/p>\u003Ch2 id=\"he800a3fd26\">生成器\u003C/h2>\u003Ch3 id=\"h402da911af\">建立生成器場景\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/classes/class_timer.html\" rel=\"nofollow\">Timer 類別說明\u003C/a>\u003C/p>\u003C/blockquote>\u003Col>\u003Cli>\u003Cstrong>Scene → New Scene\u003C/strong>，根節點選 \u003Ccode>Node2D\u003C/code>，命名為 \u003Ccode>Spawner\u003C/code>，儲存為 \u003Ccode>spawner.tscn\u003C/code>\u003C/li>\u003Cli>新增 \u003Ccode>Timer\u003C/code> 子節點\u003Cul>\u003Cli>在 Inspector 設定：\u003Cstrong>Wait Time\u003C/strong> = \u003Ccode>5.0\u003C/code>，\u003Cstrong>Autostart\u003C/strong> = 開啟\u003C/li>\u003C/ul>\u003C/li>\u003C/ol>\u003Ch3 id=\"hf683478ad9\">撰寫 spawner.gd\u003C/h3>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/classes/class_packedscene.html\" rel=\"nofollow\">PackedScene 與動態實體化\u003C/a>\u003C/p>\u003C/blockquote>\u003Cp>生成器同時管理公車與辣椒醬兩種障礙物，每次計時器到時以機率決定生成哪種（或不生成）：\u003C/p>\u003Cpre>\u003Ccode>extends Node2D\n\n@export var obstacle_scene: PackedScene   # 在 Inspector 中拖入 obstacle.tscn\n@export var rocket_scene: PackedScene     # 在 Inspector 中拖入 rocket.tscn\n\nfunc _ready() -&gt; void:\n    $Timer.timeout.connect(_spawn_obstacle)\n\nfunc _spawn_obstacle() -&gt; void:\n    if randf() &lt; 0.5:\n        return           # 50% 機率跳過本次生成，保持節奏不規律\n    if randf() &lt; 0.4:\n        _spawn_sauce()   # 剩餘中有 40% 機率改生成辣椒醬\n        return\n    _spawn_bus()\n\n# 開發用：手動測試生成\nfunc _input(event: InputEvent) -&gt; void:\n    if event.is_action_pressed(&quot;debug_spawn_bus&quot;):\n        _spawn_bus()\n    if event.is_action_pressed(&quot;debug_spawn_sauce&quot;):\n        _spawn_sauce()\n\nfunc _spawn_bus() -&gt; void:\n    var obstacle = obstacle_scene.instantiate()\n    obstacle.position.y = _get_random_y()\n    add_child(obstacle)\n\nfunc _spawn_sauce() -&gt; void:\n    var rocket = rocket_scene.instantiate()\n    rocket.position.y = _get_random_y()\n    add_child(rocket)\n\nfunc _get_random_y() -&gt; int:\n    return randi_range(1, 3) * 200   # 三個固定高度：200 / 400 / 600\u003C/code>\u003C/pre>\u003Ch3 id=\"h36aec687d9\">連接場景\u003C/h3>\u003Col>\u003Cli>開啟 \u003Ccode>main.tscn\u003C/code>，將 \u003Ccode>spawner.tscn\u003C/code> 拖曳到中 \u003Ccode>main\u003C/code> 節點下\u003C/li>\u003Cli>將 \u003Ccode>Spawner\u003C/code> 移動到畫面右側邊緣外，障礙物生成時的 x 位置由此決定\u003C/li>\u003Cli>從 FileSystem 將 \u003Ccode>obstacle.tscn\u003C/code> 與 \u003Ccode>rocket.tscn\u003C/code> 拖入對應欄位\u003C/li>\u003C/ol>\u003Ch2 id=\"h1405a4fdcd\">碰撞與遊戲結束\u003C/h2>\u003Cblockquote>\u003Cp>\u003Ca href=\"https://docs.godotengine.org/en/stable/classes/class_scenetree.html#class-scenetree-method-change-scene-to-file\" rel=\"nofollow\">SceneTree.change_scene_to_file()\u003C/a>\u003C/p>\u003C/blockquote>\u003Ch3 id=\"h73c49b1a9b\">撰寫 main.gd\u003C/h3>\u003Cp>右鍵點擊 \u003Ccode>main\u003C/code> → \u003Cstrong>Attach Script\u003C/strong> → 儲存為 \u003Ccode>main.gd\u003C/code>。 偵測到 \u003Ccode>player_died\u003C/code> 後，直接切換到獨立的遊戲結束場景。\u003C/p>\u003Cpre>\u003Ccode>extends Node2D\n\nvar score := 0.0\n\nfunc _ready() -&gt; void:\n    $player.player_died.connect(func():\n        get_tree().change_scene_to_file(&quot;res://game_over.tscn&quot;)\n    )\n\nfunc _process(delta: float) -&gt; void:\n    score += delta\n    $UI/Label.text = &quot;Score: %d&quot; % score\u003C/code>\u003C/pre>\u003Cp>\u003Cstrong>Lambda 與 Callable\u003C/strong> \u003Ccode>func():\u003C/code> 是 GDScript 的**匿名函式（lambda）\u003Cstrong>語法，可以在不另外宣告具名函式的情況下，直接把一段程式碼當作值傳入。 在 Godot 中，函式可以被當作\u003C/strong>一等公民（first-class value）**傳遞，這種「可被呼叫的值」稱為 \u003Cstrong>Callable\u003C/strong>。\u003Ccode>connect()\u003C/code> 的參數就是一個 Callable——你可以傳入具名函式的參考，也可以傳入 lambda。 以下兩種寫法效果完全相同：\u003C/p>\u003Cpre>\u003Ccode># 寫法一：具名函式\nfunc _ready() -&gt; void:\n    $player.player_died.connect(_on_player_died)\nfunc _on_player_died() -&gt; void:\n    get_tree().change_scene_to_file(&quot;res://game_over.tscn&quot;)\n# 寫法二：lambda（程式碼只在一處使用時更簡潔）\nfunc _ready() -&gt; void:\n    $player.player_died.connect(func(): get_tree().change_scene_to_file(&quot;res://game_over.tscn&quot;))\u003C/code>\u003C/pre>\u003Cp>當邏輯只有一兩行且不需要重複使用時，lambda 可以減少具名函式的數量，讓程式碼更集中。\u003C/p>\u003Ch3 id=\"h0ed6cf496b\">建立遊戲結束場景（game_over.tscn）\u003C/h3>\u003Cp>遊戲結束畫面是一個獨立場景，負責顯示訊息並等待玩家重新開始：\u003C/p>\u003Col>\u003Cli>\u003Cstrong>Scene → New Scene\u003C/strong>，根節點選 \u003Ccode>Control\u003C/code>，命名為 \u003Ccode>GameOver\u003C/code>，儲存為 \u003Ccode>game_over.tscn\u003C/code>\u003C/li>\u003Cli>在 Inspector 將 \u003Ccode>GameOver\u003C/code> 的 \u003Cstrong>Anchors Preset\u003C/strong> 設為 \u003Cstrong>Full Rect\u003C/strong>（填滿畫面）\u003C/li>\u003Cli>在其下新增子節點 \u003Ccode>Label\u003C/code>：\u003Cul>\u003Cli>\u003Cstrong>Anchors Preset\u003C/strong>：Center（置中）\u003C/li>\u003Cli>\u003Cstrong>Horizontal Alignment\u003C/strong>：Center\u003C/li>\u003Cli>\u003Cstrong>Vertical Alignment\u003C/strong>：Center\u003C/li>\u003Cli>\u003Cstrong>Theme Overrides → Font Size\u003C/strong>：\u003Ccode>48\u003C/code>\u003C/li>\u003Cli>\u003Cstrong>Text\u003C/strong>：\u003Ccode>Game Over!\\nPress enter to retry.\u003C/code>\u003C/li>\u003C/ul>\u003C/li>\u003C/ol>\u003Ch3 id=\"hc442c0fc67\">撰寫 game_over.gd\u003C/h3>\u003Cp>右鍵點擊 \u003Ccode>GameOver\u003C/code> 根節點 → \u003Cstrong>Attach Script\u003C/strong> → 儲存為 \u003Ccode>game_over.gd\u003C/code>：\u003C/p>\u003Cpre>\u003Ccode>extends Control\n\nfunc _process(delta: float) -&gt; void:\n    if Input.is_key_pressed(KEY_ENTER):\n        get_tree().change_scene_to_file(&quot;res://main.tscn&quot;)\u003C/code>\u003C/pre>\u003Ch2 id=\"hd45ff3b5cf\">分數與細節調整\u003C/h2>\u003Ch3 id=\"hd7969f8d57\">調整遊戲手感\u003C/h3>\u003Cp>在 Inspector 調整這些 \u003Ccode>@export\u003C/code> 參數，不需要修改程式碼：\u003C/p>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>變數\u003C/p>\u003C/th>\u003Cth colspan=\"1\" rowspan=\"1\">\u003Cp>所在節點\u003C/p>\u003C/th>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>acceleration\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>Player\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>gravity\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>Player\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>speed\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>Obstacle（公車）\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>speed\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>Rocket（辣椒醬）\u003C/p>\u003C/td>\u003C/tr>\u003Ctr>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>\u003Ccode>Wait Time\u003C/code>\u003C/p>\u003C/td>\u003Ctd colspan=\"1\" rowspan=\"1\">\u003Cp>Spawner Timer\u003C/p>\u003C/td>\u003C/tr>\u003C/tbody>\u003C/table>\u003Ch3 id=\"hbccc52d406\">測試檢查清單\u003C/h3>\u003Cul>\u003Cli>按住空白鍵時玩家上升，放開後下墜\u003C/li>\u003Cli>玩家無法穿越地板或天花板\u003C/li>\u003Cli>公車從右往左緩慢移動，飛出畫面後消失\u003C/li>\u003Cli>辣椒醬從右往左快速橫掃，飛出畫面後消失\u003C/li>\u003Cli>每隔幾秒隨機生成公車或辣椒醬（偶爾不生成）\u003C/li>\u003Cli>碰到任何障礙物後切換到遊戲結束畫面\u003C/li>\u003Cli>按 Enter 後重新開始遊戲\u003C/li>\u003Cli>玩家存活期間分數持續增加，顯示在右上角\u003C/li>\u003Cli>按 B 鍵手動生成公車，按 S 鍵手動生成辣椒醬（開發測試用）\u003C/li>\u003C/ul>\u003Ch2 id=\"h37b7a9c2ea\">延伸挑戰\u003C/h2>\u003Cul>\u003Cli>用 \u003Ccode>AudioStreamPlayer\u003C/code> 加入音效（噴射聲、碰撞聲）\u003C/li>\u003Cli>新增金幣供玩家蒐集以獲得額外分數\u003C/li>\u003Cli>用 \u003Ccode>FileAccess\u003C/code> 儲存並顯示歷史最高分\u003C/li>\u003Cli>在遊戲結束畫面顯示本局得分\u003C/li>\u003C/ul>","workshop-jetpack-joyride",1,0,1777555909250]