VRスターターチュートリアルパート2¶
はじめに¶

VRスターターチュートリアルシリーズのこのパートでは、VRで使用できる特別な RigidBody ベースのノードをいくつか追加します。
これは、チュートリアルパート1の最後におこなったところから続きます。そこでは、VRコントローラーを動作させ、VR_Interactable_Rigidbody
というカスタムクラスを定義しました。
ちなみに
You can find the finished project on the OpenVR GitHub repository.
破壊可能なターゲットを追加する¶
特別な RigidBody ベースのノードを作成する前に、それらを実行するために色々と必要です。破壊されたときに多数のピースに分割される単純な球体ターゲットを作成しましょう。
Scenes
フォルダにある Sphere_Target.tscn
を開きます。 シーンはかなりシンプルで、球形の CollisionShape を備えた StaticBody、球メッシュを表示する MeshInstance ノード、および AudioStreamPlayer3D ノードのみです。
特別な RigidBody ノードは球体のダメージを処理します。そのため、Area または RigidBody ノードのようなものの代わりに StaticBody ノードを使用しています。 それ以外のことについては、あまり話すことはありませんので、コードの記述にまっすぐ進みましょう。
Sphere_Target_Root
ノードを選択し、Sphere_Target.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends Spatial
var destroyed = false
var destroyed_timer = 0
const DESTROY_WAIT_TIME = 80
var health = 80
const RIGID_BODY_TARGET = preload("res://Assets/RigidBody_Sphere.scn")
func _ready():
set_physics_process(false)
func _physics_process(delta):
destroyed_timer += delta
if destroyed_timer >= DESTROY_WAIT_TIME:
queue_free()
func damage(damage):
if destroyed == true:
return
health -= damage
if health <= 0:
get_node("CollisionShape").disabled = true
get_node("Shpere_Target").visible = false
var clone = RIGID_BODY_TARGET.instance()
add_child(clone)
clone.global_transform = global_transform
destroyed = true
set_physics_process(true)
get_node("AudioStreamPlayer").play()
get_tree().root.get_node("Game").remove_sphere()
このスクリプトの仕組みを見ていきましょう。
Sphere Targetコードの説明¶
First, let's go through all the class variables in the script:
destroyed
: 球体ターゲットが破壊されたかどうかを追跡する変数。destroyed_timer
: 球体ターゲットが破壊された時間を追跡する変数。DESTROY_WAIT_TIME
: ターゲットがそれ自体を解放/削除する前に破棄できる時間の長さを定義する定数。health
: 球体ターゲットが持つ体力(HP)の量を保存する変数。RIGID_BODY_TARGET
: 破壊された球体ターゲットのシーンを保持する定数。
注釈
RIGID_BODY_TARGET
シーンをチェックしてください。それは RigidBody ノードの束と壊れた球体モデルです。
このシーンをインスタンス化するので、ターゲットが破壊されると、多数のピースに壊れたように見えます。
_ready
関数のステップごとの説明¶
All the _ready
function does is that it stops the _physics_process
from being called by calling set_physics_process
and passing false
.
The reason we do this is because all the code in _physics_process
is for destroying this node when enough time has passed, which we only want to
do when the target has been destroyed.
_physics_process
関数のステップごとの説明¶
最初に、この関数は delta
を destroyed_timer
変数に加算します。次に、destroyed_timer
が DESTROY_WAIT_TIME
以上であるかどうかを確認します。destroyed_timer
が DESTROY_WAIT_TIME
以上の場合、球体ターゲットは queue_free
関数を呼び出すことで自身を解放/削除します。
damage
関数の詳細な説明¶
damage
関数は、特別な RigidBody ノードによって呼び出され、ターゲットに加えられたダメージの量を damage
と呼ばれる関数の引数変数として渡します。damage
変数は、特別な RigidBody ノードが球体ターゲットに与えたダメージの量を保持します。
最初に、この関数は destroyed
変数が true
に等しいかどうかをチェックすることにより、ターゲットが既に破棄されていないことを確認します。destroyed
が true
に等しい場合、関数は return
を呼び出すため、他のコードは呼び出されません。これは単なる安全チェックであるため、2つのものがまったく同時にターゲットにダメージを与えた場合でも、ターゲットを2回破壊することはできません。
次に、この関数は、ダメージの量 damage
をターゲットの体力 health
から減らします。次に、health
がゼロ以下であるかどうか、つまりターゲットが破壊されたことを確認します。
ターゲットが破壊されたばかりの場合は、disabled
プロパティを true
に設定して CollisionShape を無効にします。次に、visible
プロパティを false
に設定して、Sphere_Target
MeshInstance を非表示にします。これにより、ターゲットが物理世界に影響を与えなくなり、破損していないターゲットメッシュは表示されなくなります。
この後、関数は RIGID_BODY_TARGET
シーンをインスタンス化し、それをターゲットの子として追加します。次に、clone
と呼ばれる新しくインスタンス化されたシーンの global_transform
を、破損していないターゲットの global_transform
に設定します。これにより、破損したターゲットは、破損していないターゲットと同じ位置から、同じ回転とスケールで開始します。
次に、関数は destroyed
変数を true
に設定するため、ターゲットはそれが破棄されたことを認識し、set_physics_process
関数を呼び出して true
を渡します。これは _physics_process
のコードの実行を開始し、DESTROY_WAIT_TIME
秒が経過した後、球体ターゲットはそれ自身を解放/破棄します。
次に、関数は AudioStreamPlayer3D ノードを取得し、play
関数を呼び出してサウンドを再生します。
最後に、remove_sphere
関数は Game.gd
で呼び出されます。Game.gd
を取得するために、コードはシーンツリーを使用し、シーンツリーのルートから Game.tscn
シーンのルートに移動します。
remove_sphere
関数を Game.gd
に追加する¶
まだ定義していない remove_sphere
と呼ばれる Game.gd
の関数を呼び出していることに気づいたかもしれません。Game.gd
を開き、次の追加のクラス変数を追加します:
var spheres_left = 10
var sphere_ui = null
spheres_left
: 世界に残っている球体ターゲットの量。提供されたGame
シーンには、10
の球体があるため、これが初期値です。sphere_ui
: 球体UIへの参照。チュートリアルの後半でこれを使用して、世界に残っている球の量を表示します。
これらの変数を定義したら、remove_sphere
関数を追加できます。次のコードを Game.gd
に追加します:
func remove_sphere():
spheres_left -= 1
if sphere_ui != null:
sphere_ui.update_ui(spheres_left)
この関数が実際に何をするのかを見てみましょう:
まず、spheres_left
変数から1つ削除します。次に、sphere_ui
変数が null
と等しくないかどうかを確認し、null
と等しくない場合は、sphere_ui
の update_ui
関数を、球体の数を引数として渡して呼び出します。
注釈
このチュートリアルの後半で sphere_ui
のコードを追加します!
これで Sphere_Target
を使用する準備ができましたが、それを破壊する方法はありません。ターゲットを損傷させる事が可能な特別な RigidBody ベースのノードを追加して、これを修正しましょう。
ピストルを追加する¶
最初の対話可能な RigidBody ノードとしてピストルを追加しましょう。Scenes
フォルダにある Pistol.tscn
を開きます。
コードを追加する前に Pistol.tscn
のいくつかの注意事項を簡単に確認しましょう。
Pistol.tscn
のすべてのノードは、ルートノードが回転することを期待しています。これは、ピストルがピックアップされるときにVRコントローラーに対して正しい回転になるようにするためです。ルートノードは RigidBody ノードです。このチュートリアルシリーズの最後のパートで作成する VR_Interactable_Rigidbody
クラスを使用するため、これが必要です。
Pistol_Flash
という名前の MeshInstance ノードがあります。これは、ピストルの銃身の端にある銃口フラッシュをシミュレートするために使用する単純なメッシュです。LaserSight
という MeshInstance ノードは、ピストルを狙うためのガイドとして使用され、ピストルが「弾丸」が何かに当たったかどうかを検出するために使用する ``Raycast``と呼ばれる Raycast ノードの方向に従います。最後に、ピストルの発射音を再生するために使用する AudioStreamPlayer3D ノードがピストルの最後にあります。
必要に応じて、シーンの他の部分を見てください。シーンのほとんどはかなり簡単で、上記の大きな変更があります。Pistol
という RigidBody ノードを選択し、Pistol.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends VR_Interactable_Rigidbody
var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0
var laser_sight_mesh
var pistol_fire_sound
var raycast
const BULLET_DAMAGE = 20
const COLLISION_FORCE = 1.5
func _ready():
flash_mesh = get_node("Pistol_Flash")
flash_mesh.visible = false
laser_sight_mesh = get_node("LaserSight")
laser_sight_mesh.visible = false
raycast = get_node("RayCast")
pistol_fire_sound = get_node("AudioStreamPlayer3D")
func _physics_process(delta):
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
flash_mesh.visible = false
func interact():
if flash_timer <= 0:
flash_timer = FLASH_TIME
flash_mesh.visible = true
raycast.force_raycast_update()
if raycast.is_colliding():
var body = raycast.get_collider()
var direction_vector = raycast.global_transform.basis.z.normalized()
var raycast_distance = raycast.global_transform.origin.distance_to(raycast.get_collision_point())
if body.has_method("damage"):
body.damage(BULLET_DAMAGE)
elif body is RigidBody:
var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)
pistol_fire_sound.play()
if controller != null:
controller.rumble = 0.25
func picked_up():
laser_sight_mesh.visible = true
func dropped():
laser_sight_mesh.visible = false
このスクリプトの仕組みを見ていきましょう。
ピストルのコードの説明¶
まず、extends RigidBody
の代わりに、extends VR_Interactable_Rigidbody
を使用することに注意してください。これにより、ピストルスクリプトが VR_Interactable_Rigidbody
クラスを拡張し、VRコントローラーがこのオブジェクトとやり取りできることと、このオブジェクトがVRコントローラーによって保持されているときに VR_Interactable_Rigidbody
で定義された関数を呼び出すことができるようになります。
次に、クラス変数を見てみましょう:
flash_mesh
: ピストルの銃口フラッシュをシミュレートするために使用される MeshInstance ノードを保持する変数。FLASH_TIME
: 銃口フラッシュが見える時間を定義する定数。これは、ピストルが発射できる速さも定義します。flash_timer
: 銃口のフラッシュが見える時間を保持する変数。laser_sight_mesh
: ピストルの「レーザーサイト」として機能する MeshInstance ノードを保持する変数。pistol_fire_sound
: ピストルの発射音に使用される AudioStreamPlayer3D ノードを保持する変数。raycast
: ピストルが発射されたときの弾丸の位置と法線の計算に使用される Raycast ノードを保持する変数。BULLET_DAMAGE
: ピストルからの1つの弾丸が与えるダメージの量を定義する定数。COLLISION_FORCE
: ピストルの弾丸が衝突したときに RigidBody ノードに適用される力の量を定義する定数。
_ready
関数のステップごとの説明¶
この関数はノードを取得し、適切な変数に割り当てます。flash_mesh
および laser_sight_mesh
ノードでは、両方とも visible
プロパティが false
に設定されているため、最初は表示されません。
_physics_process
関数のステップごとの説明¶
_physics_process
関数は、最初に flash_timer
が0より大きいかどうかをチェックすることで、ピストルの銃口フラッシュが見えるかどうかを確認します。flash_timer
が0より大きい場合、その時間から delta
を減らします。次に、delta
分を減らしたので、flash_timer
変数がゼロ以下かどうかを確認します。そうである場合、ピストル銃口フラッシュタイマーはちょうど終了したので、visible
プロパティを false
に設定して、flash_mesh
を非表示にする必要があります。
interact
関数のステップごとの説明¶
interact関数は、最初に flash_timer
がゼロ以下であるかどうかを確認することで、ピストルの銃口フラッシュが見えないかどうかを確認します。これを行うことで、ピストルの発射速度を銃口フラッシュが見える時間の長さに制限できます。これは、プレイヤーが連続的に発射できる速さを制限するための簡単な解決策です。
flash_timer
がゼロ以下の場合、flash_timer
を FLASH_TIME
に設定して、ピストルが再び発砲できるようになるまでに遅延が生じるようにします。その後、flash_mesh.visible
を true
に設定して、flash_timer
がゼロよりも大きいときにピストルの端の銃口フラッシュが見えるようにします。
次に、物理世界から最新の衝突情報を取得できるように、raycast
の Raycast ノードで force_raycast_update
関数を呼び出します。次に、is_colliding
関数が true
に等しいかどうかを確認することで、raycast
が何かにヒットするかどうかを確認します。
Raycast
が何かに当たった場合、get_collider
関数を介して衝突した PhysicsBody を取得します。ヒットした PhysicsBody を body
という変数に割り当てます。
次に、raycast
ノードの global_transform
の Basis から正の Z
方向軸を取得することにより、Raycast の方向を取得します。 これにより、レイキャストがZ軸を指す方向がわかります。これは、Godotエディタで ローカル空間モード
が有効になっている場合、Spatial ギズモの青い矢印が示す方向と同じです。この方向を direction_vector
と呼ばれる変数に保存します。
次に、distance_to
関数を使用して、raycast
ノードのグローバル位置 global_transform.origin
から Raycast の衝突点までの距離、raycast.get_collision_point
を取得することにより、Raycast の原点から Raycast の衝突点までの距離を取得します。これにより、Raycast が衝突する前に移動した距離がわかり、raycast_distance
という変数に格納されます。
次に、コードは PhysicsBody である body
が has_method
関数を使用して damage
という関数/メソッドを持っているかどうかをチェックします。PhysicsBody に damage
という関数/メソッドがある場合、damage
関数を呼び出し、BULLET_DAMAGE
を渡して、衝突する弾丸からのダメージを受けます 。
PhysicsBody に damage
関数があるかどうかに関係なく、body
が RigidBody ベースのノードであるかどうかを確認します。body
が RigidBody ベースのノードである場合、弾丸が衝突したときに押し出します。
適用される力の量を計算するには、単に COLLISION_FORCE
を取得し、それを raycast_distance
で割ってから、全体に body.mass
を掛けます。この計算を collision_force
と呼ばれる変数に保存します。これは、より短い距離での衝突に、より長い距離での衝突よりも多くの移動力を適用し、心もち現実的な衝突応答を提供します。
次に、apply_impulse
関数を使用して RigidBody を押し出します。位置はゼロVector3であるため、中心から力が適用され、衝突力は計算した collision_force
変数になります。
raycast
変数が何かに当たったかどうかに関係なく、pistol_fire_sound
変数で play
関数を呼び出して pistol_fire_sound
を再生します。
最後に、controller
変数が null
と等しくないかどうかを確認することで、ピストルがVRコントローラーに保持されているかどうかを確認します。null
と等しくない場合は、VRコントローラーの rumble
プロパティを 0.25
に設定します。そのため、ピストルが発射されるときにわずかな振動が発生します。
picked_up
関数のステップごとの説明¶
この関数は、単に visible
プロパティを true
に設定することで laser_sight_mesh
MeshInstance を可視にします。
dropped
関数のステップごとの説明¶
この関数は、単に visible
プロパティを false
に設定することで laser_sight_mesh
MeshInstance を非表示にします。
ピストル完成¶

プロジェクトで動作するピストルを使うために必要なことはこれだけです! 先に進み、プロジェクトを実行しましょう。階段を上ってピストルをつかむと、VRコントローラーのトリガーボタンを使用して、シーンの球体ターゲットにそれらを発射できます! ターゲットを十分に長く打ち続けた場合、それらはバラバラになります。
ショットガンを追加する¶
次に、VRプロジェクトにショットガンを追加しましょう。
ショットガンのほとんどすべてがピストルと同じであるため、特別なショットガン RigidBody を追加するのはかなり簡単です。
Scenes
フォルダにある Shotgun.tscn
を開き、シーンを確認します。ほとんどすべてが Pistol.tscn
と同じです。唯一の違いは、名前の変更以外に、単一の Raycast の代わりに、5つの Raycast ノードがあることです。これは、ショットガンが通常コーン形状で発砲するため、ショットガンが発砲するとコーン形状でランダムに回転する複数の Raycast ノードを使用して、その効果をエミュレートするためです。
それ以外は、すべてが Pistol.tscn
とほぼ同じです。
ショットガンのコードを書きましょう。Shotgun
という RigidBody ノードを選択し、Shotgun.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends VR_Interactable_Rigidbody
var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0
var laser_sight_mesh
var shotgun_fire_sound
var raycasts
const BULLET_DAMAGE = 30
const COLLISION_FORCE = 4
func _ready():
flash_mesh = get_node("Shotgun_Flash")
flash_mesh.visible = false
laser_sight_mesh = get_node("LaserSight")
laser_sight_mesh.visible = false
raycasts = get_node("Raycasts")
shotgun_fire_sound = get_node("AudioStreamPlayer3D")
func _physics_process(delta):
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
flash_mesh.visible = false
func interact():
if flash_timer <= 0:
flash_timer = FLASH_TIME
flash_mesh.visible = true
for raycast in raycasts.get_children():
if not raycast is RayCast:
continue
raycast.rotation_degrees = Vector3(90 + rand_range(10, -10), 0, rand_range(10, -10))
raycast.force_raycast_update()
if raycast.is_colliding():
var body = raycast.get_collider()
var direction_vector = raycasts.global_transform.basis.z.normalized()
var raycast_distance = raycasts.global_transform.origin.distance_to(raycast.get_collision_point())
if body.has_method("damage"):
body.damage(BULLET_DAMAGE)
if body is RigidBody:
var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)
shotgun_fire_sound.play()
if controller != null:
controller.rumble = 0.25
func picked_up():
laser_sight_mesh.visible = true
func dropped():
laser_sight_mesh.visible = false
このコードの大部分はピストルのコードとまったく同じであり、主に名前が異なるだけのわずかな変更があります。 これらのスクリプトは類似しているため、変更点のみに注目しましょう。
ショットガンのコードの説明¶
ピストルと同様に、ショットガンは VR_Interactable_Rigidbody
を拡張するため、VRコントローラーは、このオブジェクトとやり取りできること、および使用可能な機能を認識します。
新しいクラス変数は1つだけです:
raycasts
: すべての Raycast ノードを子として持つノードを保持する変数。
新しいクラス変数は Pistol.gd
の raycast
変数を置き換えます。ショットガンでは、1つではなく複数の Raycast ノードを処理する必要があるためです。他のすべてのクラス変数は Pistol.gd
と同じであり、同じように機能します。一部は、ピストル固有ではないように名前が変更されます。
interact
関数のステップごとの説明¶
interact(相互作用)関数は、最初に flash_timer
がゼロ以下であるかどうかを確認することにより、ショットガンの銃口フラッシュが見えないかどうかを確認します。これにより、ショットガンの発射速度を銃口フラッシュが見える時間で制限できます。これは、プレイヤーが連続発射できる速さを制限する簡単な解決策です。
flash_timer
がゼロ以下の場合、flash_timer
を FLASH_TIME
に設定し、ショットガンが再び発砲できるようになるまでに遅延が生じるようにします。その後、flash_mesh.visible
を true
に設定します。これにより、flash_timer
がゼロよりも大きい間、ショットガンの端の銃口フラッシュが見えるようになります。
次に、物理世界から最新の衝突情報を取得できるように、raycast
の Raycast ノードで force_raycast_update
関数を呼び出します。次に、is_colliding
関数が true
に等しいかどうかを確認することで、raycast
が何かにヒットするかどうかを確認します。
次に、forループを使用して raycasts
変数の各子ノードを調べます。これによって、コードは raycasts
変数の子である Raycast ノードのそれぞれを処理します。
各ノードについて、raycast
が Raycast ノードではないかどうかを確認します。ノードが Raycast ノードでない場合は、単に continue
を使用してスキップします。
次に、raycast
の rotation_degrees
変数をVector3に設定して、10
度の小さな円錐の周りに raycast
ノードをランダムに回転させます。XおよびZ軸は -10
〜 10
の乱数です。この乱数は rand_range
関数を使用して選択します。
次に、物理世界から最新の衝突情報を取得するために、raycast
の Raycast ノードで force_raycast_update
関数を呼び出します。次に、is_colliding
関数が true
に等しいかどうかを確認することで、raycast
が何かにヒットするかどうかを確認します。
残りのコードはまったく同じですが、このプロセスは raycasts
変数の子である各 Raycast ノードに対して繰り返されます。
Raycast
が何かに当たった場合、get_collider
関数を介して衝突した PhysicsBody を取得します。ヒットした PhysicsBody を body
という変数に割り当てます。
次に、raycast
ノードの global_transform
上の Basis から正の Z
方向軸を取得することで、raycastの方向を取得します。これにより、raycastがZ軸を指す方向がわかります。これは、Godotエディタで ローカル空間モード
が有効になっている場合に、Spatial ギズモの青い矢印が示す方向と同じです 。この方向を direction_vector
と呼ばれる変数に保存します。
次に、distance_to
関数を使用して、raycast
ノードのグローバル位置である global_transform.origin
からレイキャストの衝突点 raycast.get_collision_point
までの距離を取得することにより、レイキャストの原点からレイキャスト衝突点までの距離を取得します。 これにより、衝突する前に Raycast が移動した距離が得られ、`` raycast_distance`` という変数に格納されます。
次に、コードは PhysicsBody である body
が has_method
関数を使用して damage
という関数/メソッドを持っているかどうかをチェックします。PhysicsBody に damage
という関数/メソッドがある場合、damage
関数を呼び出し、BULLET_DAMAGE
を渡して、衝突する弾丸からのダメージを受けます 。
PhysicsBody に damage
関数があるかどうかに関係なく、body
が RigidBody ベースのノードであるかどうかを確認します。body
が RigidBody ベースのノードである場合、弾丸が衝突したときに押し出します。
適用される力の量を計算するには、単に COLLISION_FORCE
を取得し、それを raycast_distance
で割ってから、全体に body.mass
を掛けます。この計算を collision_force
と呼ばれる変数に保存します。これは、より短い距離での衝突に、より長い距離での衝突よりも多くの移動力を適用し、心もち現実的な衝突応答を提供します。
次に、apply_impulse
関数を使用して RigidBody を押し出します。位置はゼロVector3であるため、中心から力が適用され、衝突力は計算した collision_force
変数になります。
raycast
変数内のすべての :ref: Raycast <class_Raycast>` が繰り返されると、shotgun_fire_sound
変数の play
関数を呼び出して、ショットガンの発砲音を再生します。
最後に、controller
変数が null
と等しくないかどうかを確認することにより、ショットガンがVRコントローラーに保持されているかどうかを確認します。null
と等しくない場合、VRコントローラーの rumble
プロパティを 0.25
に設定します。そのため、ショットガンの発砲時にわずかな振動が発生します。
ショットガン完成¶
せいぜいいくつかの単純な名前の変更があるだけで、他のすべてはピストルとまったく同じです。
これでショットガンが完成しました!サンプルシーンでショットガンを見つけるには、壁の1つ(建物ではなく!)の後ろを見てください。
爆弾を追加する¶
さて、別の特別な RigidBody を追加しましょう。撃つものを追加する代わりに、投げることができるもの - 爆弾を追加しましょう!
Scenes
フォルダにある Bomb.tscn
を開きます。
ルートノードは VR_Interactable_Rigidbody
を使用するように拡張する RigidBody ノードです。これには、これまでに作成した他の特別な RigidBody ノードと同様の CollisionShape があります。同様に、爆弾のメッシュを表示するために使用される Bomb
と呼ばれる MeshInstance があります。
次に、単に Area
と呼ばれる Area ノードがあり、そのノードには大きな CollisionShape が子としてあります。この Area ノードを使用して、爆弾が爆発したときにその中にあるものに影響を与えます。基本的に、この Area ノードは爆弾の爆発半径になります。
いくつかの Particles ノードもあります。Particles ノードの1つは爆弾のヒューズから出る煙用で、もう1つは爆発用です。必要に応じて、パーティクルの動作を定義する ParticlesMaterial リソースを確認できます。このチュートリアルの範囲外であるため、このチュートリアルではパーティクルがどのように機能するかについてはカバーしません。
Particles ノードには、注意する必要があるものが1つあります。Explosion_Particles
ノードを選択すると、lifetime
プロパティが 0.75
に設定され、one shot
チェックボックスが有効になっていることがわかります。つまり、パーティクルは1回だけ再生され、パーティクルは 0.75
秒間続きます。これを知っておくことで、爆発 Particles の終了時に爆弾の除去のタイミングを計ることができます。
爆弾のコードを書きましょう。Bomb
RigidBody ノードを選択し、Bomb.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends VR_Interactable_Rigidbody
var bomb_mesh
const FUSE_TIME = 4
var fuse_timer = 0
var explosion_area
const EXPLOSION_DAMAGE = 100
const EXPLOSION_TIME = 0.75
var explosion_timer = 0
var exploded = false
const COLLISION_FORCE = 8
var fuse_particles
var explosion_particles
var explosion_sound
func _ready():
bomb_mesh = get_node("Bomb")
explosion_area = get_node("Area")
fuse_particles = get_node("Fuse_Particles")
explosion_particles = get_node("Explosion_Particles")
explosion_sound = get_node("AudioStreamPlayer3D")
set_physics_process(false)
func _physics_process(delta):
if fuse_timer < FUSE_TIME:
fuse_timer += delta
if fuse_timer >= FUSE_TIME:
fuse_particles.emitting = false
explosion_particles.one_shot = true
explosion_particles.emitting = true
bomb_mesh.visible = false
collision_layer = 0
collision_mask = 0
mode = RigidBody.MODE_STATIC
for body in explosion_area.get_overlapping_bodies():
if body == self:
pass
else:
if body.has_method("damage"):
body.damage(EXPLOSION_DAMAGE)
if body is RigidBody:
var direction_vector = body.global_transform.origin - global_transform.origin
var bomb_distance = direction_vector.length()
var collision_force = (COLLISION_FORCE / bomb_distance) * body.mass
body.apply_impulse(Vector3.ZERO, direction_vector.normalized() * collision_force)
exploded = true
explosion_sound.play()
if exploded:
explosion_timer += delta
if explosion_timer >= EXPLOSION_TIME:
explosion_area.monitoring = false
if controller != null:
controller.held_object = null
controller.hand_mesh.visible = true
if controller.grab_mode == "RAYCAST":
controller.grab_raycast.visible = true
queue_free()
func interact():
set_physics_process(true)
fuse_particles.emitting = true
このスクリプトの仕組みを見ていきましょう。
爆弾のコードの説明¶
他の特別な RigidBody ノードと同様に、爆弾は VR_Interactable_Rigidbody
を拡張するため、VRコントローラーはこのオブジェクトが対話可能であり、VR_Interactable_Rigidbody
で定義された関数がこのオブジェクトをVRコントローラーで保持しているときに呼び出すことができることを認識します。
次に、クラス変数を見てみましょう:
bomb_mesh
: 爆発しない爆弾に使用される MeshInstance ノードを保持する変数。FUSE_TIME
: 爆弾が爆発する前にヒューズが「燃える」時間を定義する定数fuse_timer
: 爆弾のヒューズが燃え始めてから経過した時間の長さを保持する変数。explosion_area
: 爆弾の爆発内のオブジェクトを検出するために使用される Area ノードを保持する変数。EXPLOSION_DAMAGE
: 爆弾の爆発でどの程度のダメージが適用されるかを定義する定数。EXPLOSION_TIME
: 爆弾が爆発した後、シーン内で爆弾が持続する時間を定義する定数。この値は、爆発 Particles ノードのlifetime
プロパティと同じでなければなりません。explosion_timer
: 爆弾が爆発してから経過した時間の長さを保持する変数。exploded
: 爆弾が爆発したかどうかを保持する変数。COLLISION_FORCE
: 爆弾が爆発したときに RigidBody ノードに適用される力の量を定義する定数。fuse_particles
: 爆弾のヒューズに使用される Particles ノードへの参照を保持する変数。explosion_particles
: 爆弾の爆発に使用される Particles ノードへの参照を保持する変数。explosion_sound
: 爆発音に使用される AudioStreamPlayer3D ノードへの参照を保持する変数。
_ready
関数のステップごとの説明¶
_ready
関数は最初に爆弾シーンからすべてのノードを取得し、後で使用するためにそれぞれのクラス変数に割り当てます。
それから set_physics_process
を呼び出して false
を渡すので _physics_process
は実行されません。これを行う理由は、_physics_process
のコードがヒューズの燃焼と爆弾の爆発を開始するためです。これは、ユーザーが爆弾と対話するときにのみ行いたい事です。_physics_process
を無効にしないと、ユーザーが爆弾に到達する前に爆弾のヒューズが起動します。
_physics_process
関数のステップごとの説明¶
_physics_process
関数はまず fuse_timer
が FUSE_TIME
よりも小さいかどうかを確認します。もしそうなら、爆弾のヒューズはまだ燃えています。
爆弾のヒューズがまだ燃えている場合は、時間 delta
を fuse_timer
変数に追加します。次に、delta
を追加したので、fuse_timer
が FUSE_TIME
以上であるかどうかを確認します。fuse_timer
が FUSE_TIME
以上の場合、ヒューズはちょうど燃え終わったので、爆弾を爆発させる必要があります。
爆弾を爆発させるには、まず fuse_particles
で emitting
を false
に設定して、ヒューズのパーティクルの放出を停止します。それから、爆発 Particles ノード explosion_particles
の one_shot
を true
に設定することで、単一ショットですべてのパーティクルを放出するように指示します。その後、explosion_particles
で emission
を true
に設定し、爆弾が爆発したようすを見せます。爆弾が爆発したように見せるために、bomb_mesh.visible
を false
に設定して、爆弾 MeshInstance ノードは非表示にします。
爆弾が物理世界の他のオブジェクトと衝突しないように、爆弾の collision_layer
および collision_mask
プロパティを 0
に設定します。また、RigidBody モードを MODE_STATIC
に変更して、爆弾 RigidBody が移動しないようにします。
次に、explosion_area
ノード内のすべての PhysicsBody ノードを取得する必要があります。これを行うには、forループで get_overlapping_bodies
を使用します。get_overlapping_bodies
関数は Area ノード内の PhysicsBody ノードの配列を返します。これはまさに探しているものです。
body
と呼ばれる変数に格納する各 PhysicsBody ノードについて、それが self
と等しいかどうかを確認します。これは、explosion_area
が Bomb
RigidBody 自体を爆発領域内のPhysicsBodyとして検出する可能性があるため、爆弾が誤って爆発しないようにするためです。
PhysicsBody ノード body
が爆弾でない場合、まず PhysicsBody ノードに damage
という関数があるかどうかを確認します。PhysicsBody ノードに damage
という関数がある場合、それを呼び出して EXPLOSION_DAMAGE
を渡し、爆発によるダメージを受けます。
次に、PhysicsBody ノードが RigidBody であるかどうかを確認します。body
が RigidBody の場合、爆弾が爆発したときに移動します。
爆弾が爆発したときに RigidBody ノードを移動するには、まず爆弾から RigidBody ノードへの方向を計算する必要があります。これを行うには、爆弾のグローバル位置 global_transform.origin
を RigidBody のグローバル位置から減算します。これにより、爆弾から RigidBody ノードを指す Vector3 が得られます。これを Vector3 を direction_vector
という変数に保存します。
次に、direction_vector
の length
関数を使用して、RigidBody が爆弾からの距離を計算します。距離を bomb_distance
と呼ばれる変数に保存します。
次に、COLLISION_FORCE
を bomb_distance
で割って collision_force
を掛けることで、爆弾が爆発したときに爆弾が RigidBody ノードに適用される力の量を計算します。これにより、RigidBody ノードが爆弾により近い場合、より遠くに押し出されます。
最後に、apply_impulse
関数を使用して RigidBody ノードを押し出します。Vector3
の位置をゼロにし、collision_force
に direction_vector.normalized
を力として掛けます。これにより、爆弾が爆発したときに RigidBody
ノードが飛んで行きます。
explosion_area
内のすべての PhysicsBody ノードをループした後、exploded
変数を true
に設定し、コードが爆弾を認識して呼び出す``explosion_sound`` で play
を実行すると、爆発の音が再生されます。
さて、コードの次のセクションは、まず exploded
が true
に等しいかどうかをチェックすることから始まります。
exploded
が true
に等しい場合、爆弾は爆発パーティクルが終了するのを待ってから、それ自体を解放/破棄します。爆弾が爆発してからの時間を追跡できるように、explosion_timer
に時間 delta
を加算します。
delta
を追加した後に explosion_timer
が EXPLOSION_TIME
以上である場合は、ちょうど爆発タイマーが終了しています。
爆発タイマーがちょうど終了した場合、explosion_area.monitoring
を false
に設定します。これを行う理由は、monitoring
プロパティがtrueのときに Area ノードを解放または削除したときにエラーを出力するバグがあったためです。これが起こらないようにするには、explosion_area
で monitoring
をfalseに設定するだけです。
次に、controller
変数が null
と等しくないかどうかを確認することにより、爆弾がVRコントローラーによって保持されているかどうかを確認します。爆弾がVRコントローラーによって保持されている場合、VRコントローラーの held_object
プロパティ controller
を null
に設定します。VRコントローラーはもはや何も保持していないため、controller.hand_mesh.visible
を true
に設定することで、VRコントローラーのハンドメッシュを表示します。次に、VRコントローラーのグラブモードが RAYCAST
であるかどうかを確認し、controller.grab_raycast.visible
を true
に設定して、grab raycastの レーザーサイト
を可視化します。
最後に、爆弾がVRコントローラーに保持されているかどうかに関係なく、queue_free
を呼び出して、爆弾のシーンをシーンから解放/削除します。
interact
関数のステップごとの説明¶
最初に interact
関数が set_physics_process
を呼び出して true
を渡すので、_physics_process
のコードが実行を開始します。これは爆弾のヒューズを開始し、最終的に爆弾の爆発につながります。
最後に、fuse_particles.visible
を true
に設定して、ヒューズパーティクルを開始します。
爆弾完成¶
これで爆弾の準備ができました!オレンジ色の建物で爆弾を見つけることができます。
VRコントローラーの速度の計算方法により、より自然な投げのような動きの代わりに、突きのような動きを使用して爆弾を投げるのが最も簡単です。 VRコントローラーの速度を計算するために使用しているコードでは、投げのような動きの滑らかな曲線を追跡するのが難しいため、常に正しく動作するとは限らず、不正確に計算された速度につながる可能性があります。
剣を追加する¶
最後に、ターゲットを破壊できる RigidBody ベースの特別なノードを1つ追加しましょう。ターゲットをスライスできるように、剣を追加しましょう!
Scenes
フォルダにある Sword.tscn
を開きます。
ここではあまり多くのことはありません。ルート Sword
のすべての子ノード RigidBody ノードは、VRコントローラーがそれらを選択したときに正しく配置されるように回転し、MeshInstance があります剣を表示するためのノード、および何かと衝突する剣の音を保持する AudioStreamPlayer3D ノードがあります。
ただし、少し異なる点が1つあります。Damage_Body
と呼ばれる KinematicBody ノードがあります。見てみると、コリジョンレイヤー上ではなく、単一のコリジョンマスク上にあることがわかります。これは、KinematicBody がシーン内の他の PhysicsBody ノードに影響を与えることはありませんが、PhysicsBody ノードの影響は引き続き受けます。
Damage_Body
KinematicBody ノードを使用して、剣がシーン内の何かと衝突したときの衝突点と法線を検出します。
ちなみに
While this is perhaps not the best way of getting the collision information from a performance point of view, it does give us a lot of information we can use for post-processing! Using a KinematicBody this way means we can detect exactly where the sword collided with other PhysicsBody nodes.
それは本当に剣のシーンについてふさわしい唯一の注意事項です。Sword
RigidBody ノードを選択し、Sword.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends VR_Interactable_Rigidbody
const SWORD_DAMAGE = 2
const COLLISION_FORCE = 0.15
var damage_body = null
func _ready():
damage_body = get_node("Damage_Body")
damage_body.add_collision_exception_with(self)
sword_noise = get_node("AudioStreamPlayer3D")
func _physics_process(_delta):
var collision_results = damage_body.move_and_collide(Vector3.ZERO, true, true, true);
if (collision_results != null):
if collision_results.collider.has_method("damage"):
collision_results.collider.damage(SWORD_DAMAGE)
if collision_results.collider is RigidBody:
if controller == null:
collision_results.collider.apply_impulse(
collision_results.position,
collision_results.normal * linear_velocity * COLLISION_FORCE)
else:
collision_results.collider.apply_impulse(
collision_results.position,
collision_results.normal * controller.controller_velocity * COLLISION_FORCE)
sword_noise.play()
このスクリプトの仕組みを見ていきましょう!
剣のコードの説明¶
他の特別な RigidBody ノードと同様に、剣は VR_Interactable_Rigidbody
を拡張するため、VRコントローラーはこのオブジェクトが対話可能であり、VR_Interactable_Rigidbody
で定義された関数がこのオブジェクトがVRコントローラーに保持されているときに呼び出せることを認識します。
次に、クラス変数を見てみましょう:
SWORD_DAMAGE
: A constant to define the amount of damage the sword does. This damage is applied to every object in the sword on every_physics_process
callCOLLISION_FORCE
: 剣が PhysicsBody と衝突したときに RigidBody ノードに適用される力の量を定義する定数。damage_body
: 剣が PhysicsBody ノードを突き刺しているかどうかを検出するために使用される KinematicBody ノードを保持する変数。sword_noise
: 剣が何かと衝突したときにサウンドを再生するために使用される AudioStreamPlayer3D ノードを保持する変数。
_ready
関数のステップごとの説明¶
_ready
関数で行っているのは、Damage_Body
KinematicBody ノードを取得し、それを damage_body
に割り当てることです。剣が剣のルート RigidBody ノードとの衝突を検出しないようにするため、damage_body
で add_collision_exception_with
を呼び出して self
を渡します。これで剣は検出されません。
最後に、剣衝突音の AudioStreamPlayer3D ノードを取得し、それを sword_noise
変数に適用します。
_physics_process
関数のステップごとの説明¶
まず、剣が何かと衝突しているかどうかを判断する必要があります。これを行うには、damage_body
ノードの move_and_collide
関数を使用します。move_and_collide
の通常の使用方法とは異なり、速度を渡すのではなく、空の Vector3 を渡します。damage_body
ノードを移動させたくないので、test_only
引数(4番目の引数)を true
に設定して、KinematicBody が実際に衝突ワールド内で衝突を引き起こすことなく衝突情報を生成するようにします。
move_and_collide
関数は、剣の衝突を検出するために必要なすべての情報を含む KinematicCollision クラスを返します。move_and_collide
の戻り値を collision_results
という変数に割り当てます。
次に、collision_results
が null
と等しくないかどうかを確認します。collision_results
が null
と等しくない場合、剣が何かと衝突していることがわかります。
次に、剣が衝突した PhysicsBody が has_method
関数を使用して damage
という関数/メソッドを持っているかどうかを確認します。PhysicsBody に damage_body
という関数がある場合、それを呼び出して、剣が与えるダメージ量 SWORD_DAMAGE
を渡します。
次に、剣が衝突した PhysicsBody が RigidBody であるかどうかを確認します。剣が衝突したものが RigidBody ノードである場合、controller
が null
に等しいかどうかを確認することで、剣がVRコントローラーに保持されているかどうかを確認します。
剣がVRコントローラーによって保持されていない場合、controller
は null
に等しいため、剣が apply_impulse
関数を使用して衝突した RigidBody ノードを移動します。apply_impulse
関数の position
には、collision_results
の KinematicCollision クラス内に格納されている collision_position
変数を使用します。apply_impulse
関数の速度については、collision_normal
に剣の RigidBody ノードの linear_velocity
と COLLISION_FORCE
を掛けた値を使用します。
剣がVRコントローラーによって保持されている場合、つまりcontroller
が null
と等しくない場合、剣が apply_impulse
関数を使用して衝突した RigidBody ノードを移動します`。apply_impulse
関数の position
には、collision_results
の KinematicCollision クラス内に格納されている collision_position
変数を使用します。apply_impulse
関数の velocity
には、collision_normal
にVRコントローラーの速度を掛け、さらにCOLLISION_FORCE
を掛けます。
最後に、PhysicsBody が RigidBody であるかどうかに関係なく、sword_noise
で play
を呼び出すことで、何かと衝突する剣の音を再生します。
完成した剣¶

これで、ターゲットをスライスできます!ショットガンとピストルの間の隅にある剣を見つけることができます。
ターゲットUIの更新¶
球体ターゲットが破壊されるとUIを更新しましょう。
Scenes``フォルダにある ``Main_VR_GUI.tscn
を開きます。必要に応じてシーンのセットアップ方法を自由に確認してください。ただし、このチュートリアルが長くなりすぎないように、このチュートリアルではシーンのセットアップについては説明しません。
GUI
Viewport ノードを展開し、 Base_Control
ノードを選択します。Base_Control.gd
という新しいスクリプトを追加し、次を追加します:
extends Control
var sphere_count_label
func _ready():
sphere_count_label = get_node("Label_Sphere_Count")
get_tree().root.get_node("Game").sphere_ui = self
func update_ui(sphere_count):
if sphere_count > 0:
sphere_count_label.text = str(sphere_count) + " Spheres remaining"
else:
sphere_count_label.text = "No spheres remaining! Good job!"
このスクリプトが実際にどのようにすばやく機能するかを見ていきましょう。
まず、_ ready
で、残りの球の数を示す Label を取得し、それを sphere_count_label
クラス変数に割り当てます。次に、 get_tree().root
を使用して Game.gd
を取得し、sphere_ui
をこのスクリプトに割り当てます。
update_ui
では、球体 Label のテキストを変更します。少なくとも1つの球体が残っている場合は、テキストを変更して、まだ世界に残っている球体の数を表示します。球が残っていない場合は、テキストを変更してプレイヤーを祝福します。
最後の特別なRigidBodyの追加¶
最後に、このチュートリアルを完了する前に、VRでゲームをリセットする方法を追加しましょう。
Scenes
にある Reset_Box.tscn
を開きます。Reset_Box
RigidBody ノードを選択し Reset_Box.gd
という新しいスクリプトを作成します。次のコードを追加します:
extends VR_Interactable_Rigidbody
var start_transform
var reset_timer = 0
const RESET_TIME = 10
const RESET_MIN_DISTANCE = 1
func _ready():
start_transform = global_transform
func _physics_process(delta):
if start_transform.origin.distance_to(global_transform.origin) >= RESET_MIN_DISTANCE:
reset_timer += delta
if reset_timer >= RESET_TIME:
global_transform = start_transform
reset_timer = 0
func interact():
# (Ignore the unused variable warning)
# warning-ignore:return_value_discarded
get_tree().change_scene("res://Game.tscn")
func dropped():
global_transform = start_transform
reset_timer = 0
このスクリプトがどのように機能するかを簡単に見てみましょう。
リセットボックスのコードの説明¶
作成した他の特別な RigidBody ベースのオブジェクトと同様に、リセットボックスは VR_Interactable_Rigidbody
を拡張します。
start_transform
クラス変数は、ゲームの開始時にリセットボックスのグローバル変換を格納します。reset_timer
クラス変数は、リセットボックスの位置が移動してから経過した時間の長さを保持します。RESET_TIME
定数は、リセットボックスがリセットされるまで待機する時間の長さを定義し、RESET_MIN_DISTANCE
定数は、リセットタイマーが開始する前にリセットボックスが初期位置からどれだけ離れている必要があるかを定義します。
_ready
関数では、シーンの開始時にリセット位置の global_transform
を保存するだけです。これは、十分な時間が経過したときに、リセットボックスオブジェクトの位置、回転、スケールをこの初期transformにリセットできるようにするためです。
_physics_process
関数では、コードはリセットボックスの現在位置に対するリセットボックスの初期位置が RESET_MIN_DISTANCE
よりも遠いかどうかを確認します。さらに遠い場合は、reset_timer
に時間 delta
を加算し始めます。reset_timer
が RESET_TIME
以上になると、global_transform
を start_transform
にリセットし、リセットボックスが初期位置に戻るようにします。次に、reset_timer
を 0
に設定します。
interact
関数は、単に get_tree().change_scene
を使用して Game.tscn
シーンをリロードします。これにより、ゲームシーンがリロードされ、すべてがリセットされます。
最後に、dropped
関数は global_transform
を start_transform
の初期変換にリセットするので、リセットボックスは初期位置/回転を保持します。次に、reset_timer
が 0
に設定され、タイマーがリセットされます。
リセット ボックスが完了しました¶
これが完了したら、リセットボックスをつかんで操作すると、シーン全体がリセット/再起動され、すべてのターゲットを再び破壊できます!
注釈
なんらかの移行をせずにシーンを突然リセットすると、VRに不快感が生じる可能性があります。
最終ノート¶

ふう!これは大変な仕事でした。
これで、複数の異なるタイプの特別な RigidBody ベースのノードが使用および拡張できる、完全に機能するVRプロジェクトができました。これがGodotでフル機能のVRゲームを作成するための入門書として役立つことを願っています!このチュートリアルで詳しく説明するコードと概念を拡張して、パズルゲーム、アクションゲーム、ストーリーベースのゲームなどを作成できます!
警告
You can download the finished project for this tutorial series on the OpenVR GitHub repository, under the releases tab!