Godot Engine Advent Celendar 2022 21日目
この記事はGodot Engine Advent Celendar 2022 21日目の記事となります。
この記事では、Godot Engine を使用した Jump ’n’ run game (ラン&ジャンプゲーム) の作り方を解説します。
目次
ラン&ジャンプゲームの作り方
ラン&ジャンプゲームとは
ラン&ジャンプゲームとは、 ジャンプで足場を飛び移ることを目的とした. Platformer (プラットフォーマー) のプレイヤーの移動を自動にしたものです。
おそらくですが、このジャンルは、携帯電話のアプリで登場した「チャリ走」が始まりなのではないかと思っています。
キャラクターは、自動で右方向に移動してしまうため、プレイヤーができることは「タイミング良くジャンプして足場を飛び移る」という操作のみで、そのゲームプレイに注力したステージ構成となっています。
その後、BIT.TRIP RUNNER などスタイリッシュな見た目になったり、そのお手軽なゲームプレイから、スマートフォン向けのアプリとしてスーパーマリオ ランなどが登場したりと、何かとカジュアルユーザーに人気のジャンルです。
最近の傾向としては、スマートフォンを縦持ちでプレイできることから、3Dで奥に向かって進むタイプのゲームが人気のようです。
ただ今回の記事では 「2D機能」で実装するため、2Dの Side scroller (サイドビュー) で ラン&ジャンプ ゲームの作り方を紹介します。
使用する素材
今回使用する素材は以下のものとなります。
run-and-jump
+-- bg_back.png: 背景画像 (奥)
+-- bg_sky.png: 背景画像 (空)
+-- floor.png: 足場画像
+-- mplus_theme.tres: M+フォントテーマ
+-- plus-1c-reqular.ttf: M+フォント(TTF)
+-- player.png: プレイヤー画像
+-- spike.png: トゲ画像
プロジェクト作成
それではプロジェクトを作成します。名前は何でもOKですが今回は「TestRunAndJump」としておきます。
プロジェクトを作成したら、素材データをすべて「ファイルシステム」にドラッグ&ドロップして追加しておきます。
プレイヤーの作成
まずはプレイヤーを作成します。プラットフォーマーを作るときのキャラクターは「KinemeticBody2D」だと都合が良いので、「KinematicBody2D」ノードを作成します。
ノード名は「Player」に変更しておきます。
次に「player.png」をキャンバスにドラッグ&ドロップします。
この「player.png」は、8パターンのスプライトシートなので、Playerスプライトの「Animation > Hframes」の値を「8」にします。
続けてコリジョンノードの「CollisionShape2D」を作成します。
ここまでのノード構成は以下のとおりです。
コリジョンの形状が設定されていないので、「CollisionShape2D」のインスペクタから「Shape > [空]」をクリックして「新規 RectangleShape2D (矩形コリジョン)」を選びます。
するとコリジョンが作られるのですが、スプライトの位置やコリジョンのサイズが合っていないので、調整をします。
おおよそこのようになれば良いです。
ただ、スプライトの足元と中央は、できれば揃うようになっていた方が良いです。
念のためパラメータをのせておきますが、このあたりの設定が面倒な場合は、プレイヤー作成ができたらいったんプロジェクトを添付しますので、そのデータをつかってしまっても良いです。
- Playerスプライト
- Transform > Position > x: 0
- Transform > Position > y: -28
- CollisionShape2Dノード
- CollisionShape2D > Shape: RectangleShape2D
- CollisionShape2D > Extents > x: 10
- CollisionShape2D > Extents > y: 24
- Transform > Position > x: 0
- Transform > Position > y: -24
最後にスクリプトをアタッチします。
そしてスクリプトを以下のように記述します。
extends KinematicBody2D
const ANIM_SPEED = 7 # アニメーション速度
const GRAVITY = 45 # 重力
const JUMP_POWER = 1000 # ジャンプ力
const MOVE_SPEED = 300 # 移動速度.
const MAX_JUMP_CNT = 2 # 2段ジャンプまで可能.
onready var _spr = $Player # スプライト
var _tAnim:float = 0 # アニメーションタイマー.
var _velocity := Vector2.ZERO # 移動ベクトル.
var _is_jumping = false # ジャンプ中かどうか.
var _cnt_jump = 0 # ジャンプ回数.
var _speed := MOVE_SPEED # 移動速度.
# 更新.
func _process(delta: float) -> void:
# アニメーションの更新.
_update_anim(delta)
func _update_anim(delta:float) -> void:
_tAnim += delta
var t = int(_tAnim * ANIM_SPEED) % 2
var tbl = [2, 3] # 走りアニメーションしか使わない.
_spr.frame = tbl[t]
後々必要な定数や変数も追加しているので、少しややこしいですが、ここでやっているのはアニメーションの更新だけで、スプライトのフレームの「2」と「3」を交互に切り替えているだけです。
これを動かすためにいったんメインとなるシーンを作成します。
「+」をクリックするなどして新規シーンを作成します。
2Dシーンをクリックします。
新規で2Dシーンが作成されるので、名前を「Main」に変更します。
Mainシーンに「Player.tscn (プレイヤーシーン)」をドラッグ&ドロップで配置します。
Mainシーンを実行して配置したプレイヤーが走りアニメーションを行うのを確認します。
ここまでのプロジェクトファイルを添付しておきます。
足場(Floor)の作成
新しくシーンを作成するので「+」をクリックします。
「その他のノード」をクリックして、「StaticBody2D」を作成します。
ノード名は「Floor」に変更しておきます。
次に「floor.png」をキャンバスにドラッグ&ドロップして Spriteを作成します。
続けて「CollisionShape2D」を作成します。
ノード階層は以下のようになります。
CollsionShapeの種類が設定されていないので、インスペクタから「Shape > [空]」をクリックして「新規 RectangleShape2D」を選びます。
そうしたらコリジョンのサイズをスプライトの大きさに合わせます。
Extents の (x, y) = (30, 8) になるかと思います。
Floorシーンを保存したら、Mainシーンに配置します。
では実行して確認…と思ったのですが、プレイヤーの重力を設定していなかったので、そのあたりを設定します。
Player.gd を開いて以下のように修正します。
const JUMP_POWER = 1000 # ジャンプ力
const MOVE_SPEED = 300 # 移動速度.
const MAX_JUMP_CNT = 2 # 2段ジャンプまで可能.
onready var _spr = $Player # スプライト
var _tAnim:float = 0 # アニメーションタイマー.
var _velocity := Vector2.ZERO # 移動ベクトル.
var _is_jumping = false # ジャンプ中かどうか.
var _cnt_jump = 0 # ジャンプ回数.
var _speed := MOVE_SPEED # 移動速度.
# 更新.
func _process(delta: float) -> void:
# アニメーションの更新.
_update_anim(delta)
func _update_anim(delta:float) -> void:
_tAnim += delta
var t = int(_tAnim * ANIM_SPEED) % 2
var tbl = [2, 3] # 走りアニメーションしか使わない.
_spr.frame = tbl[t]
# -----------------------
# ここから重力とジャンプ処理
# -----------------------
func _physics_process(delta: float) -> void:
# 重力を反映.
_velocity.y += GRAVITY
_velocity = move_and_slide(_velocity)
if Input.is_action_just_pressed("ui_accept"):
# ジャンプ.
_velocity.y = -JUMP_POWER
追加したのは「ここから重力とジャンプ処理」以下の部分となります。
では実行して動作を確認します(SPACEキーでジャンプします)。
ここまで実装した内容のプロジェクトファイルを添付しておきます。
プレイヤーのジャンプ周りの調整
ひとまずジャンプできるようになったのですが、さらにプレイヤーのジャンプアニメーションや2段ジャンプを実装しておきます。
現状だと「走りアニメーション」と「ジャンプ中のアニメーション」に変化がないので、ここを修正します。
「Player.gd」を開いて “_physics_process()” を以下のように修正します。
func _physics_process(delta: float) -> void:
# 重力を反映.
_velocity.y += GRAVITY
# ※ここの第2引数を追加
_velocity = move_and_slide(_velocity, Vector2.UP)
# ※ジャンプ中チェック.
if is_on_floor():
# 着地した
_is_jumping = false
_velocity.y = 0 # 重力クリア.
else:
# ジャンプ中.
_is_jumping = true
if Input.is_action_just_pressed("ui_accept"):
# ジャンプ.
_velocity.y = -JUMP_POWER
修正は2ヵ所です。
- move_and_slide() の第2引数に Vector2.UP を追加
- is_on_floor() で着しているかどうかのチェックを追加
is_on_floor() は 地面に着地しているかどうかを判定する便利な関数なのですが、それを有効にするためには move_and_slide() の第2引数にどの方向 (ベクトル) から接触したら床と扱うのか、という情報を与える必要があります。
これでジャンプ中かどうか (_is_jumping) を判定できるようになったので、_update_anim() を以下のように修正します。
func _update_anim(delta:float) -> void:
_tAnim += delta
if _is_jumping:
# ジャンプ中
_spr.frame = 2
return
var t = int(_tAnim * ANIM_SPEED) % 2
var tbl = [2, 3] # 走りアニメーションしか使わない.
_spr.frame = tbl[t]
実行して、ジャンプ中は「2」フレーム目のスプライト画像で固定されていることを確認します。
Player.gd の _physics_process() を以下のように修正して Double Jump (2段ジャンプ) を実装します。
func _physics_process(delta: float) -> void:
# 重力を反映.
_velocity.y += GRAVITY
# ※ここの第2引数を追加
_velocity = move_and_slide(_velocity, Vector2.UP)
# ジャンプ中チェック.
if is_on_floor():
# 着地した
_is_jumping = false
_velocity.y = 0 # 重力クリア.
_cnt_jump = 0 # ジャンプ回数をリセットする.
else:
# ジャンプ中.
_is_jumping = true
if _cnt_jump < MAX_JUMP_CNT:
if Input.is_action_just_pressed("ui_accept"):
# ジャンプ.
_velocity.y = -JUMP_POWER
_cnt_jump += 1
ジャンプするたびに _cnt_jump をカウントアップして、2回までジャンプできるようにします。
では、実行して動作を確認します。
これでも良いのですが、2段ジャンプできるかどうかが一見わかりにくいという問題があります。
そこで2段ジャンプが可能な場合、front flip (前方宙返り) をするようにしてみます。_update_anim() を以下のように修正します。
func _update_anim(delta:float) -> void:
_tAnim += delta
if _is_jumping:
# ジャンプ中
if _cnt_jump == 1:
# 2段ジャンプできるので前方宙返りする.
_spr.rotation_degrees += 2000 * delta
else:
_spr.rotation_degrees = 0
_spr.frame = 2
return
# 回転をリセットする.
_spr.rotation_degrees = 0
var t = int(_tAnim * ANIM_SPEED) % 2
var tbl = [2, 3] # 走りアニメーションしか使わない.
_spr.frame = tbl[t]
では実行して動作を確認します。
単に回転させているだけで、専用のアニメーションでないため、少し動きが粗いように見えますが、前よりは明確に2段ジャンプ可能なタイミングが視認できるようになります。
ここまで実装したプロジェクトファイルを添付しておきます。
スクロールの実装
続けてスクロールを実装します。
Mainシーンに Camera2D を作成して配置します。
初期状態だとカメラの位置が (0, 0) に配置されてしまっているので、画面中央に移動しておきます。
Camera2D のインスペクタから、「Node2D > Transform > Position」の (x, y) を (512, 300) にすることでも調整できます。
さらに インスペクタから「Current」にチェックを入れて有効化しておきます。
次に、Mainシーンにスクリプトをアタッチします。
スクリプトには以下のように記述します。
extends Node2D
# カメラの座標関連
const CAMERA_POS_Y = 320 # カメラの位置(Y).
const CAMERA_OFS_X = 400 # プレイヤーからのオフセット
onready var _player = $Player # プレイヤー.
onready var _camera = $Camera2D # カメラ.
func _ready() -> void:
pass
func _process(delta: float) -> void:
# カメラをプレイヤーに追従させる.
_camera.position.x = _player.position.x + CAMERA_OFS_X
Playerノードからプレイヤー位置を取得し、それをカメラの X座標に反映させます。さらにカメラ位置はプレイヤーよりも手前にすることで、プレイヤーの表示位置を画面の左側に固定します。
実行すると、以下のような配置となっていることが確認できます。
カメラのY座標は画面中央で、X座標はプレイヤーよりも「前方」にしたいため「+400 (CAMERA_OFS_X)」となっています。
最後に Player.gd を開いて、_physics_process() に移動処理を実装しておきます。
func _physics_process(delta: float) -> void:
# 移動速度を反映.
_velocity.x = _speed
# 重力を反映.
_velocity.y += GRAVITY
……
修正箇所は最初の「移動速度を反映」を追加するだけです。
では実行して動作を確認します。
スクロールは実装できた…ように見えるのですが、プレイヤーがすぐに落下して消えてしまいます。
Player.gd の _physics_process() を編集して落下して消えないようにしておきます。
func _physics_process(delta: float) -> void:
# 移動速度を反映.
_velocity.x = _speed
# 重力を反映.
_velocity.y += GRAVITY
# デバッグ用 (画面外に出ないようにする)
if position.y > 680:
_velocity.y = -1500
# ※ここの第2引数を追加
_velocity = move_and_slide(_velocity, Vector2.UP)
……
追加したコードは「デバッグ用」の部分となります。
では実行して、画面外に出たときに上方向に移動して復帰することを確認します。
スクロールする背景を表示して、プレイヤーが右へ進んでいる印象を強くします。
なお、背景をスクロールさせる方法の詳細については、以下の記事で詳しく書いています。
【Godot】多重スクロールの実装方法まずは Main シーンに ParallaxBackground を追加します。
背景は「最背面」に表示したいので、Mainノードの直下に移動させます。
そして「ParallaxLayer」ノードを 2つ追加します。
このようなノード構成になればOKです。
そして、「ParallaxLayerノード」に「bg_back.png」を配置、「ParallaxLayer2ノード」に「bg_sky.png」を配置します。
スプライトの位置は、それぞれ “Offset > Centered” のチェックを外し、Transform > Position を (0, 0) にすることで画面にピッタリ表示されるようになります。
そして、”ParallaxLayerノード” (BgBackスプライトが含まれるもの) を選び、インスペクタから以下のように設定します。
- Motion > Scale > x: 0.7
- Motion > Mirroring > x: 1024
これは、BgBack (町並みの背景) を 1024px (画面サイズ) でミラーリングし、カメラ速度の0.7の速さ (少しゆっくり) でスクロールする設定となります。
次に “ParallaxLayer2ノード” (BgSkyスプライトが含まれるもの) を選び、インスペクタから以下のように設定します。
- Motion > Scale > x: 0.2
- Motion > Mirroring > x: 1024
これは、BgSky (空の背景) を 1024px (画面サイズ) でミラーリングし、カメラ速度の0.2倍の速さ (ゆっくり) でスクロールする設定です。なぜ空の背景をここまでゆっくりにするのかというと、空は距離的に遠いため、ゆっくりにすることで奥行きを表現できるようになるためです。
では実行して背景がスクロールすることを確認します。
ここまで実装したプロジェクトファイルを添付しておきます。
足場を自動生成する
スクロールが実装できたので、次に足場の自動生成を実装します。
まずは足場管理用のノードとして Node2D を Mainシーンに追加します。
ノードの名前は「FieldMgr」にリネームしておきます。
そして、FieldMgr ノードの下の階層に「Floor」オブジェクトをすべて移動させておきます。
今後、足場オブジェクトはこの「FieldMgr」の下の階層にすべて配置します。
ゲーム開始状態の足場をいくつか追加したいので、現在配置済みの足場を複製したりして、以下のように並べておきます。
Godot のエディタは、「複製」や「スナップして移動」などの便利ツールが用意されているので、この機能で複製すると楽です。
少しややこしいので、手順を動画にしておきました。
Main.gd を開いて、足場を生成する処理を実装します。
extends Node2D
# 生成するシーン
var obj_floor = preload("res://Floor.tscn") # 床オブジェクト
# カメラの座標関連
const CAMERA_POS_Y = 320 # カメラの位置(Y).
const CAMERA_OFS_X = 400 # プレイヤーからのオフセット
const FLOOR_INTERVAL = 64 # 足場は 64px 単位で登場する.
const SCREEN_W = 1024 # 画面の幅.
onready var _player = $Player # プレイヤー.
onready var _camera = $Camera2D # カメラ.
onready var _field_mgr = $FieldMgr # フィールド管理.
var _prev_x:float = 0 # 前回の座標(X)
var _prev_y:float = 320 # 前回の座標(Y)
var _cnt_create:int = 0 # 足場を生成した回数.
var _timer:float = 0 # 経過時間.
func _ready() -> void:
# プレイヤーの初期位置を設定しておく.
_prev_x = _player.position.x
func _process(delta: float) -> void:
# カメラをプレイヤーに追従させる.
_camera.position.x = _player.position.x + CAMERA_OFS_X
# 足場の生成チェック.
_check_floor()
# 足場の出現チェック.
func _check_floor() -> void:
# FLOOR_INTERVALの単位になるようにする.
var prev = floor(_prev_x / FLOOR_INTERVAL) * FLOOR_INTERVAL # 前回.
var now = floor(_player.position.x / FLOOR_INTERVAL) * FLOOR_INTERVAL # 現在値.
if prev != now:
# 出現タイミングになった.
_cnt_create += 1 # 生成カウンタをアップする.
if _cnt_create%9 == 8:
# 出現座標(Y)を更新する.
var y = _prev_y + (randi()%8 - 4) * 32
if y < 200:
y = 200 # 200よりも上にならないようにする
if y > 540:
y = 540 # 540よりも下にならないようにする
_prev_y = y
if _cnt_create%9 < 6:
# (0〜5)なら床出現.
_create_floor(now, _prev_y)
# 今回のプレイヤー位置を保存しておく.
_prev_x = _player.position.x
# 足場を生成する.
func _create_floor(px:float, py:float) -> void:
var obj = obj_floor.instance()
obj.position.x = px + SCREEN_W
obj.position.y = py
_field_mgr.add_child(obj)
かなり大幅に書き換えていますが、注目すべきなのは _check_floor() です。
- FLOOR_INTERVALの単位 (64px) に値を丸める
- 前回の値と今回の値に変化があれば足場を生成する
- 足場は 6つ連続で作って、3つ空白を作る
- 9回判定したら、新しいY座標を再計算する
足場のサイズは1つあたり64pxなので、64pxのカメラのスクロール値が 64px の境界を超えたら足場を出現させます。
出現ルールとしては 6つ出現したら、3つぶん空白を開けるようにしています。そして、9つぶん処理したら足場の高さを変えるようにしています。
では実行して動作を確認します。
足場との衝突を色々と確認するとわかるのですが、横からや下から衝突したときに引っかかりが発生しています。
Godotの衝突は標準で全方向からの衝突応答(押し返し)が実装されています。(というよりも一般的にはこの実装です)
ただ今回の足場は見た目的にも上からのみ乗れるようにした方が、自然な感じがします。
このような一方向からのみ衝突がある床を Oneway床 (一方通行床) と呼びます。それでは、Godotで Oneway床を実装する方法について説明します。
といっても実装方法は簡単で、”Floor.tscn” を開いて、インスペクタから「One Way Collision」にチェックを入れるだけです。
ちなみに一方通行床を有効にすると、キャンバス上の Floorオブジェクトにコリジョンの有効な方向が表示されるようになります。
では実行して、一方通行床が有効になっていることを確認します。
動作上、あまり問題に気がしますが、画面外に消えた足場のインスタンスが消えていないのが気になったので、これを消す処理を実装します。
Main.gd を開いて、_precess() を以下のように修正します。
func _process(delta: float) -> void:
# カメラをプレイヤーに追従させる.
_camera.position.x = _player.position.x + CAMERA_OFS_X
# 足場の生成チェック.
_check_floor()
# 画面外に出た足場を消す.
for child in _field_mgr.get_children():
if child.position.x < _player.position.x - 100:
child.queue_free()
実行してプレイヤーの後ろ 100px に移動した床が消えることを確認します。
なお、今回はわかりやすく消えているところを見えるようにしましたが、通常はもう少し後ろにして消えるのを見えないようにした方が良いと思います。
ここまでの実装内容を反映したプロジェクトファイルを添付しておきます。
ゲームオーバー処理を実装する
最後にゲームオーバー処理を実装します。
Mainシーンに Label ノードを2つ追加します。
このように追加しておきます。
ノード名は「LabelSpeed」「LabelGameover」にリネームしておきました。
そしてそれぞれのインスペクタの「Control > Theme > [空]」をクリックして「読み込み」を選びます。
「ファイルを開く」ダイアログが表示されるので、「mplus_theme.tres」を選んで「開く」を押します。
これでフォントが適用されたので、「LabelSpeed」のテキストに「Speed: 300」として左上に配置、「LabelGameover」のテキストに「GAME OVER」と入力して、画面中央に配置します。
ただこのままだと、ラベルのテキストがカメラのスクロールに流されてしまいます。
ラベルテキストの位置を固定する場合は「CanvasLayer」ノードを追加します。
そしてこのように CanvasLayerノードの下に各ラベルノードをぶら下げます。
これでラベルの位置が固定されます。
Main.gd を開いて以下のように編集します。
extends Node2D
# 生成するシーン
var obj_floor = preload("res://Floor.tscn") # 床オブジェクト
# -------------------------------------
# ※ここを追加.
# 状態
enum eState {
MAIN, # メイン.
GAMEOVER, # ゲームオーバー.
}
# -------------------------------------
# カメラの座標関連
const CAMERA_POS_Y = 320 # カメラの位置(Y).
const CAMERA_OFS_X = 400 # プレイヤーからのオフセット
const FLOOR_INTERVAL = 64 # 足場は 64px 単位で登場する.
const SCREEN_W = 1024 # 画面の幅.
onready var _player = $Player # プレイヤー.
onready var _camera = $Camera2D # カメラ.
onready var _field_mgr = $FieldMgr # フィールド管理.
# -------------------------------------
# ※ここを追加.
onready var _labelSpeed = $CanvasLayer/LabelSpeed
onready var _labelGameover = $CanvasLayer/LabelGameover
# -------------------------------------
var _prev_x:float = 0 # 前回の座標(X)
var _prev_y:float = 320 # 前回の座標(Y)
var _cnt_create:int = 0 # 足場を生成した回数.
var _timer:float = 0 # 経過時間.
# -------------------------------------
# ※ここを追加.
var _state = eState.MAIN # 状態.
# -------------------------------------
func _ready() -> void:
……
「※ここを追加」と書かれている部分が追加箇所です。
状態定数(eState)や、ラベルノード(_labelSpeed, _labelGameover)の取得、状態変数(_state)の追加をしています。
続けて、Main.gd 内の _process() の修正と、新たに _update_main() / _update_gameover() を追加します。
func _process(delta: float) -> void:
match _state:
eState.MAIN:
_update_main(delta)
eState.GAMEOVER:
_update_gameover(delta)
# 更新 > メイン.
func _update_main(delta:float) -> void:
if is_instance_valid(_player) == false:
# プレイヤーが消滅したらゲームオーバー.
_state = eState.GAMEOVER
return
if _player.position.y > 640:
# 落下死.
_player.queue_free()
return
# カメラをプレイヤーに追従させる.
_camera.position.x = _player.position.x + CAMERA_OFS_X
# 足場の生成チェック.
_check_floor()
# 現在のスピードを表示.
_labelSpeed.text = "SPEED: %d"%_player._speed
# 画面外に出た足場を消す.
for child in _field_mgr.get_children():
if child.position.x < _player.position.x - 100:
child.queue_free()
# 更新 > ゲームオーバー.
func _update_gameover(delta:float) -> void:
# "GAME OVER" を表示.
_labelGameover.visible = true
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでリスタート.
get_tree().change_scene("res://Main.tscn")
追加した処理としては以下のとおりです。
- プレイヤーのY座標が 640 を超えたら queue_free() で消滅させる
- 現在のプレイヤーのスピードを _labelSpeed で表示する
- プレイヤーのインスタンスが消滅したら eState.GAMEOVER に遷移する
- ゲームオーバー時には “_labelGameover” を表示する
- ゲームオーバー時には Spaceキーでリスタートできる
それと LabelGameover はゲームオーバー時のみ表示したいので、ノードツリー状で非表示にしておきます。
では実行して、落下するとゲームオーバーになる、”GAME OVER” の文字が表示される、Spaceキーでリスタートできることを確認します。
ゲームオーバー時のインパクトが弱いので、画面揺れを入れてみます。
Main.gd を開いて 揺れ時間の定数 (TIMER_SHAKE) を追加します。
extends Node2D
# 生成するシーン
var obj_floor = preload("res://Floor.tscn") # 床オブジェクト
const TIMER_SHAKE = 0.5 # 0.5秒の間揺らす
# 状態
enum eState {
MAIN, # メイン.
GAMEOVER, # ゲームオーバー.
}
それと揺れ時間を計測する変数 (_tShake) を追加します。
var _prev_x: float = 0
var _prev_y: float = 320
var _cnt_create: int = 0
var _timer: float = 0
var _state = eState.MAIN
var _tShake: float = 0 # 画面揺らすタイマー.
そして、_update_main() で eState.GAMEOVER に遷移するタイミングで 揺れ時間 (_tShake) を設定するように修正します。
func _update_main(delta: float) -> void:
if is_instance_valid(_player) == false:
# プレイヤーが消滅したらゲームオーバー.
_tShake = TIMER_SHAKE # 揺れ開始.
_state = eState.GAMEOVER
return
# 落下チェック.
if _player.position.y > 640:
さらに _update_gameover() で画面揺れ処理を入れておきます。
func _update_gameover(delta: float) -> void:
# "GAME OVER" を表示
_label2.visible = true
# 画面揺らす
_camera.offset = Vector2.ZERO
_tShake -= delta
if _tShake > 0:
var rate = _tShake / TIMER_SHAKE
var vx = 64 * rate
var vy = 16 * rate
_camera.offset.x = rand_range(-vx, vx)
_camera.offset.y = rand_range(-vy, vy)
# 画面揺れ中はリスタートできなくする.
return
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでリスタート
get_tree().change_scene("res://Main.tscn")
では実行して動作を確認します。
なお、今回のコードでは、揺れ時間を処理する変数を定義しましたが、Godot 3.x では yield() 、Godot 4 以降では await() で実装する方法もありますので、興味があれば試してみても良いかもしれません。
一通りのゲームが完成したのでプロジェクトファイルを添付しておきます。
おまけ
いったんはここまででラン&ジャンプゲームは完成ですが、ここからはゲームらしさを追加する要素として、以下の機能を実装していきます。
- 1. 時間が経過するほど移動速度が上昇させる
- 2. 障害物を出現させる
- 3. 2段ジャンプをしたときにスロー処理にする
1. 時間が経過するほど移動速度を上昇させる
Player.gd を開いて、速度設定と取得の関数を追加します。(※_speed変数の定義方法に誤りがあったのでそれも修正しています)
var _speed:float = MOVE_SPEED # 移動速度.
# 速度の設定
func set_speed(speed:float) -> void:
_speed = speed
# 速度の取得
func get_speed() -> float:
return _speed
GDScriptの変数は基本的にどこからでもアクセスできるので、わざわざ関数を用意する必要はないのですが、処理をわかりやすくするために追加しました。
次に Main.gd を開いて _update_main() を編集します。
# 更新 > メイン.
func _update_main(delta:float) -> void:
if is_instance_valid(_player) == false:
# プレイヤーが消滅したらゲームオーバー.
_tShake = TIMER_SHAKE # 揺れ開始.
_state = eState.GAMEOVER
return
if _player.position.y > 640:
# 落下死.
_player.queue_free()
return
# カメラをプレイヤーに追従させる.
_camera.position.x = _player.position.x + CAMERA_OFS_X
# 足場の生成チェック.
_check_floor()
# 時間経過に合わせてプレイヤーの速度を変更する
_timer += delta
_player.set_speed(200 + _timer * 30)
# 現在のスピードを表示.
_labelSpeed.text = "SPEED: %d"%_player.get_speed()
# 画面外に出た足場を消す.
for child in _field_mgr.get_children():
if child.position.x < _player.position.x - 100:
child.queue_free()
では実行して動作を確認します。
ゲームの経過時間に合わせて移動速度が変化するようになりました。
ただ、個人的な印象では速度が 600 を超えたあたりから思うように制御ができなくなる上、速度が可変だと着地場所の予測が難しくなるため、ちゃんとゲームらしくするには速度ではなく、地形や障害物の配置をしっかり考えた方が良さそうです。
ということで、ここまでのプロジェクトファイルを添付しておきます。
2. 障害物オブジェクトを生成する
次に障害物オブジェクトを実装してみます。「+」をクリックして新規シーンを追加します。
「その他のノード」を選びます。
障害物であるスパイク(トゲ)は、プレイヤーとの接触判定だけ行えれば十分であるため「Area2D」ノードを選びます。
そしてノード名を「Spike」にリネームしてきます。
続けて「AnimatedSprite」ノードを追加します。
AnimatedSpriteのインスペクタから「AnimatedSprite > Frames > [空]」をクリックして、「新規 SpriteFrames」を選びます。
そして作られた「SpriteFrames」をクリックします。
するとエディタの下部に「アニメーションフレーム」が表示されるので、そこから「スプライトシートからフレームを追加する」アイコンをクリックします。
ファイル選択ダイアログを表示されるので「spike.png」を選んで「開く」をクリックします。
フレームを選択ダイアログが表示されるので、以下のように設定します。
- 水平:4
- 垂直:1
そうしたら「すべてのフレームを選択 (消去)」をクリックするとフレームが分割されるので、「4フレームを追加」をクリックします。
アニメーションフレームが以下のように分割されればOKです。
そうしたら、AnimatedSpriteのインスペクタから「Playing」にチェックを入れてアニメーションを確認します。
スパイクにコリジョンを設定します。「CollisionShape2D」ノードを追加します。
これにより、ノード階層は以下の通りとなります。
CollsionShape2Dのインスペクタから、「Shape > [空]」をクリックして、「新規 CircleShape2D」を選びます。
スパイクを覆うように円のコリジョンを設定します。
次にコリジョンレイヤーを設定します。
メニューから「プロジェクト > プロジェクト設定」を選びます。
プロジェクト設定の「一般 > プロパティ > Layer Names > 2D物理」を選び、以下のように設定します。
- Layer 1: Player
- Layer 2: Floor
- Layer 3: Spike
各オブジェクトのインスペクタの Collision 設定から Layer と Mask を以下のように設定します。
- Player オブジェクト
- Layer: 1 (Player)
- Mask: 1 + 2 + 3 (すべてと衝突する)
- Floor オブジェクト
- Layer: 2 (Floor)
- Mask: 1 (Playerとのみ衝突する)
- Spike オブジェクト
- Layer: 3 (Spike)
- Mask: 1 (Playerとのみ衝突する)
まずは Spikeにスクリプトを追加します。
ただ、何か特別な処理はしないので、以下のように、ほぼ空っぽで問題ないです。
extends Area2D
そして衝突のシグナルを実装するために、インスペクタから「ノード」を選んで「body_entered()」をダブルクリックします。
そのまま「接続」をクリックします。
シグナルメソッド「_on_Spike_body_entered()」を以下のように記述して、衝突したオブジェクト (Player) を削除するようにします。
extends Area2D
func _on_Spike_body_entered(body: Node) -> void:
# 接触したオブジェクトを消す
body.queue_free()
Main.gd を開いて、スパイクの生成処理を実装します。
extends Node2D
# 生成するシーン
var obj_floor = preload("res://Floor.tscn") # 床オブジェクト
var obj_spike = preload("res://Spike.tscn") # スパイクオブジェクト
const TIMER_SHAKE = 0.5 # 0.5秒の間揺らす
……
preload() でスパイクオブジェクトを読み込んでおきます。
そして _check_floor() を以下のように修正します。
# 足場の出現チェック.
func _check_floor() -> void:
# FLOOR_INTERVALの単位になるようにする.
var prev = floor(_prev_x / FLOOR_INTERVAL) * FLOOR_INTERVAL # 前回.
var now = floor(_player.position.x / FLOOR_INTERVAL) * FLOOR_INTERVAL # 現在値.
if prev != now:
# 出現タイミングになった.
_cnt_create += 1 # 生成カウンタをアップする.
if _cnt_create%9 == 8:
# 出現座標(Y)を更新する.
var y = _prev_y + (randi()%8 - 4) * 32
if y < 200:
y = 200 # 200よりも上にならないようにする
if y > 540:
y = 540 # 540よりも下にならないようにする
_prev_y = y
if _cnt_create%9 < 6:
# (0〜5)なら床出現.
_create_floor(now, _prev_y)
# -----------------------------------
# ※ここを追加
if _cnt_create%19 == 0:
# スパイク出現.
_create_spike(now, _prev_y)
# -----------------------------------
# 今回のプレイヤー位置を保存しておく.
_prev_x = _player.position.x
「※ここを追加」が今回追加した部分です。床の生成数が 19の倍数の場合に生成するようにしています。
次に「_create_spike()」を追加します。
# スパイク生成.
func _create_spike(px: float, py: float) -> void:
var obj = obj_spike.instance()
obj.position.x = px + SCREEN_W
obj.position.y = py - 64 - 32 * (randi()%4) # ランダムで床の上に生成する
_field_mgr.add_child(obj)
床の生成関数 _create_floor() と似た処理になっていますが、Y座標を少しずらして床の上のランダムな位置に出現するところが異なります。
では実行して動作を確認します。
Spikeオブジェクトが自動生成され、接触するとゲームオーバーとなります。
スパイクを実装したプロジェクトファイルを添付しておきます。
3. 2段ジャンプ時にスローにする
正直なところ、この処理は微妙なような(あってもなくてもゲームプレイの印象が変わらない)気がしましたが、ひょっとしたら需要があるかもしれない…ので記載しておきます。
仕組みとしては、Godotはデフォルトで 60FPS で動作しているのですが、一時的に 30FPS に落とすことでスローを実現しています。
Player.gd の _physics_process() を以下のように修正します。
func _physics_process(delta: float) -> void:
# 移動速度を反映.
_velocity.x = _speed
# 重力を反映.
_velocity.y += GRAVITY
# デバッグ用 (画面外に出ないようにする)
if position.y > 680:
_velocity.y = -1500
# -------------------------
# ※ここを追加.
Engine.set_time_scale(1)
if _cnt_jump == MAX_JUMP_CNT and _velocity.y < 0:
# 2段ジャンプの上昇中はスローになる.
Engine.set_time_scale(0.5)
# -------------------------
# ※ここの第2引数を追加
_velocity = move_and_slide(_velocity, Vector2.UP)
# ジャンプ中チェック.
if is_on_floor():
# 着地した
_is_jumping = false
_velocity.y = 0 # 重力クリア.
_cnt_jump = 0 # ジャンプ回数をリセットする.
else:
# ジャンプ中.
_is_jumping = true
if _cnt_jump < MAX_JUMP_CNT:
if Input.is_action_just_pressed("ui_accept"):
# ジャンプ.
_velocity.y = -JUMP_POWER
_cnt_jump += 1
「※ここから追加」の部分が追加したところです。
Engine.set_time_scale() の初期値は「1」なので、「0.5」にすることでスローとなります。
スロー処理を実装したプロジェクトファイルを添付しておきます。
Godot 4対応版プロジェクト
最後のスロー処理を実装済みのプロジェクトのみですが、Godot 4.x に対応したプロジェクトを添付しておきます。