5 min read

【20 Games 挑戰 #2】《Jetpack Joyride》

遊戲簡介

這是一款 2011 年推出的經典手機遊戲,其特色是單一按鍵操作:

  • 按住時角色上升,放開時角色會自然下墜
  • 地圖會自動向左捲動,障礙從右側生成
  • 玩家需閃避電網、飛彈,並盡可能遠行、收集金幣

連結為原始遊戲影片:

成果分享

這是「20 款遊戲挑戰」中的第 2 步,過程中學習以下:

  • 重複利用前一款的資料或程式碼
  • 儲存高分的機制
  • 加入簡單音效(包含跑步、噴射、受傷)
  • 加入粒子效果(例如噴射火焰)
0:00
/0:18

點選以下連結試玩:

噴射背包 by Peter_kncok_code
Play in your browser

場景架構總覽

以下是我用 Godot 建立的場景結構:


 ┖╴Main
    ┠╴background2D        (Node2D):背景群組,用來包含多層捲動背景
    ┃  ┠╴background1      (Sprite2D):第一張背景圖,用來做視差捲動
    ┃  ┖╴background2      (Sprite2D):第二張背景圖,與 background1 輪流重複
    ┠╴Player              (CharacterBody2D):玩家角色,負責控制移動與碰撞
    ┃  ┠╴CollisionShape2D (CollisionShape2D):碰撞框
    ┃  ┠╴AnimatedSprite2D (AnimatedSprite2D):角色動畫(跑步、跳躍等)
    ┃  ┠╴CPUParticles2D   (CPUParticles2D):噴射粒子效果(噴射火焰)
    ┃  ┠╴RunAudio         (AudioStreamPlayer2D):跑步聲音
    ┃  ┠╴JetAudio         (AudioStreamPlayer2D):噴射時的火焰音效
    ┃  ┖╴HurtAudio        (AudioStreamPlayer2D):受傷音效(撞到陷阱、飛彈)
    ┠╴Ground              (TileMapLayer):地板,由 tilemap 組成,會自動向左移動
    ┠╴Trap                (Area2D):靜態陷阱(如電網),可與玩家碰撞觸發扣血
    ┃  ┠╴AnimatedSprite2D (AnimatedSprite2D):陷阱動畫(如電光)
    ┃  ┖╴CollisionShape2D (CollisionShape2D):陷阱碰撞框
    ┠╴CoinSpawner         (Node):產生金幣群組的腳本控制器
    ┠╴TrapSpawner         (Node):產生陷阱的控制器
    ┃  ┖╴MissileWarningSpawner  (Node):追蹤玩家高度、先產生警告圖再發射飛彈
    ┠╴HUD                 (CanvasLayer):顯示 UI 分數、金幣、生命等資訊的畫面層
    ┃  ┠╴CoinLabel        (Label):顯示目前取得的金幣數量
    ┃  ┠╴DistanceLabel    (Label):顯示目前奔跑距離
    ┃  ┠╴HeartFull1       (Sprite2D):顯示目前剩餘血量的圖案(愛心)
    ┃  ┠╴HeartFull2      
    ┃  ┠╴HeartFull3
    ┃  ┠╴HeartEmpty1      (Sprite2D):顯示已失去血量(灰色愛心)
    ┃  ┠╴HeartEmpty2
    ┃  ┠╴HeartEmpty3  
    ┃  ┖╴JumpBtn          (TouchScreenButton):手機版使用者點擊畫面跳躍(模擬空白鍵)
    ┖╴AudioStreamPlayer2D (AudioStreamPlayer2D):背景音樂播放器

小技巧補充:任意節點加上以下程式碼,可以打印出場景樹!

func _ready():
	print_tree_pretty()

踩坑記錄與解法

TileMapLayer 產生地板

一開始用 StaticBody2D + add_child() 不斷新增地板,結果節點太多、效能下降。
改用 TileMapLayer 並動態平移與補格,達成無限捲動效果,效能大幅提升。


音效播放被 queue_free() 清除

角色受傷後要播放音效再消失,若直接寫:

func _on_body_entered(body):
    if body.is_in_group("Player"):
        audio_player.play()
        queue_free()

結果聲音根本播不出來!因為節點馬上被刪掉了。

解法是先隱藏並關掉碰撞,等音效播完再刪除:

visible = false
$CollisionShape2D.disabled = true
audio_player.play()
audio_player.connect("finished", Callable(self, "queue_free"))

TouchScreenButton 必須設定 InputMap

我原本以為 TouchScreenButton 放上去就能按,但其實需要設定 actionui_accept,才能與空白鍵邏輯對應。

小結與下一步

這款《Jetpack Joyride》是我在「20 款遊戲挑戰」中的第二款作品,練習了粒子效果簡單音效UI 設計,也在效能優化上學到不少技巧。

雖然還有可以再雕琢的地方,但在限定時間內完成預定功能,已經很滿足。這次的經驗會帶進下一款遊戲中。

這篇文章是我進行「20 款遊戲挑戰」的其中一站,目前已完成第 2 款,接下來將挑戰更多經典遊戲機制、動畫特效與關卡設計!

如果你對以下內容有興趣:

  • Godot 遊戲開發實戰經驗
  • 獨立遊戲創作的完整紀錄

也歡迎到 Threads 與我互動,一起交流開發心得。

彼得叩叩 (@peter_knock_code) • Threads, Say more
658 Followers • 0 Threads • 獨立遊戲開發 x 機器視覺工程師. See the latest conversations with @peter_knock_code.

— Peter