ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
Godot_v4.0-rc1_win64.exe.zipを使用しています。
目的
何か発射しましょう。
内容
1. 新たにシーンを作成して、弾丸を作成します。
2. 弾丸シーンをGDScriptから生成・追加しましょう
3. タイマーシグナルを利用して一定時間置きに発射しましょう。
ベースプロジェクト
下記で作成したプロジェクトをベースに機能追加をします。
【Godot 4.0】スマホ3Dゲームを作るための勉強 その7 3D空間のタッチしたところに移動したい
https://qiita.com/FootInGlow/items/564dfe87dc7fc5135c67
新規シーンを作成する
シーンメニューから新規シーンを選択します。
その他のノードボタンを押下して、CharacterBody3Dノードを追加します。
下記のようにしましょう。
CharacterBody3Dノードは「Bullet」に名前を変更します。
一度Ctrl-Sキーなどでシーンを保存しましょう。
保存するとシーンファイルが追加されます。
シーンファイルは拡張子が.tscnで、前回作成したシーンはglobal.tscn、今回作成したシーンはbullet.tscnです。ファイル名を変更している場合は適宜読み替えてください。
-
MeshInstance3Dにカプセル型を設定します。
インスペクタ内のMeshInstance3D/Meshを新規CapsuleMeshに設定します。
少し小さくしましょう。Radiusを0.1m、Heightを0.5mにします。
-
CollisionShape3Dに衝突形状を設定
MeshInstance3Dと同じ形・大きさにします。
インスペクタ内のCollisionShape3D/Shapeに新規CapsuleShape3Dを設定します。
Radiusを0.1m、Heightを0.5mにします。
## スクリプトを設定する
bulletを右クリックして「スクリプトをアタッチ」を選択します。
前方に進むスクリプトです。
extends CharacterBody3D
@export var m_v_dir = Vector3(0.0, 0.0, -1.0)
@export var m_d_speed_mps = 2.0
func _physics_process(delta):
velocity = m_v_dir.normalized() * m_d_speed_mps
move_and_slide()
一次的にシーンの動きを確認する
globalシーンにBulletシーンを一時的に追加して動きを確認します。
ファイルシステムにあるBullet.tscnを、
シーン内のglobalにドラッグアンドドロップします。
global直下にBulletが追加されました。
他のノードとぶつからないように、y=1.5mにします。
実行すると、球の前方にBulletで追加したノードが進むようになります。
確認したらglobalに追加したBulletは削除してください。
Playerノードから発射するようにする
スクリーンにタッチしている間、1秒周期で繰り返しPlayerからBulletを発射するようにします。
先ほどはIDE上のドラッグアンドドロップでBulletシーンをglobalシーンに追加しましたが、同じことをGDScriptから実行します。
Timerノードを追加して周期的にsignalを受信する
Godotにはノード間で通信をするためのsignal通信の仕組みがあります。
Timerノードは指定した時間経過すると、timeoutシグナルを発行することができ、同じシーン内の任意のスクリプトで受信することができます。タイマーは自動で繰り返されます。1回だけタイマーを起動することも簡単にできるので、時間でイベントが欲しいときにはとても便利です。
Playerノードを右クリックして「子ノードを追加」からTimerを追加します。
また名称をTimer_fireにします。
Timer_fireをクリック選択した状態でインスペクタを表示します。
Autostartをオンにします。実行すると自動的にタイマーが開始します。
続いてインスペクタの隣の「ノード」タブをクリックします。
シグナルを選択(選択すると青になります)すると下記のように、Timerノードが発行するsignalが表示されます。
timeoutしたときにPlayerノードに通知してほしいので、timeoutを右クリックして「接続」します。
同じシーンの中のスクリプトを持つノードを選択できます。受信側メソッド名を確認して、Playerを選択して接続ボタンを押下します。
Playerのスクリプトを見ると、先ほどのダイアログに表示されていた受信側メソッドが追加されます。
これでTimerノードのtimeoutシグナルがPlayerのスクリプトに接続されました。接続されていることはIDEの3か所で確認することができます。
まずシーンのsignalを追加したTimer_fireノードの右に電波のようなアイコンが表示されます。
この電波のようなアイコンをクリックすると、
ノード/シグナル表示がアクティブになります。
先ほど接続したtimeoutを見ると下に「..::_on_timer_fire_timeout()関数と接続したことが表示されています。
「..」が1個上の階層を意味するので、シーン内でTimerの親ノードのスクリプトの_on_timer_fire_timeout()関数を表しています。
この関数をダブルクリックすると接続したPlayerのスクリプトの該当の関数が表示されます。
関数の左側にある「➡コ」みたいなアイコンをクリックすると、送信元であるソースノード、送信先のターゲットノードとその間を接続するシグナル名を表示します。
PlayerのGDScriptの修正してtimeout時にBulletシーンを追加する
タッチしている間、1秒周期で発射するように Player.gdスクリプトを下記のように修正します。
またドラッグアンドドロップでPlayerのインスペクタにBullet.tscnを登録する必要がありますので後で説明します。
extends CharacterBody3D
@export var m_speed_mps = 5.0
@export var m_ray_length_m = 1000 # [m]十分な長さにする
@export var m_scn_bullet : PackedScene
var m_touch_pos = Vector2.ZERO
var m_d_move_to_pos_x_m = 0
var m_f_is_screen_touch = false
func _input(event):
if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
m_touch_pos = event.position
# スマホタッチ中フラグ
m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
func _physics_process(delta):
var camera = get_viewport().get_camera_3d()
# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
var from3d = camera.project_ray_origin(m_touch_pos)
var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m
# 3D ray physics queryの作成
var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
query.collide_with_areas = true # Area3Dを検知できるようにする
# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
if result:
m_d_move_to_pos_x_m = result.position.x
# 移動制御は毎周期実行する
var d_diff_x_m = (m_d_move_to_pos_x_m - transform.origin.x);
var d_speed_mps = m_speed_mps if d_diff_x_m>0 else -m_speed_mps
var d_move_diff_x_m = d_speed_mps * delta
if absf(d_move_diff_x_m) > absf(d_diff_x_m):
# 移動すると目標位置を超えるため、目標位置を設定する
transform.origin.x = m_d_move_to_pos_x_m
else:
move_and_collide(Vector3(d_move_diff_x_m, 0, 0))
func _on_timer_fire_timeout():
# スマホタッチ中の場合、発射する
if m_f_is_screen_touch:
var scn_bullet = m_scn_bullet.instantiate()
scn_bullet.transform.origin = transform.origin
add_sibling(scn_bullet)
Bulletシーンファイルをスクリプトのメンバ変数に設定する
Godot3.5の時はこのように宣言していました。4.0で変更された点のひとつです。
export(PackedScene) var m_scn_bullet
シーン内のPlayerをクリック選択してからインスペクタを見ると、M Scn Bulletという名前で表示されています。まだ何も登録していないので、<empty>です。
ファイルシステムのbullet.tscnを<empty>にドラッグアンドドロップして登録しましょう。
登録したシーンのイメージが表示されます。
スマホタッチ中フラグの追加
_inputを修正してスマホタッチ中のフラグを設定するようにしました。
-
var m_f_is_screen_touch = false
スマホタッチ状態を保持できるようにメンバ変数を追加します。 -
m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
_input()の中で、eventがInputEventScreenDrag且つevent.is_pressed()がtrueの時、もしくはeventがInputEventScreenDragのときに、タッチ中と判断しています。
timeoutシグナル受信したときに発射処理の実装
timeoutをしたときに、スマホタッチしている場合は、Bulletを発射する処理を実装しました。
-
func _on_timer_fire_timeout():
Timer_fireのtimeoutシグナルを接続したときに追加したメソッドです。 -
if m_f_is_screen_touch:
スマホタッチ中のみ以下の処理を実行 -
var scn_bullet = m_scn_bullet.instantiate()
新規にBulletシーンのインスタンスを生成します。
m_scn_bulletには先ほどドラッグアンドドロップでbullet.tscnをセットされています。 -
scn_bullet.transform.origin = transform.origin
Playerと同じ位置を設定します。 -
add_sibling(scn_bullet)
Playerと同じ階層にBulletシーンインスタンスを追加します。
シーンを追加する場合、add_childとadd_siblingがあります。
add_childではPlayerの子ノードとして追加されます。その場合Playerが移動するとBulletもついてきて期待する動作とは異なる結果になります。
add_siblingとするとPlayerと同じ階層に追加されます。
実行しましょう。
シーンのリモートを確認すると、add_sibling()で追加したBulletはPlayerと同じ階層に追加されていることがわかります。
問題が2つあります。ひとつはBulletが消えないことです。
2つ目の問題は、BulletやPlayerの球がおかしな方向(Z軸方向)に動くことです。これは衝突判定をしているPlayerの中にBulletを生成したので、衝突による速度計算がおかしくなっているためだと思います。
画面外に出た場合Bulletを消すようにする
Bulletが画面外にでたところで消すようにしましょう。
bullet.tscnを開いて、Bulletを右クリックして「子ノードを追加」からVisibleOnScreenNotifier3Dを追加します。
現在のカメラから見える範囲に入った場合、出た場合にsignalを発行することができます。
ノード/シグナルを表示して、screen_exitedを右クリックして、Bulletのスクリプトに接続します。
受信側メソッドを確認して接続ボタンを押下します。
_on_visible_on_screen_notifier_3d_screen_exitedメソッドが追加されました。
下記のように修正しましょう。
extends CharacterBody3D
@export var m_v_dir = Vector3(0.0, 0.0, -1.0)
@export var m_d_speed_mps = 2.0
func _physics_process(delta):
velocity = m_v_dir.normalized() * m_d_speed_mps
move_and_slide()
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
- queue_free()
自身のノードインスタンスを開放(削除)します。
実行しましょう。
リモートで確認するとBulletが画面から消えると、シーンからも消えるのが確認できます
Bulletの発射位置を指定する
Collision Layer/Maskを設定して衝突しないようにすればよいのですが、登場位置を正確に設定する必要もあるので、今回はBulletの発射位置を指定して対処します。(Collision Layer/Maskは次回制御します)
global.tscnを開いて、Playerを右クリックして「子ノードを追加」からMarker3Dを追加します。
Node3D/Transform/Zに-0.7mを設定します。
球の半径が0.5m、Bulletの半径が0.1mなのでその合計より前方にしました。
Marker3Dはこのように球の前方に表示されます。
Godot3.5のとき、Marker3DはPosition3Dという名称でした。
Marker3Dの位置を利用してBulletの発射位置を設定します。
extends CharacterBody3D
@export var m_speed_mps = 5.0
@export var m_ray_length_m = 1000 # [m]十分な長さにする
@export var m_scn_bullet : PackedScene
var m_touch_pos = Vector2.ZERO
var m_d_move_to_pos_x_m = 0
var m_f_is_screen_touch = false
func _input(event):
if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
m_touch_pos = event.position
# スマホタッチ中フラグ
m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
func _physics_process(delta):
var camera = get_viewport().get_camera_3d()
# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
var from3d = camera.project_ray_origin(m_touch_pos)
var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m
# 3D ray physics queryの作成
var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
query.collide_with_areas = true # Area3Dを検知できるようにする
# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
if result:
m_d_move_to_pos_x_m = result.position.x
# 移動制御は毎周期実行する
var d_diff_x_m = (m_d_move_to_pos_x_m - transform.origin.x);
var d_speed_mps = m_speed_mps if d_diff_x_m>0 else -m_speed_mps
var d_move_diff_x_m = d_speed_mps * delta
if absf(d_move_diff_x_m) > absf(d_diff_x_m):
# 移動すると目標位置を超えるため、目標位置を設定する
transform.origin.x = m_d_move_to_pos_x_m
else:
move_and_collide(Vector3(d_move_diff_x_m, 0, 0))
func _on_timer_fire_timeout():
# スマホタッチ中の場合、発射する
if m_f_is_screen_touch:
var scn_bullet = m_scn_bullet.instantiate()
scn_bullet.transform.origin = $Marker3D.global_transform.origin
add_sibling(scn_bullet)
func _on_timer_fire_timeout():の位置設定処理を変更します
- scn_bullet.transform.origin = $Marker3D.global_transform.origin
Marker3Dのglobal座標をBulletに設定します。
子ノードは「\$+ノード名」で表すので、\$Marker3Dです。シーンからスクリプトエディター画面にドラッグアンドドロップすると相対パスを考慮してスクリプトに挿入されて便利です。
またMarker3DはPlayerの子ノードですの、\$Marker3D.transform.originはPlayrのローカル座標系です。
BulletはPlayerと同じ世界座標系で動作するためtransformではなくglobal_transformを使用しています。
実行しましょう。
球とBulletが押し合うことはなくなります。
以上