この記事について

上から見下ろす2Dゲームでは、プレイヤーを上下左右へ動かし、壁に当たったら止める処理が基本になります。GodotではCharacterBody2Dに移動速度を設定し、move_and_slide()で衝突を調べながら動かします。

この記事では、矢印キーでキャラクターを動かし、オレンジ色の壁にぶつける場面を作ります。Godot 4.6.3のGDScriptを使い、エディターの項目名は英語表記で説明します。重力やジャンプを使わない見下ろし型の移動です。

この記事でわかること

  • Input MapとInput.get_vector()で上下左右の入力を受け取る方法
  • CharacterBody2DとCollisionShape2Dで壁にぶつかるキャラクターを作る方法
  • velocityとmove_and_slide()の役割、およびdeltaを掛けない理由

基本手順

手順1:シーンと4方向の入力を用意する

空のプロジェクトでScene → New Sceneを開き、2D Sceneを選びます。ルートのNode2DをMainに名前変更し、res://main.tscnとして保存します。Project → Project Settings → Display → WindowでViewport Widthを800、Viewport Heightを520にします。

続けてProject SettingsのInput Mapタブを開きます。Add New Actionへmove_leftと入力してAddを押し、同じ方法でmove_right、move_up、move_downも追加します。それぞれのアクション右側の+からキー入力を登録し、次の組み合わせにします。

  • move_left:左矢印キー
  • move_right:右矢印キー
  • move_up:上矢印キー
  • move_down:下矢印キー

キー入力の追加画面で対応する矢印キーを押し、表示されたキーを確認してOKを押します。各アクションのDeadzoneは0.2にします。アクション名は後からコードで使うので、小文字とアンダースコアを含めて一致させます。

Input Mapにmove_left、move_right、move_up、move_downを登録した状態
手順1:4つのアクションへ対応する矢印キーを登録します。

手順2:プレイヤーの画像と当たり判定を作る

Mainの子にCharacterBody2Dを追加し、Playerに名前変更します。PlayerのInspectorでTransform → PositionをX=160、Y=288、Motion ModeをFloatingにします。Floatingは床と天井を区別しない見下ろし型の移動に向いています。

Playerの子にSprite2DとCollisionShape2Dを追加します。移動用のキャラクター画像(ZIPをダウンロード)を保存して展開し、中のidle_0.svgをプロジェクトフォルダー内に作ったassetsフォルダーへ入れます。GodotのFileSystemに表示されたres://assets/idle_0.svgを、Sprite2DのTexture欄へドラッグします。

CollisionShape2Dを選び、Shapeの空欄からNew RectangleShape2Dを作成します。作成したリソースをクリックして展開し、SizeをX=32、Y=32にします。Sprite2DとCollisionShape2DのPositionはどちらもX=0、Y=0、ScaleはX=1、Y=1のままにします。

Main (Node2D)
└ Player (CharacterBody2D)
  ├ Sprite2D
  └ CollisionShape2D

Sprite2Dは見た目、CollisionShape2Dは当たり判定です。今回の画像は40×40ピクセルですが、当たり判定は少し小さい32×32にしています。CollisionShape2DのDisabledとOne Way Collisionはオフにします。

Playerの下にSprite2DとCollisionShape2Dがあり、32×32のRectangleShape2Dを設定した状態
手順2:Playerの見た目と当たり判定を別々に用意します。

手順3:ぶつかる壁と背景を置く

Mainの子にStaticBody2Dを追加してWallと名付け、PositionをX=432、Y=288にします。Wallの子へCollisionShape2Dを追加し、新しいRectangleShape2Dを作成します。SizeはX=48、Y=192です。PlayerのShapeを使い回さず、壁用のリソースを新しく作ります。

Wallの子にPolygon2Dを追加し、Visualに名前変更します。Visualを選び、InspectorのPolygon2D → Data → PolygonにあるPackedVec…と表示されたボタンをクリックして、頂点の配列を展開します。Sizeを4にして、要素0〜3のXとYへ次の値を入力します。VisualのPositionは(0, 0)のままです。

  • 要素0:X=−24、Y=−96
  • 要素1:X=24、Y=−96
  • 要素2:X=24、Y=96
  • 要素3:X=−24、Y=96

Colorをオレンジ色にすると壁の位置がわかりやすくなります。完成サンプルの色は、各成分を0〜1で表すとR=0.96、G=0.56、B=0.38、A=1です。数値欄が狭くて読みにくい場合は、Inspectorの左境界を左へドラッグして幅を広げてください。

Mainの子にもう1つPolygon2Dを作ってGroundと名付け、SceneドックでWallとPlayerより上へ移動します。Positionは(0, 0)のままにします。Visualと同じようにInspectorのPolygon2D → Data → Polygonを展開し、Sizeを4にして、次の値を入力します。

  • 要素0:X=40、Y=144
  • 要素1:X=760、Y=144
  • 要素2:X=760、Y=464
  • 要素3:X=40、Y=464

GroundのColorは暗い青にします。完成サンプルの色は、各成分を0〜1で表すとR=0.10、G=0.17、B=0.23、A=1です。Groundは見た目だけの背景なので、CollisionShape2Dは付けません。

Main (Node2D)
├ Ground (Polygon2D)
├ Wall (StaticBody2D)
│ ├ Visual (Polygon2D)
│ └ CollisionShape2D
└ Player (CharacterBody2D)
  ├ Sprite2D
  └ CollisionShape2D

PlayerとWallのCollision → LayerとMaskは、どちらも1だけをオンにします。Layerは自分が所属する層、Maskは調べる相手の層です。この例では両方を同じ設定にそろえます。壁側のCollisionShape2DもDisabledとOne Way Collisionをオフにしてください。

StaticBody2DのWallに壁の表示用Polygon2Dと48×192のCollisionShape2Dを置いた状態
手順3:Wallの見た目と当たり判定を同じ位置に合わせます。

手順4:velocityを設定してmove_and_slide()を呼ぶ

Playerを選んでAttach Scriptを押し、res://player.gdを作ります。Templateがオンなら外してください。スクリプト全体を次の内容にします。

extends CharacterBody2D

@export var speed: float = 200.0

func _physics_process(_delta: float) -> void:
	var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
	velocity = direction * speed
	move_and_slide()

Input.get_vector()へ渡す順番は「左、右、上、下」です。入力がなければdirectionは(0, 0)になるので、キーを離すとvelocityも0になります。左右と上下を同時に押してもベクトルの長さが1を超えないため、斜め移動だけ速度が速くなることを防げます。

speedは1秒あたりの移動量で、この例では200ピクセルです。velocityへ「方向×速度」を設定してから、move_and_slide()で実際に移動します。move_and_slide()は衝突すると速度を変更することがあるため、次の物理フレームでは入力からvelocityを設定し直します。

ここではvelocityにdeltaを掛けません。move_and_slide()が物理フレームの経過時間を使って移動量を計算するためです。引数の_deltaは使わないことを表す名前です。Godot 4ではmove_and_slide(velocity)という引数付きの書き方や、戻り値をvelocityへ代入する書き方もしません。詳しくは公式のvelocityの説明とInput.get_vector()の説明を参照できます。

player.gdでInput.get_vectorからvelocityを設定しmove_and_slideを呼ぶコード
手順4:移動処理は_physics_process()の中にまとめます。

手順5:上下左右の移動と壁沿いの移動を確かめる

シーンとスクリプトを保存し、Run Current Sceneからmain.tscnを実行します。Windows・Linuxの標準ショートカットはF6です。ゲーム画面をクリックしてから右矢印キーを押し、キャラクターが壁の左側で止まることを確認します。

続けて右矢印と上矢印を同時に押します。壁を突き抜けず、壁に沿って上へ移動できれば、衝突と滑りの処理が動いています。壁より上まで回り込んでから右へ進み、キーを離すと止まることも確かめます。

Debug → Visible Collision Shapesをオンにして実行すると、画像と当たり判定の位置を見比べられます。壁の左端はX=408、Playerの当たり判定の半幅は16なので、壁の左面に押し付けたときのPlayerの中心はX=392付近になります。衝突用の小さな余白があるため、小数点以下まで392と一致するとは限りません。

キャラクターがオレンジ色の壁の左側で止まり、壁沿いに移動できる実行結果
手順5:右へ進んで止まり、右上入力で壁沿いに移動することを確かめます。

完成サンプルの画面上部には、Position、Actual speed、Wallという観察用の表示があります。順に位置、実際の速度、壁との衝突状態を表します。これらはHUDというCanvasLayerの表示で、プレイヤーを動かすために必須のノードではありません。また、Groundの外周には壁を作っていないため、背景の外へは移動できます。

うまくいかないときの確認箇所

キーを押しても動かない

最初にDebuggerのエラーを確認します。次に、ゲーム画面に入力フォーカスがあるか、4つのInput Map名とコードの文字列が一致するか、player.gdをPlayerへ付けたかを確認します。PlayerのInspectorに表示されるSpeedが0になっていないかも見てください。

壁をすり抜ける

WallがStaticBody2Dか、双方のCollisionShape2DにShapeが設定されているか、Disabledがオフかを調べます。CollisionShape2DはそれぞれPlayerとWallの直接の子にします。LayerとMaskを1に戻して試し、移動コードでpositionを直接書き換えていないかも確認します。Polygon2Dだけでは衝突しません。

移動が極端に遅い、斜めだけ速い

遅い場合はvelocityにdeltaを掛けていないか確認します。斜めだけ速い場合は、入力を独自に足し合わせるコードが残っていないか調べ、掲載したInput.get_vector()へそろえてください。

壁から離れて止まる、見た目が壁へ入り込む

Visible Collision Shapesで衝突形状を表示し、画像との大きさの違いを確かめます。この例の画像は40×40、衝突形状は32×32なので、画像の端が壁と少し重なることはあります。意図しないずれがあれば、子ノードのPositionとScale、ShapeのSizeを見直します。

まとめ

見下ろし型の移動は、Input.get_vector()で方向を読み、velocityへ速度を設定し、move_and_slide()で衝突を処理する流れです。見た目の画像と当たり判定は別に用意します。

最初は壁を1枚だけ置き、「自由に移動する」「壁で止まる」「壁に沿って滑る」の3つを確かめると、入力と衝突の問題を切り分けやすくなります。