【Godot Engine】マインクラフトのスペクテイターモードっぽいカメラを作る

3Dゲームの実験的なプロジェクトでシーンの内容を確認する際,自由に操作できるカメラがあると便利である.マイクラのスペクテイターモードのイメージで適当に作ってみたらいい感じのものができたのでメモ.

この記事で作ったGodotプロジェクトのリポジトリはこちら: https://github.com/chqwa/godot-spectator-camera-example

メインシーンを作る

まず,カメラや地形などを配置するメインのシーンを作成する.

Godot Engineを起動してプロジェクトを作成したら, 3Dのルートノードを作成して保存する(ここではMainという名前にした).

Mainノード直下にWorldEnvironmentノードとDirectionalLight3Dノードを配置する.

WorldEnvironmentのインスペクターからプロパティを以下のように変更する:

  1. Environmentをクリックして新規Environmentを選択
  2. 作成したEnvironmentBackgroundModeSkyに設定.
  3. Backgroundの下にSkyという項目が現れるので,SkySky をクリックして新規Skyを選択
  4. 作成したSkySky Materialをクリックして新規ProcedualSkyMaterialを選択

ここまでで,WorldEnvironmentのインスペクターの内容が以下の画像のようになっていればよい.

main scene

続いて,Mainノード直下にMeshInstance3Dノードを追加する.MeshInstance3DインスペクターからMesh新規BoxMeshを設定する.

ここで,設定が正しいか確認するために仮のカメラを置いてシーンを一度実行しておくとよい(このカメラは後で削除する).Mainノード直下にCamera3Dノードを追加し,先ほど配置したMeshInstance3Dが見える位置に適当に動かす.

プロジェクトを実行し,以下の画像のような画面が出ればOK.

initial scene

キーマップを追加する

スペクテイターカメラを作成する前に,カメラ操作のためのキーマップをプロジェクト設定から追加しておく.

画面上のプロジェクトプロジェクト設定インプットマップを開く.インプットマップで以下の7つのアクションとキー割り当てを行う:

アクション キー
forward W
back S
left A
right D
up Space
down Shift
quit Esc

キー割り当ての後,以下の画面のようになっていればOK.

key input map

カメラ用のシーンを作る

画面上部のシーン新規シーンから3Dシーンを作成し,ルートノードの型をCamera3Dに,ノード名をSpectatorCameraに変更する.

カメラ制御スクリプトの作成と実行

ルートノードを右クリックし,スクリプトをアタッチするから新規スクリプトを作成する.

エディタを開いたら既存の記述を削除し,以下の内容に置き換える:

extends Camera3D


func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("quit"):
		get_tree().quit()

このスクリプトの内容について簡単に述べると,まず_readyでマウスカーソルを非表示にしてウインドウ内に留めるための設定を行っている._unhandled_inputはキー入力やマウス操作があったときに呼び出されるメソッドで,quitアクション,つまりESCキーの入力があった際にアプリケーションを終了する処理が記述されている.

スクリプトが書けたらMainシーンのCamera3Dノードを削除し,いま作成したSpectatorCameraに置き換える.先ほどと同じようにカメラの視界にMeshInstance3Dが入るよう適宜調整する.

ここでシーンを実行すると(カメラの配置が同じなら)先ほどと同じ画面が表示されるはずである.ただし,マウスカーソルがウインドウ内に留まって動かなくなっており,終了するにはESCキーを押す必要がある.

WASDキーによるカメラの水平移動処理

続いて,キー入力によってカメラを前後左右に動かす処理を記述する.

スクリプト冒頭でfloat変数speedVector3変数velocityを宣言する.変数speedには@exportアノテーションをつけておき,外部から値を変更できるようにしておく.

extends Camera3D

@export var speed: float = 4.0

var velocity: Vector3


func _ready() -> void:
    ...

_unhandled_inputメソッドの後に以下の_physics_processメソッドを記述する:

func _physics_process(delta: float) -> void:
	var input_dir = Input.get_vector("left", "right", "forward", "back")
	var direction = Vector3(input_dir.x, 0.0, input_dir.y).normalized()
	direction = direction.rotated(Vector3.UP, rotation.y)

	if direction:
		velocity.x = direction.x * speed
		velocity.z = direction.z * speed
	else:
		velocity.x = move_toward(velocity.x, 0, speed)
		velocity.z = move_toward(velocity.z, 0, speed)

	position += velocity * delta

_physics_processメソッドではキー入力された方向をVector2型変数input_dirで受け取った後,計算用にVector3型変数directionに変換するdirection = direction.rotated(Vector3.UP, rotation.y)の行は現時点では必要ないが,後ほどマウスによるカメラの回転を実装した際,「キー入力の正面」と「カメラの正面」を合わせる際に必要となる.

その後,directionがゼロベクトルでないなら(キー入力があったなら)それを速度velocityに反映し,そうでないなら速度がゼロに近づくようvelocityの値を更新する.最終的に速度に時間変化を掛けたベクトルを位置positionに足してカメラ位置を更新する.

プロジェクトを実行すると,キー入力によってカメラを水平方向に動かせることが確認できる.

カメラの上下移動処理

ShiftおよびSpaceによる上下移動も水平移動と同様に実装可能である.

上下移動は以下のようにInput.get_axis("down", "up")でキー入力を取得し,その値をvelocity.yに反映することで実現できる.

func _physics_process(delta: float) -> void:
	var up_down = Input.get_axis("down", "up")  # ***新たに追加***
	var input_dir = Input.get_vector("left", "right", "forward", "back")
	var direction = Vector3(input_dir.x, 0.0, input_dir.y).normalized()

	if direction:
		velocity.x = direction.x * speed
		velocity.z = direction.z * speed
	else:
		velocity.x = move_toward(velocity.x, 0, speed)
		velocity.z = move_toward(velocity.z, 0, speed)

	velocity.y = up_down * speed  # ***新たに追加***

	position += velocity * delta

マウス移動によるカメラの回転

マウス移動によるカメラの回転処理を実装する.

まず,カメラの回転に関するパラメタとして,カメラ感度を表す変数sensitivityおよび回転範囲の限界を示す変数tilt_limitをスクリプト冒頭に記述する.

extends Camera3D

@export var speed: float = 4.0
@export_range(0.0, 1.0) var sensitivity = 0.005  # ***新たに追加***
@export var tilt_limit = deg_to_rad(90)  # ***新たに追加***

var velocity: Vector3


func _ready() -> void:
    ...

_unhandled_inputメソッド内にマウス移動時のイベント処理を追加する._unhandled_inputは入力がキーボードでもマウスでも共通して呼び出されるメソッドなので,メソッド内で条件式によって処理を分ける.条件式event is InputEventMouseMotionによってマウスが移動した時のイベントのみを取り出せる.Vector2型変数event.relativeからマウスの移動量を取得できるので,これを使ってカメラの回転処理を以下のように書くことができる.

func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("quit"):
		get_tree().quit()

	if event is InputEventMouseMotion:
		var camera_dir = Vector2(event.relative.x, event.relative.y)
		rotation.x += -camera_dir.y * sensitivity
		rotation.x = clampf(rotation.x, -tilt_limit, tilt_limit)
		rotation.y += -camera_dir.x * sensitivity

回転処理の式は一見すると,左辺と右辺でxyが入れ替わってて複雑に見えるが,要は

  • マウスが前後 (Y軸方向) に動いたらカメラを垂直方向 (X軸周り) に回転
  • マウスが左右 (X軸方向) に動いたらカメラを水平方向 (Y軸周り) に回転

しているだけである.負号についてはエディタ上でカメラを実際に回転させながらTransformRotationの値を見ながら理解するとよい.

スクリプトの完成形

最終的なスクリプトは以下のようになる.

extends Camera3D

@export var speed: float = 4.0
@export_range(0.0, 1.0) var sensitivity = 0.005
@export var tilt_limit = deg_to_rad(90)

var velocity: Vector3


func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("quit"):
		get_tree().quit()

	if event is InputEventMouseMotion:
		var camera_dir = Vector2(event.relative.x, event.relative.y)
		rotation.x += -camera_dir.y * sensitivity
		rotation.x = clampf(rotation.x, -tilt_limit, tilt_limit)
		rotation.y += -camera_dir.x * sensitivity


func _physics_process(delta: float) -> void:
	var up_down = Input.get_axis("down", "up")
	var input_dir = Input.get_vector("left", "right", "forward", "back")
	var direction = Vector3(input_dir.x, 0.0, input_dir.y).normalized()
	direction = direction.rotated(Vector3.UP, rotation.y)

	if direction:
		velocity.x = direction.x * speed
		velocity.z = direction.z * speed
	else:
		velocity.x = move_toward(velocity.x, 0, speed)
		velocity.z = move_toward(velocity.z, 0, speed)

	velocity.y = up_down * speed

	position += velocity * delta

3D酔い防止のマークをつける

以上で機能としては完成なのだが,このまま使ってみると3D酔いしそうになるのがわかると思う.そこで,3D酔いを軽減するためのマークをUI機能を使って作成する.

SpectatorCameraシーンの直下にCanvasLayerを追加し,その下にControl,その更に下にLabelを配置する.CanvasLayerは3Dシーン上に2DシーンであるControl系のノードを描画するために必要なノードである.

ControlAnchors PresetRect前面にし,Container SizingHorizontalVerticalを両方縮小中心地に変更する.インスペクターの内容が以下の画像のようになっていればよい.

UI Layout

GodotのUI設定をインスペクター上で行うのはやや煩雑なため,シーンの表示を2Dに切り替えてから (画面上部中央の2D),シーンドック上部右側にある緑色のアイコンから設定するとよい.

UI Editing

先ほど追加したLabelAnchors PresetRect前面にする.また,Textの文字列を+にし,テキストを上下左右中央揃えにするためにHorizontal AlignmentVertical Alignmnentを両方Centerに設定する.

最後に,Controlの設定に戻り,MouseFilterプロパティをPassに変更する (項目が多くて見つけづらいので検索機能を使うとよい.Label側にも同じ項目があるので間違えないように注意).実際に試してみるとわかるが,この設定をしなかった場合,UIにマウス入力が吸われてカメラの操作ができなくなってしまう.

プロジェクトを実行すると,以下の様に画面中心に常に+マークが表示される.今回はカメラで映す対象がBoxMeshひとつなので実感しづらいかもしれないが,これが大きな地形になった場合はかなりの効力を発揮するのがわかると思う.

final