歩けるゲームの作成
2026/7/4
ゲームの制作はGodotEngine4.6.2を使って行う。
手順やコードはほとんどChatGPTに丸投げして進めていく。
「どうやるの?」と聞きまくってその通りにするだけと言うとちょっと人聞きが悪いが「どんな状況なのか」と「何がしたいのか」を考えて正確に伝えさえすれば相当な手間をショートカットできる、というのがツールとしての今のAIの本質だと思う。
・モデリングデータの出力
まずは作った3Dモデルをゲームで使える形に出力する。
3Dデータのファイル形式についてはよくわからないが.glbという形式でエクスポートすれば問題ないようだ。

Blenderのエクスポート設定で選択したオブジェクトのみを出力できる。
この際にミラーやサブディビジョンサーフェスなどのまだ適用していないモディファイアーを適用状態で出力できる。
これができるのだからモディファイアーは編集中にはできるだけ適用すべきではない。

Godotで新規3Dプロジェクトを作成したら、プロジェクトフォルダに.glbファイルを入れるとリアルタイムで自動インポートされる。
ファイルシステムを見るとテクスチャ画像などが自動で展開されている。
この素材を使ってゲームを作った状態でフォルダ内のファイルを変更するとゲーム内の素材も自動で更新される。
なんならデバッグ中でゲームが動作している最中に変えても更新される、すごい。
・テストフィールドの作成
作成したばかりのプロジェクトにはNode3Dというノードがあるだけ。
これは位置情報を持つだけの空フォルダのようなものという認識でいいらしい。

ここに作成した3Dモデルをドラッグ&ドロップするだけでゲーム内にモデルが設置できる。
キャラクターを作るときは、衝突判定などの機能をはじめから内包したCharacterBody3Dというノードを使うといいようだ。

CharacterBody3Dを新規作成し、その中にモデルをドラッグ&ドロップ、CollisionShape3Dという当たり判定を司るノードを入れてあげればこれだけでキャラクターとして成立するとのこと。
まだ当たり判定の形や大きさを指定していないのでエラーが出ている。

ノードの各種設定は右側のインスペクターというところで変えられる。
CollisionShape3Dの形状はShapeというところに設定でき、四角や円柱は角が引っかかるのでキャラクターはカプセル型を使うのが一般的だそう。
位置や大きさはプレビュー画面の矢印でもインスペクターのTransformでも変えられるが、判定の大きさをここのスケールで変えるべきではない。
スケールはノード全体の大きさを変更するため、このノードに後から何か子ノードを追加した際に子ノードが一緒に拡大されてしまったりとトラブルの原因になる。

少しわかりにくいが形状を設定した後、CapsuleSape3Dをもう一度クリックするとサイズの設定画面が出る。
床判定のために足の裏だけピッタリと合わせ、他はやや小さめにしておくのがいいようだ。

床はキャラクターではないのでStaticBody3Dを使うのがいいらしい、何が違うのかは分からない。

カメラがないとゲーム画面に何も映らないのでCamera3Dノードを置いて(0m,1m,3m)あたりに設置する。
プレビュー画面は明るいが実際には光源を置かないと真っ暗なのでDirectionalLight3Dも追加する。
DirectionalLight3Dは太陽光のような光源で、マップ全体に並行に均一な光が降り注ぐようになる。デフォルトだと真横向きなので斜めにしておいた方がいいかも。
キャラクターと床をいい感じの位置にしたら実際に動かしてみる。

まだ何の操作もできないが、無事ゲームを実行できた。
・キャラクター操作のプログラム
キャラクターを操作できるようにするためのプログラムを作成する。

キャラクターのノードを右クリックして「スクリプトをアタッチ」を選ぶと、プログラムコードを取り付けることができる。
そのまま「作成」を押せばノードと同じ名前のプログラムファイルが自動で新規作成され、コーディングができる画面に移動する。

プレビュー画面やコーディング画面の切り替えは上のタブでできる。
方向キーで移動できるプログラムをChatGPTに生成してもらって丸ごと入れ替える。
extends CharacterBody3D
const SPEED = 5.0
func _physics_process(delta):
var input_dir = Vector2.ZERO
input_dir.x = Input.get_axis("ui_left", "ui_right")
input_dir.y = Input.get_axis("ui_up", "ui_down")
velocity.x = input_dir.x * SPEED
velocity.z = input_dir.y * SPEED
move_and_slide()
方向キーを押すとキャラクターが動く!
シュールな絵面なのでこれを見て「すげぇ!」となる人と「なにこれ…」となる人がいるだろうが私は前者なので大喜びだ。
Godotはプロジェクト設定からインプットマップというのを設定できる。
このコードは初期設定からある「ui_left」「ui_right」「ui_up」「ui_down」の4つが押されたら動くようになっており、これらにどのボタンが割り振られているかは設定で変えられる。
デフォルトでは矢印キーとゲームパッドが設定されているが、これを追加したり変更したりすればWASD操作にもできる。
プログラムのコードはこれくらいの短さなら読めば何となく分かるし覚えていられるが、量が増えたら数日もすれば自力で読めなくなる可能性が高い。
本当に全てをAIに書かせて毎回全文を入れ替えるならそれでもいいが、コードが複雑で長くなるとAIのミスも増えるしうっかり関係ないところを変更されて気づかないままなんてことになったら目もあてられない。
自分でも部分的に書き換えたり、必要な部分だけをAIに投げられるようにコメントを書いておいた方がいい。
# CharacterBody3Dを継承する
# このスクリプトを付けたノードは、3D空間で物理演算を使って移動できるようになる
extends CharacterBody3D
# 移動速度を定数として定義する
# constは変更されない値を表す
const SPEED = 5.0
# 毎回の物理演算フレーム(通常は1秒間に60回)呼ばれる関数
# deltaは前回のフレームから経過した時間(今回は使用していない)
func _physics_process(delta):
# 入力方向を保存する2次元ベクトルを作成
# (0,0)で初期化しているので最初は「入力なし」
var input_dir = Vector2.ZERO
# 左右キーの入力を取得する
#
# ui_leftが押されると -1
# ui_rightが押されると +1
# 両方またはどちらも押されていない場合は 0
input_dir.x = Input.get_axis("ui_left", "ui_right")
# 前後キーの入力を取得する
#
# ui_upが押されると -1
# ui_downが押されると +1
input_dir.y = Input.get_axis("ui_up", "ui_down")
# CharacterBody3Dが持つ速度(velocity)のX成分を設定する
# 左右入力 × SPEED の速度で移動する
velocity.x = input_dir.x * SPEED
# CharacterBody3Dでは前後方向はZ軸で表される
# 入力 × SPEED の速度を設定する
velocity.z = input_dir.y * SPEED
# velocityの値を使ってキャラクターを移動させる
# 壁との衝突や床との接触なども考慮して移動してくれる
move_and_slide()
ChatGPTにコードを丸ごと投げて「コメントで説明を書いて」と言えばこんな具合になる。
初学者だと言えば細かく書いてくれるし、全部書かせない場合も何なのか分からないところを個別で説明してもらって書いておく。
・キャラクターファイルの中身
いつまでも棒立ちでは味気ないのでアニメーションを再生できるようにしようとしたが、この辺りはChatGPTが言っていることと実際の状況が異なり少し難航した、最近のバージョンで色々変わったのかもしれない。

結論から言うと.glbファイルをドラッグ&ドロップでシーンに追加するのは勝手が悪かった。
ミカ.glbというファイルは中にモーションやボーンのデータなどを内包しているはずなのだが、シーン画面ではただの「ミカ(Node3D)」として扱われており中身を開けない。
実際には分からないが、これは.glbファイルと直接繋がっている名前だけの存在のような形でゲームに配置されていると思われる。
ファイスシステムの方にある.glbファイルをダブクルリックすれば中身は見れるのだがここにノードを追加したりはできない。
シーン一覧でモデルデータを変更したり閲覧できるようにするために、今作っているゲームとは別のシーンを作ってそれをゲームに配置し「.glbファイル」→「キャラだけのシーン」→「ゲーム本体」という風に繋げることで、キャラクターの中身を触りやすくしておいた方がいいと感じた。

ファイルシステムの.glbファイルを右クリックし「新しい継承シーン」を選ぶ。

するとモデルだけの新しいシーンが作成され、ここでなら.glbファイルの中身の構造が見えている。
このシーンを保存すると.tscnというファイルになる。

これをゲームのシーンにドラッグ&ドロップすると.glbをドラッグ&ドロップしたときと同じようにNode3Dができる。(元の「ミカ」を消していないため「ミカ2」になってしまったが)
これもこの画面では中を見ることができないが、.tscnファイルの方を開くことで中を修正できる。
.glbを直接設置した方のNode3Dは削除し、今後はこちらを使っていく。
・アニメーションの再生
@onready var anim = $"ミカ/AnimationPlayer"
func _ready():
anim.play("Idle")
のコードだけでIdleという名前のモーションを再生できるとのことだったが、再生はされたもののモーションがループせず止まってしまった。

ファイルシステムの.glbファイルをダブルクリックすると中身を見ることができ、ここからモーションのループモードを設定して再インポートを押すことでモーションがループするようになった。(画像は少し開発が進んだファイルなのでIdleモーションが複数ある)
キャラが初期ポーズではなくなっており(遠くてわかりにくいが)待機モーションが再生され続けている。
extends CharacterBody3D
# キャラクターモデル内のAnimationPlayerを取得する
@onready var anim = $"ミカ/AnimationPlayer"
# プレイヤーの移動速度(m/s)
const SPEED = 2.0
func _physics_process(delta):
# 入力を取得する
var input_dir = Vector2.ZERO
input_dir.x = Input.get_axis("ui_left", "ui_right")
input_dir.y = Input.get_axis("ui_up", "ui_down")
# 入力方向に応じて移動速度を設定する
velocity.x = input_dir.x * SPEED
velocity.z = input_dir.y * SPEED
# 入力があるかどうかでアニメーションを切り替える
if input_dir.length() > 0:
# 既にWalkを再生中でなければWalkへ切り替える
if anim.current_animation != "Walk":
anim.play("Walk")
else:
# 既にIdleを再生中でなければIdleへ切り替える
if anim.current_animation != "Idle":
anim.play("Idle")
# CharacterBody3Dを移動させる
move_and_slide()
移動中と停止中でモーションを切り替える。
あとは向きが変わってカメラが動けばほとんどゲームだ。
実は重力が働いていないので床から落ちなかったりするのだがその辺りも含めてChatGPTとコードを追加していった。
完成品がこちら、結構紆余曲折あった。
ChatGPTに言われるままに機能を追加していったせいで実装順がこんがらがって途中で動作しなくなってしまった。
移動方向がカメラの向きによって変わる機能を実装しようとして失敗したのだがChatGPTの話がループし始めてしまったので、一度歩くだけのプログラムに戻し実装順をこちらで指定しながら順に組み込むことで何とかなった。
こういうことがあるのでコード自体はAIに書かせればいいのだが、マクロな全体の設計とミクロな個々の部品の動作をそれぞれ把握しておいて部品単位でAIに作り直させたり、最悪手作業で部品の組み立てだけ行ったりする必要はどうしてもあるだろう。
そのためにもコメントは細かに書いておいて、不明な部分は説明してもらっておくと安心できる。

完成品のシーン構成はこうなっており、Camera3DがCameraRigという名前のNode3Dに入っており、このCameraRigに短いプログラムがアタッチされている。
extends Node3D
@export var player: Node3D
var mouse_sens = 0.002
func _process(delta):
global_position = player.global_position
func _input(event):
if event is InputEventMouseMotion:
rotate_y(-event.relative.x * mouse_sens)
CameraRigがプレイヤーの位置に常についてくるようになっており、これの子ノードになっているCamera3Dも一緒についてくることになる。
extends CharacterBody3D
# キャラクターモデル内のAnimationPlayerを取得
@onready var anim = $"ミカ/AnimationPlayer"
# 移動方向決定のためカメラを取得
@export var camera_rig: Node3D
# プレイヤーの移動速度(m/s)
const SPEED = 0.7
# 重力加速度(m/s)
const GRAVITY = 20.0
func _physics_process(delta):
# WASD入力を取得
var input_dir = Vector2.ZERO
input_dir.x = Input.get_axis("ui_left", "ui_right")
input_dir.y = Input.get_axis("ui_up", "ui_down")
# 正規化(斜めを一定速度にする)
if input_dir.length() > 1:
input_dir = input_dir.normalized()
# CameraRigの向きを取得
var cam_basis = camera_rig.global_transform.basis
# カメラ基準の前後左右
var forward = cam_basis.z
var right = cam_basis.x
forward.y = 0
right.y = 0
forward = forward.normalized()
right = right.normalized()
# 入力方向をカメラ基準へ変換
var move_dir = right * input_dir.x + forward * input_dir.y
# 速度設定
velocity.x = move_dir.x * SPEED
velocity.z = move_dir.z * SPEED
# 移動方向へ向きを変える
if move_dir.length() > 0:
look_at(global_position + move_dir, Vector3.UP)
# 入力があるかどうかでアニメーションを切り替える
if input_dir.length() > 0:
# 既にWalkを再生中でなければWalkへ切り替える
if anim.current_animation != "Walk":
anim.play("Walk")
else:
# 既にIdleを再生中でなければIdleへ切り替える
if anim.current_animation != "Idle":
anim.play("Idle")
# CharacterBody3Dを移動
move_and_slide()
# 重力落下
if not is_on_floor():
velocity.y -= GRAVITY * delta
else:
velocity.y = 0
プレイヤーの方はこうなっている。
走り書きで制作記録を残すだけのつもりが、なんだかだんだん講座のような書き方になってしまった。
ゲームが形になるこの辺までは参考になることもあると思うのでひと段落するまで書いたが、これ以降はもっと雑に結果だけ残していく。