Godot4.1.3で縦スクロールシューティングゲームをつくります。格好よくしたいので見た目は3Dで作っていきますが、動きは2Dです。
本記事の目標は、Player機をタッチ&ドラッグで操作するまで
本記事では、Godot4で新規プロジェクトを作成して、スマホ用縦スクロールシューティングゲーム用のGameシーンを作成します。また、Player機を表示して、スマホ画面上をドラッグすると、追従してPlayer機が移動するようにGDScriptを実装します。
github
本記事で実装したものを、githubに登録しました。
スマホゲーム用新規プロジェクト
Godot4.1.3で新規プロジェクトを作成します。
スマホ縦持ちで遊べるようにしたいので、プロジェクト設定の「ウインドウ」を設定を下記のように変更します。
項目 | 設定値 |
---|---|
サイズ/ビューポート | 幅:500、高さ:900 |
ストレッチ/モード | canvas_items※ |
ポータブル/方向 | Portraitにして、縦方向の表示に固定する |
※canvas_itemsにすると、スコアなど描画したときに、表示デバイスの解像度に影響を受けずに、大きさの比率を自動調整していい具合に描画してくれます。
下記はAndroidアプリをエクスポートする時に必要になる設定です。右上のAdvanced Settingsを有効にすると表示される項目で、レンダリング/テクスチャのImport ETC2 ASTCにチェックを入れる必要があります。
今回はスマホ用のゲームを開発するため、タッチにより操作をしますが、デバッグはWindowsPC上で実行してマウスで操作します。その場合「マウスでタッチ操作をエミュレート」を有効にすると、Godotがマウス操作をタッチ操作に変換するため、タッチイベント処理のみ実装すればタッチ操作もマウス操作も両方処理することができるようになります。
フォルダ構成
フォルダ構成を決めます。
フォルダ名 | 内容 |
---|---|
assets | 3Dモデルや画像などのリソース |
Character | Playerや敵などのキャラクターシーン、スクリプト |
Game | ゲームシステムのシーン、スクリプト |
前回アプリを作ったときに、ファイルが増えたらフォルダ分けをすれば良いと思っていたのですが、参照されているシーンを別のフォルダに移動すると、Godotが追従できず参照元のファイルが開けなくなることがありました。最初にきちんと考えてフォルダ構成を決めた方が良いと思います。もしフォルダ構成を変更する場合は、バックアップを取って少しずつ確認しながら実施することをお勧めします。
3Dモデル
BlenderでPlayer機をモデリングしました。個人的にとても気に入っています。glTF2.0にエクスポートしたplayer.glbをassetsフォルダに格納しました。
Playerシーンを作成し、大きさを5mに決める。
Playerシーンを作成し、character/playerフォルダに格納しました。
ルートノードはとりあえずCharacterBody3Dにしました。Assetsに格納したplayer.glbをドラッグして子ノードにしました。
Playerのサイズは自動車と同じくらいと想定して5mくらいになるようにScaleのx,y,zを1.7に設定しました。今後、敵など他のキャラクターのサイズを決めるときは、Playerのサイズを基準にすると考えやすいと思います。2倍にしたいときは10mという感じです。
Player機の衝突判定用にCollisionShape3Dを追加しました。CollisionShape3Dに設定するShapeの形状とサイズは機体デザインよりも小さくします。ShapeにはCapsuleShape3Dを設定し、Height4.5m、Radius0.4mにして、X軸周りに90°傾けました。
Gameシーンを作成
Gameシーンを作成して、Gameフォルダに配置しました。
ルートノードはNode3Dで、Camera3DとDirectionLight3DとPlayerシーンを追加しました。
Area3DのTouchDetectorArea3Dは、画面をタッチしたときに、Godotが管理している物理空間状態を利用して、3D空間上の位置を計算するために配置したものです。後で説明します。
ノード種別 | ディフォルト値からの変更 |
---|---|
Camera3D | position y=70m, Rotation x=-60° |
DirectionalLight3D | Rotation x=-60° |
Player | 初期値のまま |
Area3D (TouchDetectorArea3D) | 初期値のまま |
CollisionShape3D | BoxShape3D Size (x,y,z)=(1000m, 1m, 1000m), position y=-0.5m |
Playerの移動方法
スマホの画面のタッチ操作でPlayer機を動かします。タッチした場所に移動する方法もありますが、Player機が指で隠れて見えなくなると、細かい操作がしにくいので、スマホ画面上のどこでもよいので、ドラッグした分だけ移動するようにしようと思います。
あと指の動きに完全に追従すると、ワープみたいになってしまうので、移動目標位置を指に追従するようにして、そこに向かって設定した速度で移動するようにします。
ドラッグ操作を検知して、Player機を移動するScriptの説明
playerにスクリプトをアタッチして、res://character/player/player.gdを作成しました。
全体を見せた後、詳細について説明します。
Scriptの全体
下記のように修正しました。
extends CharacterBody3D
# Player
@export var _d_speed_mps = 100.0
# ドラッグ操作検知用
var _f_is_screen_touch = false # 画面にタッチ(ドラッグ含む)している場合true、タッチしていない場合はfalse
var _v2_drag_pos = Vector2.ZERO # 最新のタッチ位置・ドラッグ位置
# タッチしたときの、3D上のタッチ位置とPlayer位置
var _v_first_touch_pos = null
var _v_first_touch_player_pos = Vector3.ZERO
# 画面をタッチした位置から、検索する3Dオブジェクトまでの最大距離
@export var _d_ray_length_m = 1000 # [m]十分な長さにする
func _input(event):
# 画面にタッチしている状態と、タッチしていない状態を判断する
if event is InputEventScreenTouch:
_f_is_screen_touch = event.is_pressed()
elif event is InputEventScreenDrag:
# ドラッグしているということは、タッチしている。(参考:InputEventScreenDragの場合、event.is_pressed()は常にfalse)
_f_is_screen_touch = true
else:
pass
# ドラッグ位置(タッチ位置)を更新する
_v2_drag_pos = event.position
func _physics_process(delta):
if _f_is_screen_touch:
var camera = get_viewport().get_camera_3d()
# カメラを利用して3D空間のカメラの3D位置と、カメラからタッチしたピクセルを見た方向の1000m先の3D位置を計算する
var from3d = camera.project_ray_origin(_v2_drag_pos)
var to3d = from3d + camera.project_ray_normal(_v2_drag_pos) * _d_ray_length_m
# 3D ray physics queryの作成
var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
query.collide_with_areas = true # Area3Dを検知できるようにする
# spaceと呼ばれる、物理3D空間状態の情報を利用して、Area3Dを含む衝突位置を計算する
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
if result:
# 衝突位置を取得
var v_drag_pos = result.position
v_drag_pos.y = global_position.y # Y方向には移動しないための設定
if _v_first_touch_pos == null :
# 初めてタッチしたときの、3D位置を記憶する
_v_first_touch_pos = v_drag_pos
# 初めてタッチしたときの、プレイヤーの3D位置を記憶する
_v_first_touch_player_pos = global_position
else:
pass
# 3D空間の移動目標位置
var v_target_pos = _v_first_touch_player_pos + ( v_drag_pos - _v_first_touch_pos )
# Player位置から移動目標位置までの相対位置を計算
var v_target : Vector3 = v_target_pos - global_position
# 移動目標位置に向かう速度ベクトルを生成
var v_velocity : Vector3 = v_target.normalized() * _d_speed_mps
if v_target.length() < ( v_velocity * delta ).length():
# 目的地までの距離が、delta当たりの移動量よりも小さい場合は、飛び越えてしまうため、Playerの位置を目標位置にする
global_position = v_target_pos
else:
# CharacterBody3Dのvelocityに速度を設定して動かす
velocity = v_velocity
move_and_slide()
else:
# タッチ位置に衝突したものが無い
pass
else:
# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
_v_first_touch_pos = null
func _input(event)でスマホ画面のタッチ状態を判定し、タッチ位置を記憶する
「_input」メソッドを実装すると、UI操作イベントを処理できるようになります。
下記の処理で、画面にタッチしている場合は、メンバ変数_f_is_screen_touch=trueに設定し、タッチしていない場合は_f_is_screen_touch=falseに設定します。
# 画面にタッチしている状態と、タッチしていない状態を判断する
if event is InputEventScreenTouch:
_f_is_screen_touch = event.is_pressed()
elif event is InputEventScreenDrag:
# ドラッグしているということは、タッチしている。(参考:InputEventScreenDragの場合、event.is_pressed()は常にfalse)
_f_is_screen_touch = true
else:
pass
スマホのタッチ状態が変わると、event=InputEventScreenTouchが通知されます。その場合event.is_pressed()でタッチ状態を判定することができます。
ドラッグ状態に変化があるとevent=InputEventScreenDragが通知されます。event.is_pressed()は常にfalseになるため使えませんが、ドラッグしているということは画面をタッチしているのは明白なので、_f_is_screen_touchをtrueに設定します。
続いて、画面を(最後に)タッチした位置をメンバ変数_v2_drag_posに記憶します。
# ドラッグ位置(タッチ位置)を更新する
_v2_drag_pos = event.position
func _physics_process(delta)でタッチした画面位置から3D空間上の位置を算出する
スマホ画面上をタッチした位置は、_inputメソッドで、メンバ変数_v2_drag_posに記憶しています。
cameraの情報を利用して、_v2_drag_posを3D空間上のカメラのレンズ上の位置に変換したものをfrom3dに設定し、見ている方向の1000m先(_d_ray_length_mの値)の位置をto3dに設定します。
if _f_is_screen_touch:
var camera = get_viewport().get_camera_3d()
# カメラを利用して3D空間のカメラの3D位置と、カメラからタッチしたピクセルを見た方向の1000m先の3D位置を計算する
var from3d = camera.project_ray_origin(_v2_drag_pos)
var to3d = from3d + camera.project_ray_normal(_v2_drag_pos) * _d_ray_length_m
from3dからto3dに直線を引いたときに、一番最初に衝突する物体の衝突位置を、Godotが管理している物理空間状態(world's physics 3D space state)から検索します。まずfrom3dとto3dから問い合わせ用の情報、queryを作成します。Area3Dとの衝突も検出してほしいので、queryのcollide_with_areasをtrueにします。
_physics_processメソッド内であれば、物理空間状態space_stateをget_world_3d().direct_space_stateで取得でき、result = space_state.intersect_ray(query)とメソッドを実行すると、result.positionに衝突位置が格納されます。
この時タッチした位置に物体がないと、衝突位置が検出できないため、タッチしたときに必ず衝突位置を検出できるように、GameシーンにはArea3dのTouchDetectorArea3Dノードを追加しています。
# 3D ray physics queryの作成
var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
query.collide_with_areas = true # Area3Dを検知できるようにする
# spaceと呼ばれる、物理3D空間状態の情報を利用して、Area3Dを含む衝突位置を計算する
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
画面タッチ位置から3D空間上の位置を検出する方法は、こちらの記事も参考にしていただければ幸いです。
3D空間上のタッチ位置(ドラッグ位置)をv_drag_posに保存する
今タッチ(ドラッグ)している位置に対応する3D空間上の位置、result.positionをv_drag_posに代入します。この後、移動量を計算する時に、Y軸の差分は不要なので(Y軸の差分が出ると困るので)、yの位置を固定します。
if result:
# 衝突位置を取得
var v_drag_pos = result.position
v_drag_pos.y = global_position.y # Y方向には移動しないための設定
func _physics_process(delta)でドラッグ開始を検知して、ドラッグ開始位置とPlayer位置を記憶する
_physics_process()メソッドの最初のif文では、_f_is_screen_touchがfalseの場合、elseの処理で_v_first_touch_pos=nullに設定し、スマホ画面にタッチしていないことを表しています。
つまり、_f_is_screen_touchがtrueで、(resultも有効で)かつ、_v_first_touch_posがnullの時は初めてタッチした時=ドラッグ開始を意味しますので、下記の処理で、ドラッグ開始位置を_v_first_touch_posに記憶します。またドラッグ開始時のPlayer機の位置も_v_first_touch_player_posに記憶します。
func _physics_process(delta):
if _f_is_screen_touch:
:
if result:
# 衝突位置を取得
var v_drag_pos = result.position
v_drag_pos.y = global_position.y # Y方向には移動しないための設定
if _v_first_touch_pos == null :
# 初めてタッチしたときの、3D位置を記憶する
_v_first_touch_pos = v_drag_pos
# 初めてタッチしたときの、プレイヤーの3D位置を記憶する
_v_first_touch_player_pos = global_position
else:
pass
:
else:
# タッチしていない場合はnullを設定することで、次にタッチしたときに、新しいタッチ位置とPlayer位置を記憶する
_v_first_touch_pos = null
ドラッグ開始位置からドラッグ位置までの移動量を求めて、Player機の移動目標位置を計算する
ドラッグによる移動量は、現在のタッチ位置から、ドラッグ開始位置を引いたものになります。
ドラッグ開始時のPlayer位置に、ドラッグによる移動量を足した位置が、Playerの移動目標位置になります。
# 3D空間の移動目標位置
var v_target_pos = _v_first_touch_player_pos + ( v_drag_pos - _v_first_touch_pos )
Player機の速度を移動目標位置に向けて設定する
Playerの移動目標位置v_target_posから現在のPlayerの位置を引き算したものが、Player機が移動するべき方向と移動量、v_targetになります。
v_target.normalized()により方向のみを表す成分(単位ベクトル)になり、_d_speed_mpsを掛け算すると、速度v_velocity になります。
あとはCharacterBody3Dの場合は、velocityに速度を設定して、move_and_slide()を実行すると移動します。
ただし目標位置までの距離が極小の場合、1回の移動で目標位置を飛び越えます。次の周期でも目標位置を飛び越えて元の位置の戻るため、見た目はPlayer機がぶるぶる震えるようになります。
この問題を対処するため、この周期で動く移動距離を(速度×delta).length()で求めて、目標位置までの移動距離v_target.length()より大きい場合は、Player位置を移動目標位置に設定します。
# Player位置から移動目標位置までの相対位置を計算
var v_target : Vector3 = v_target_pos - global_position
# 移動目標位置に向かう速度ベクトルを生成
var v_velocity : Vector3 = v_target.normalized() * _d_speed_mps
if v_target.length() < ( v_velocity * delta ).length():
# 目的地までの距離が、delta当たりの移動量よりも小さい場合は、飛び越えてしまうため、Playerの位置を目標位置にする
global_position = v_target_pos
else:
# CharacterBody3Dのvelocityに速度を設定して動かす
velocity = v_velocity
move_and_slide()
まとめ
以上で、ドラッグ操作によりPlayer機を移動できるようになりました。Player機が画面の外に出てしまう問題などありますが、今後対処したいと思います。
以上です。