第3回目の今回は、プレイヤーキャラクターの動きに合わせてカメラが移動し、Part 2 の時より広いタイルマップ上をキャラクターが移動できるようにしていく。
なお、2Dゲームのカメラについて、公式ドキュメントにも説明があるので、併せて確認いただくのが良いだろう。
公式オンラインドキュメント:
Camera2D
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー
Level1 シーンに Camera2D ノードを追加する
2Dゲームにカメラを追加するには、「Camera2D」ノードを利用する。ではさっそく「Level1」ノードに「Camera2Dノード」を追加しよう。ノードの並び順を少し変えているがこの時点ではただの好みだ。インスタンスノードを下に持ってきている。
インスペクターで「Camera2D」ノードの「Current」プロパティをオンにしておこう。これがオフだとその『Camera2D」ノードはカメラとして機能しない。カメラを複数用意してそれらで画面を切り替えるようなゲームでない限り、このプロパティは基本的にオンになるだろう。
カメラをプレイヤーキャラクターに連動させる
続いて、追加した「Camera2D」ノードがプレイヤーを追跡するようにしていく。これは GDScript で制御していくのだが、新しいスクリプトをアタッチするノードは「Camera2D」ノードではなく「Level1」だ。理由は、「Player」ノードと「Camera2D」ノードの両方にアクセスしやすいのが「Level1」ノードだからだ。
ではシーンドックで「Level1」ノードを選択し、スクリプトをアタッチしよう。
スクリプトのパスは「res://Levels/Lvel1.gd」する。テンプレートは「No Comments」にして、デフォルトのコメントやメソッドを削除する手間を省略しよう。問題なければ、「作成」をクリックしてスクリプトを開こう。
スクリプトを開いたら、まず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個分の高さ
- キャラクターはタイル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
関数の出力結果を見てみよう。
出力パネルの結果をみると、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_left
やlimit_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 は完了だ。カメラ用意して、タイルマップをさらに拡大させて、その上をキャラクターに走らせることができた。ようやくプラットフォーマーの骨格ができてきた。
次回は敵キャラクターを追加する予定なので、お楽しみに。