第3回目の今回は、プレイヤーキャラクターの動きに合わせてカメラが移動し、Part 2 の時より広いタイルマップ上をキャラクターが移動できるようにしていく。

なお、2Dゲームのカメラについて、公式ドキュメントにも説明があるので、併せて確認いただくのが良いだろう。

公式オンラインドキュメント:
Camera2D

Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー



Level1 シーンに Camera2D ノードを追加する

2Dゲームにカメラを追加するには、「Camera2D」ノードを利用する。ではさっそく「Level1」ノードに「Camera2Dノード」を追加しよう。ノードの並び順を少し変えているがこの時点ではただの好みだ。インスタンスノードを下に持ってきている。

Camera2Dノードを追加

インスペクターで「Camera2D」ノードの「Current」プロパティをオンにしておこう。これがオフだとその『Camera2D」ノードはカメラとして機能しない。カメラを複数用意してそれらで画面を切り替えるようなゲームでない限り、このプロパティは基本的にオンになるだろう。
Currentプロパティをオンにする



カメラをプレイヤーキャラクターに連動させる

続いて、追加した「Camera2D」ノードがプレイヤーを追跡するようにしていく。これは GDScript で制御していくのだが、新しいスクリプトをアタッチするノードは「Camera2D」ノードではなく「Level1」だ。理由は、「Player」ノードと「Camera2D」ノードの両方にアクセスしやすいのが「Level1」ノードだからだ。

ではシーンドックで「Level1」ノードを選択し、スクリプトをアタッチしよう。

スクリプトのパスは「res://Levels/Lvel1.gd」する。テンプレートは「No Comments」にして、デフォルトのコメントやメソッドを削除する手間を省略しよう。問題なければ、「作成」をクリックしてスクリプトを開こう。

Level1.gdスクリプトを作成

スクリプトを開いたら、まず4行目以降にonreadyキーワード付きのプロパティを3つ用意しよう。

extends Node2D

onready var player = $Player
onready var map = $TileMap
onready var camera = $Camera2D

追加した3つのプロパティには、それぞれ「Level1」ノードの子ノードである「Player」、「TileMap」、「Camera2D」を代入している。これらの子ノードにアクセスする必要があるので、事前にプロパティとして用意している。

続いて、組み込みメソッドの_processを追加しよう。

func _process(_delta):
	camera.global_position = player.global_position

_processメソッドは_physics_processメソッドと似ているが少し異なる。どちらも引数がdeltaとなっているが、_physics_processの場合は毎フレーム一定の長さのためdeltaも常に一定だ。しかし_processメソッドの場合、1フレームの長さはその時の処理の量によって変動する。つまりdeltaが一定ではない。きっちり一定間隔で処理が必要な場合は_physics_processメソッドを使い、常に処理は必要だが、正確に一定間隔である必要はない場合は_processメソッドを使う、という認識で良いだろう。

ちなみに、メソッド内で引数deltaを使うことがない場合は引数の表記を_deltaとしておくと余計なアラートが出なくなる。

さて_processメソッド内に記述したコードは、「Camera2D」ノードのglobal_positionの値を「Player」ノードのglobal_positionの値と同じにする処理だ。

なお、ここではglobal_positionという組み込みのプロパティを利用しているが、現時点でのシーンツリーの構造が変更されない限りはpositionプロパティでも問題ない。なぜならpositionは親ノードの位置からの相対的位置を示し、global_positionはゲーム画面上の絶対的な位置を示すからだ。シーンツリーの構造がいつ更新されるかわからない場合の複数ノードの位置の利用はglobal_positionを使用するのが得策だろう。

では、カメラがプレイヤーキャラクターについてくるか確認してみよう。

カメラがキャラクターに連動するか確認

きっちりキャラクターをカメラの中心に捉えたまま連動しているので、OKとしよう。
なお、このタイミングで、プレイヤーキャラクターの移動速度が快適ではなかったので、以下のプロパティの値変更をした。あなたもご自身が気持ち良いと感じる値に適宜調整してほしい。

  • max_speed: 80
  • max_dash_speed: 120


タイルマップを発展させる

前回の Part2 ではあまりタイルマップを作り込まなかったので、カメラが動くようになったこのタイミングで、きっちり「Level1」シーンのマップを作ってしまおう。

使用するタイルは、Part 2 で作成した「earth」アトラスと「blocks」アトラスだけでOKだ。これらのタイルで、初めてプレイヤーが体験するレベルをイメージしてタイルマップを完成させよう。

ここでプレイヤーキャラクターのサイズと動きによって、下記の制約があるのでご注意いただきたい。

  • キャラクターはタイル2個分の高さ
    キャラクターはタイル2個分の高さ
  • キャラクターはタイル3個分の高さまでしかジャンプで飛び乗れない
    キャラクターはタイル3個分の高さまでしかジャンプで飛び乗れない
  • キャラクターは通常のスピードではタイル3個分の幅までしかジャンプで飛び越えられない
    キャラクターは通常のスピードではタイル3個分の幅までしかジャンプで飛び越えれない

サンプルとして、このチュートリアルでは以下のようなマップを作成した。もっと短くてもいいし、長くてもいい。ここは完全にあなたの自由だ。

タイルマップを発展させる

なお、タイルマップを作成中、2Dワークスペースの左下には現在のカーソルの位置とどの種類のタイルが配置されているかが表示される。例えば、今回のサンプルのタイルが配置されている一番右下の位置にカーソルを合わせると、「155, 15 [blocks]」と表示されている。x, y が (0, 0)の位置から数えて、右方向に 155 マス、下方向に 15 マスの位置までタイルを配置していることがわかる。

タイルマップ上のタイルの位置と種類

それでは、このタイルマップでプレイヤーキャラクターを動かしてみよう。

タイルマップでプレイヤーキャラクターを動かしてみる

カメラが常にプレイヤーキャラクターを中央に捉えたままだが、できればカメラに映されるプレイ画面は、配置されたタイルの一番端を超えないようにしたいところだ。次はこの部分を更新していく。

タイルマップの端に合わせてカメラの移動範囲を制限する

それでは改めて「Level1.gd」スクリプトを開いて編集していく。

まずは_readyメソッドを追加する。

func _ready():
	adjust_camera()

メソッド内でadjust_cameraメソッドを実行するようにコーディングした。このメソッドを今から定義する。

func adjust_camera():
	var map_limits = map.get_used_rect()
	print("map_limits", map_limits)
	var map_cell_size = map.cell_size
	print("map_cell_size", map_cell_size)

「TileMap」ノードのメソッドにget_used_rectがある。これは現在のタイルマップでタイルが配置されている範囲を返してくれる。返される値は(position.x, position.y, end.x, end.y)の形式だが、それぞれの値は pixel ではなく、グリッド数、つまりタイルのマス目の数だ。

同じく「TileMap」ノードのメソッドにcell_sizeがある。これはタイル一つ分の縦・横のサイズを Vector2 型の値で返してくれる。

print関数は、「TileMap」ノードの2つのメソッドでどのような値が返されるのかを確認するために追加している。さっそくプロジェクトを実行して、print関数の出力結果を見てみよう。
TileMapのメソッドで返される値を確認

出力パネルの結果をみると、map_limitsメソッドで返される値は(0, 1, 156, 15)で、cell_sizeメソッドで返される値は(16, 16)だった。これらの値を利用して、カメラの移動範囲を制限していく。

具体的には、map_limitsメソッドで得られる結果のそれぞれの要素に対して、cell_sizeメソッドで得られる結果の x または y の値を乗算することで、タイルを配置している上下左右の範囲を pixel 単位で取得することができる。その値を「Camera2D」ノードの移動制限用のプロパティlimit_xxxに適用すれば良い。

ではadjust_cameraメソッドを以下のように更新しよう。

func adjust_camera():
	var map_limits = map.get_used_rect()
	#print("map_limits", map_limits)
	var map_cell_size = map.cell_size
	#print("map_cell_size", map_cell_size)
  camera.limit_left = map_limits.position.x * map_cell_size.x
	camera.limit_right = map_limits.end.x * map_cell_size.x
	#camera.limit_top = map_limits.position.y * map_cell_size.y #指定しない
	camera.limit_bottom = map_limits.end.y * map_cell_size.y
	camera.limit_smoothed = true

「Camera2D」ノードのlimit_leftlimit_rightのプロパティはそれぞれの方向に対するカメラの移動制限を pixel 単位で指定することができる。

ただ、「Camera2D」ノードのlimit_topの値だけ指定しない。理由は2つある。

1つは、最も高い位置のタイルに乗ってさらにプレイヤーキャラクターがジャンプする場合、キャラクターが画面上に全く映らない状態になってしまうからだ。

もう1つの理由は、limit_topの値の方がlimit_bottomの値よりも優先されてしまうからだ。画面上最も上にあるタイルの y 軸の位置が 0 グリッドより大きい(画面下方向)場合、カメラが常にlimit_bottomの値を超えた状態になり得るからだ。下のスクリーンショットがサンプルだ。1タイル分下に下がってしまっているのは、画面上最も上に位置するタイルが y 軸上 0 グリッドではなく 1 グリッドの位置にあるためだ。
画面下方向の移動制限を超えた状態

最後のlimit_smoothedプロパティは、その値がtrueの場合はカメラの移動制限範囲に到達した時に、カメラがスムーズに止まる。

printメソッドはもう不要なので削除かコメントアウトしておこう。

では、これで一度プロジェクトを実行して、カメラがタイルを配置している範囲までしか移動しないことを確認しよう。

画面下方向の移動制限を超えた状態

現状、カメラはタイルマップの端で止まるようになったが、プレイヤーキャラクターはカメラに映らなくなっても動けてしまう状態だ。

プレイヤーキャラクターの画面左方向の移動を制限しておこう。「Player.gd」スクリプトの一番最後に以下のコードを追加する。

if position.x < 16:
  position.x = 16

プレイヤーキャラクターのスプライトのテクスチャが 32 x 32 px なので、16 px がちょうどプレイヤーキャラクターの中心の値になる。プレイヤーキャラクターの中心がposition.x == 16の位置に来た時、プレイヤーキャラクターの左端がちょうどゲーム画面の左端と一致しているはずだ。だからposition.xが 16 未満の場合はposition.xを16にするようにした。これで画面左端に見えない壁ができたような状態になり、プレイヤーキャラクターはそれ以上左側に進むことができなくなった。

実際にプロジェクトを実行して確認してみよう。

画面左端でプレイヤーが止まるか確認

プレイヤーキャラクターが画面右端に到達した場合の同様の処理は行わない。なぜなら、右方向はキャラクターの進行方向なので、まだ先に行けそうなのに行けないと、プレイヤーは違和感を感じてしまうからだ。代わりに、タイルマップのタイルの配置を工夫して対処する。

例えば、完全に壁を作ってしまうのも一つだ。
右端に壁を作るパターン

もしくは、一番右端のタイルをキャラクターが行き着けない場所に配置するのも良いだろう。
右端のタイルに到達できなくするパターン

プレイヤーキャラクターが画面下方向に画面から消えた場合は、ライフを減らすか、ゲームオーバーにするなどの実装を今後やっていくことになるので、今はこのままにしておこう。



Part 3 で編集したスクリプトのコード

最後に今回の Part 3 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。

Level1.gd の全コード
extends Node2D # Created @ Part 3


onready var player = $Player
onready var map = $TileMap
onready var camera = $Camera2D


func _ready():
	adjust_camera()

func _process(_delta):
	camera.global_position = player.global_position

func adjust_camera():
	var map_limits = map.get_used_rect()
	print("map_limits", map_limits)
	var map_cell_size = map.cell_size
	print("map_cell_size", map_cell_size)
	camera.limit_left = map_limits.position.x * map_cell_size.x
	camera.limit_right = map_limits.end.x * map_cell_size.x
	#camera.limit_top = map_limits.position.y * map_cell_size.y # 指定しない
	camera.limit_bottom = map_limits.end.y * map_cell_size.y
	camera.limit_smoothed = true
Player.gd の全コード
extends KinematicBody2D # Created @ Part 1


export var acceleration = 256
export var max_speed = 64
export var max_dash_speed = 200
export var friction = 0.1
export var gravity = 512
export var jump_force = 224
export var air_resistance = 0.02
var velocity = Vector2()

onready var sprite = $AnimatedSprite


func _physics_process(delta):
	velocity.y += gravity * delta
	var x_input = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	
	if x_input != 0:
		velocity.x += x_input * acceleration
		if Input.is_action_pressed("dash"):
			velocity.x = clamp(velocity.x, -max_dash_speed, max_dash_speed)
		else:
			velocity.x = clamp(velocity.x, -max_speed, max_speed)
		sprite.flip_h = x_input < 0
	
	if is_on_floor():
		if x_input == 0:
			sprite.play("idle")
			velocity.x = lerp(velocity.x, 0, friction)
		else:
			sprite.play("run")

		if Input.is_action_just_pressed("jump"):
			sprite.play("jump")
			velocity.y = -jump_force

	else:
		if x_input == 0:
			velocity.x = lerp(velocity.x, 0, air_resistance)

		if Input.is_action_just_released("jump") and velocity.y < -jump_force / 2:
			velocity.y = -jump_force / 2

	velocity = move_and_slide(velocity, Vector2.UP)
	
	# Added @ Part 3
	if position.x < 16:
		position.x = 16


おわりに

以上で Part 3 は完了だ。カメラ用意して、タイルマップをさらに拡大させて、その上をキャラクターに走らせることができた。ようやくプラットフォーマーの骨格ができてきた。

次回は敵キャラクターを追加する予定なので、お楽しみに。