
前回の記事ではGodotの基礎学習についてまとめました。今回はその続編として、Udemyの「Godot4: Build a 2D Action-Adventure Game」を完走し、実践的な2Dアクションアドベンチャーゲームの開発に挑戦しました。

基本的なプレイヤー操作から、NPCとの対話、パズル、戦闘システムまで体系的に網羅されたコースです。この記事では、コースを通して学んだGodotの重要機能や概念を整理して共有します。
コース概要

「Godot4: Build a 2D Action-Adventure Game」は、2Dアクションアドベンチャーゲームをゼロから作り上げる英語コースです。開発を進めるなかで、こんな疑問が自然に解消されていきます。
- エリア間の移動はどう実装する?
- オブジェクトを押す仕組みは?
- 開封済み宝箱の状態をどう保持する?
- NPCとの会話をどう管理する?
- 複数スイッチで開く扉の実装方法は?
- 被ダメ時のホワイトフラッシュはどうやる?
具体的なカリキュラムとしては、プレイヤーの8方向移動、Terrainsによるオートタイル環境構築、Y-Sortによる重なり順制御、RigidBody2Dの物理パズル、ダイアログシステム、Autoloadによるデータ永続化、敵AI+ノックバック付き戦闘システムなどを一通りカバーしています。
前回紹介した「Godot Engineで気軽に2Dゲームを作ろう」より難易度は上がりますが、「ゲーム開発で本当に必要な知識」が詰まった良コースで す。Udemyは頻繁にセールがあるので、お気に入り登録して狙うのがおすすめです。
Godotの重要機能・概念ノート
ここからは、コースで特に重要だと感じた機能を深堀りします。
Process Mode:ポーズ機能の制御

「NPCと会話中は背景の敵やプレイヤーは止めたいが、ダイアログの操作は続けたい」――ゲーム開発でよくあるこの要件を、Godotでは各ノードのProcess Modeで解決します。
- Pausable(デフォルト):
get_tree().paused = trueで停止する。プレイヤーや敵など、ゲーム世界のオブジェクト向け。 - Always: ポーズを無視して常に動作。会話中のNPCやUI、BGMに使う。
- When Paused: ポーズ中だけ動作。ポーズメニュー専用UIに最適。
UnityではTime.timeScale = 0でゲーム全体を止めつつ、個別のスクリプトでTime.unscaledDeltaTimeを使い分ける必要がありましたが、Godotではノード単位でProcess Modeを設定するだけ。非常にスマートです。
実装例:ダイアログ表示中にゲームをポーズする

※会話中はシーンが停止し、会話終了と同時に解除される
# NPC.gd
# このNPCノードのProcess Modeをインスペクターで "Always" に設定しておく
func _process(delta):
if Input.is_action_just_pressed("interact") and can_talk:
if is_dialog_active():
close_dialog()
get_tree().paused = false
else:
open_dialog()
get_tree().paused = true
NPC自身は止めずにゲーム全体をポーズすることで、ダイアログの開閉を安全に処理できます。
CharacterBody2DのMotion Mode

CharacterBody2DにはMotion Modeという設定があり、ゲームのジャンルに応じた物理挙動を切り替えられます。プロジェクト開始時に必ず確認すべき項目です。
- Grounded(デフォルト): 重力が適用され、
is_on_floor()が機能する。プラットフォーマーや横スクロールアクション向け。 - Floating: 重力なし、床の概念もなし。トップダウンのアクションゲームやシューティング向け。
# Player.gd (Motion Modeを "Floating" に設定)
extends CharacterBody2D
@export var speed: float = 200.0
func _physics_process(delta):
var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
設定を間違えると意図しない重力が発生したり、床判定がおかしくなるので注意。
InputMap:キーバインド管理

UnityではInput.GetKey(KeyCode.A)のようにキーを直接指定しがちですが、GodotのInput Map(プロジェクト設定 → Input Map)では「アクション名」を定義してキーを紐づけます。コードでは"move_left"のような抽象名で入力を 扱うため、可読性が高く、キーコンフィグの実装も容易です。
func _process(delta):
# 単発の入力(押した瞬間)
if Input.is_action_just_pressed("interact"):
open_chest()
# 継続的な入力(押し続けている間)
if Input.is_action_pressed("dash"):
speed = DASH_SPEED
else:
speed = NORMAL_SPEED
# 4方向を正規化されたVector2で取得(非常に便利)
var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
Input.get_vector()は4アクションから正規化されたVector2を返してくれるため、トップダウンの移動処理を1行で書けます。
入力処理の使い分け
InputMapでアクションを定義したら、入力の「状態」に応じて適切なメソッドを使い分けます。
| 用途 | メソッド | 具体例 |
|---|---|---|
| 押した瞬間の1回限り | is_action_just_pressed() | ジャンプ、攻撃、メニュー開閉 |
| 押している間ずっと | is_action_pressed() | 移動、ダッシュ、チャージ |
| 離した瞬間 | is_action_just_released() | チャージ攻撃の発動 |
| アナログ入力(0.0〜1.0) | get_action_strength() | ゲームパッドのトリガー |
func _process(delta):
# 1回限りのアクション
if Input.is_action_just_pressed("jump"):
if is_on_floor():
velocity.y = JUMP_VELOCITY
if Input.is_action_just_pressed("attack"):
perform_attack()
# 継続的なアクション
if Input.is_action_pressed("dash"):
current_speed = dash_speed
else:
current_speed = normal_speed
# チャージ系:押している間ためて、離したら発動
if Input.is_action_pressed("charge"):
charge_power += charge_rate * delta
charge_power = min(charge_power, max_charge)
if Input.is_action_just_released("charge"):
fire_charged_shot(charge_power)
charge_power = 0.0
よくある間違いとして、is_action_pressed()で射撃を処理すると毎フレーム弾が発射されてしまいます。単発アクションには必ずis_action_just_pressed()を使いましょう。
move_and_slideとmove_toward

Godotの移動処理で中心となる2つの関数です。
move_and_slide():CharacterBody2Dの主力。現在のvelocityに基づいて移動し、壁や床との衝突を自動処理してくれる。move_toward(target, delta): 現在の値を目標値に向かって少しずつ変化させる。滑らかな加速・減速に使う。
この2つの使い分けが重要になるのが、ノックバック実装の場面です。コースで実際にぶつかった問題を紹介します。
velocityを直接代入する方式だと、ノックバック中にプレイヤーが移動入力をした瞬間、ノックバック効果が一瞬で消えてしまいます。move_towardで段階的に速度を変化させることで、ノックバックが自然に減衰しながら通常移動に戻る挙動を実現できます。
# 直接代入だとノックバックが即消える
func move_player():
var move_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = move_vector * move_speed # ノックバックの速度が即上書きされる
# move_towardで段階的に変化させる
@export var acceleration: float = 500.0
func move_player():
var move_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
var target_velocity = move_vector * move_speed
velocity = velocity.move_toward(target_velocity, acceleration * delta)
# ノックバック処理
func apply_knockback(direction: Vector2, strength: float):
velocity += direction * strength
move_towardの第2引数は「1フレームで目標にどれだけ近づくか」を指定します。acceleration * deltaの形で使うことで、フレームレートに依存しない滑らかな遷移が得られます。
グループ:柔軟なオブジェクト判定

「攻撃が当たったオブジェクトが敵かどうか判定したい」――この要件に対して、Godotではグループを使います。
UnityのTagシステムに近い概念ですが、重要な違いがあります。Unityでは1つのGameObjectに1つのTagしか設定できませんでした。Godotのグループは複数設定可能なので、「このオブジェクトは "enemies" であり "damageable" でもある」といった柔軟な分類ができます。
設定方法はシンプルで、ノードのインスペクター → Nodeタブ → Groupsからグループ名を追加するだけ。コードではis_in_group()で判定します。
# Playerの攻撃判定用Area2Dに接続
func _on_sword_area_body_entered(body: Node2D):
if body.is_in_group("enemies"):
body.take_damage(attack_power)
elif body.is_in_group("pushable"):
pass # 押せるオブジェクトの処理
Collision Layers/Masks

衝突判定をきちんと整理しないと、「プレイヤーの剣が味方にも当たる」「敵同士が引っかかる」といった問題が起きます。GodotのCollision Layers/Masksは、これを防ぐ仕組みです。
- Layer: そのオブジェクトが「どの層に存在するか」
- Mask: そのオブジェクトが「どの層と衝突判定を行うか」
たとえば、Player(Layer 1)、Enemies(Layer 2)、Weapons(Layer 3)と分けた場合:
| オブジェクト | Layer | Mask | 理由 |
|---|---|---|---|
| プレイヤー | 1 | 2 | 敵に触れてダメージを受ける |
| 敵 | 2 | 1 | プレイヤーのみと接触(敵同士はすり抜け) |
| プレイヤーの武器 | 3 | 2 | 敵のみを攻撃(プレイヤーとは衝突しない) |
# 武器のMaskで敵のみ検出するため、この関数には敵だけが入ってくる
func _on_weapon_area_body_entered(body):
if body.is_in_group("enemies"):
body.take_damage(attack_power)

「プロジェクト設定」→「Layer Names」で各レイヤーに名前を付けておくと、インスペクターでの設定が格段に分かりやすくなります。
Terrains:オートタイル作成

GodotのTerrains機能は、いわゆるオートタイルを驚くほど簡単に作れるシステムです。UnityでRule Tileを使ったことがある人なら、その手軽さに感動するはずです。

TileSetリソース内の「Terrains」タブで、タイルの各辺がどの地形タイプに接するかを視覚的にペイントする だけ。TileMapエディタでブラシツールを使えば、境界線を自動判定して適切なタイルを配置してくれます。
便利機能として、ランダムブラシ(ダイスアイコン)で複数タイルからランダム選択、確率制御で特定タイルの出現頻度調整、「F」キーで全タイルに衝突判定を一括適用などがあります。
Marker2D:管理ノードの定番
Unityでは空のGameObjectにスクリプトをアタッチして「マネージャー」を作るのが定番でしたが、GodotではMarker2Dがその役割を担います。
Marker2Dは位置情報(Transform)だけを持つ最も軽量な2Dノードです。レンダリングも物理演算もないため、GameManagerやPuzzleManagerのようなシーン管理スクリプトの置き場所として最適です。Unityの空GameObjectと違い、エディタ上で十字マーカーが表示されるので視認性も良好です。
Editable ChildrenとScene継承
ベースシーンからバリエーションを作成する方法は2つあります。用途に応じた使い分けが重要です。
- Editable Children: シーンに配置したインスタンスを右クリック→「Editable Children」で中身を直接編集。変更はその配置先にのみ反映される。見た目やセリフだけ 違うモブNPCの量産に便利。
- Scene Inheritance: ベースシーン(BaseNPC.tscn)を継承して新しいシーン(Shopkeeper.tscn)を作成。親の機能を引き継ぎつつ独自機能を追加できる。「話す」+「売買する」機能を持つ商人NPCなど、機能的に異なる派生種に最適。

| 特徴 | Editable Children | Scene 継承 |
|---|---|---|
| 向いているNPC | 村人A、村人Bなど(見た目・セリフ違い) | 商人、鍛冶屋など(独自機能持ち) |
| 再利用性 | 低い(その場限り) | 高い(継承シーンを各所に配置可能) |
| 管理 | シンプル(ベースシーンのみ) | 体系的(機能ごとにファイルが分離) |
Autoload:シーンをまたぐデータ管理
「宝箱を開けてエリア移動して戻ってきたら、また閉じている」――シーン切替でデータが失われる問題は、GodotではAutoloadで解決します。UnityのDontDestroyOnLoad+シングルトンに相当する機能です。
プロジェクト設定でスクリプトをAutoloadに登録すると、ゲーム起動時に自動読み込みされ、どのシーンからでもグローバルにアクセスできま す。

# GameManager.gd (AutoLoadに登録)
extends Node
var opened_chests: Array[String] = []
var player_hp: int = 3
var player_spawn_position: Vector2
# TreasureChest.gd
extends StaticBody2D
@export var chest_id: String # "forest_chest_01"などユニークIDを設定
func _ready():
if GameManager.opened_chests.has(chest_id):
play_open_animation(false)
func open_chest():
GameManager.opened_chests.append(chest_id)
play_open_animation(true)
プレイヤーのHP、スコア、インベントリ、クエスト進捗など、シーンをまたいで維持したいデータはすべてAutoloadで管理できます。
modulateによるホワイトフラッシュ

ダメージを受けた瞬間に一瞬白く光る「ホワイトフラッシュ」は、プレイヤーへのフィードバックとして非常に効果的です。Godotではmodulateプロパティで簡単に実装できます。
modulateはノードとその子孫の色に乗算されるカラー値で、デフォルトは白(1, 1, 1)。値を大きくすると明るく、小さくすると暗くなります。CharacterBody2Dのmodulateを変更すれば、子のAnimatedSprite2Dも自動で色が変わるため、個別操作は不要です。
# Player.gd
func take_damage(amount):
# ...ダメージ計算...
flash_effect()
func flash_effect():
modulate = Color(2, 2, 2) # 白く光らせる
await get_tree().create_timer(0.1).timeout # 0.1秒待機
modulate = Color(1, 1, 1) # 元に戻す
await get_tree().create_timer(0.1).timeoutは、タイマーノードを追加せずに一時的な待機処理を1行で書ける便利な記法です。
AnimatedSprite2D vs AnimationPlayer
Godotには主要な2Dアニメーションシステムが2つあり、用途で使い分けます。
AnimatedSprite2D
スプライトフレームアニメーション専用。歩行、攻撃、アイドルなど、スプライトシートの切り替えだけで完結するアニメーションに最適です。
if velocity.x > 0:
$AnimatedSprite2D.play("move_right")
elif velocity.x < 0:
$AnimatedSprite2D.play("move_left")
else:
$AnimatedSprite2D.stop()
AnimationPlayer

位置、回転、スケール、任意のプロパティを同時制御できる汎用アニメーションシステム。UnityのAnimatorに近い存在で、剣を振る動作やUI演出、カメラワークなど複合的な制御に使います。
func attack():
var player_animation: String = $AnimatedSprite2D.animation
if player_animation == "move_right":
$AnimatedSprite2D.play("attack_right")
$AnimationPlayer.play("attack_right") # 剣の位置・角度を制御
使い分けの指針
| アニメーション内容 | 推奨システム |
|---|---|
| スプライトフレーム切り替えのみ | AnimatedSprite2D |
| 位置・回転・スケール変更 | AnimationPlayer |
| 複数オブジェクトの同期 | AnimationPlayer |
| 複雑な状態遷移 | AnimationTree |
実際の開発では、キャラクターの基本動作にAnimatedSprite2D、武器やエフェクトの動作にAnimationPlayerを組み合わせて使うのが一般的です。
まとめ
今回のコースを通して、Godotの設計思想の一貫性と2D開発における強さを改めて実感しました。Process Mode、Motion Mode、Collision Layersといった概念が統一的に設計されており、「やりたいこと」に対する解決策がストレートに用意されています。
特に、Terrains、Y-Sort、move_and_slide()など、2Dゲーム開発で「 こういう機能が欲しかった」という部分が標準搭載されている点は大きな魅力です。Unity経験者にとって、Godotは学習コストの低さと開発の快適さを両立した有力な選択肢だと感じました。