VRスターターチュートリアルパート2

はじめに

../../../../_images/starter_vr_tutorial_sword.png

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 関数のステップごとの説明

最初に、この関数は deltadestroyed_timer 変数に加算します。次に、destroyed_timerDESTROY_WAIT_TIME 以上であるかどうかを確認します。destroyed_timerDESTROY_WAIT_TIME 以上の場合、球体ターゲットは queue_free 関数を呼び出すことで自身を解放/削除します。

damage 関数の詳細な説明

damage 関数は、特別な RigidBody ノードによって呼び出され、ターゲットに加えられたダメージの量を damage と呼ばれる関数の引数変数として渡します。damage 変数は、特別な RigidBody ノードが球体ターゲットに与えたダメージの量を保持します。

最初に、この関数は destroyed 変数が true に等しいかどうかをチェックすることにより、ターゲットが既に破棄されていないことを確認します。destroyedtrue に等しい場合、関数は 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_uiupdate_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_timerFLASH_TIME に設定して、ピストルが再び発砲できるようになるまでに遅延が生じるようにします。その後、flash_mesh.visibletrue に設定して、flash_timer がゼロよりも大きいときにピストルの端の銃口フラッシュが見えるようにします。

次に、物理世界から最新の衝突情報を取得できるように、raycastRaycast ノードで force_raycast_update 関数を呼び出します。次に、is_colliding 関数が true に等しいかどうかを確認することで、raycast が何かにヒットするかどうかを確認します。


Raycast が何かに当たった場合、get_collider 関数を介して衝突した PhysicsBody を取得します。ヒットした PhysicsBodybody という変数に割り当てます。

次に、raycast ノードの global_transformBasis から正の Z 方向軸を取得することにより、Raycast の方向を取得します。 これにより、レイキャストがZ軸を指す方向がわかります。これは、Godotエディタで ローカル空間モード が有効になっている場合、Spatial ギズモの青い矢印が示す方向と同じです。この方向を direction_vector と呼ばれる変数に保存します。

次に、distance_to 関数を使用して、raycast ノードのグローバル位置 global_transform.origin から Raycast の衝突点までの距離、raycast.get_collision_point を取得することにより、Raycast の原点から Raycast の衝突点までの距離を取得します。これにより、Raycast が衝突する前に移動した距離がわかり、raycast_distance という変数に格納されます。

次に、コードは PhysicsBody である bodyhas_method 関数を使用して damage という関数/メソッドを持っているかどうかをチェックします。PhysicsBodydamage という関数/メソッドがある場合、damage 関数を呼び出し、BULLET_DAMAGE を渡して、衝突する弾丸からのダメージを受けます 。

PhysicsBodydamage 関数があるかどうかに関係なく、bodyRigidBody ベースのノードであるかどうかを確認します。bodyRigidBody ベースのノードである場合、弾丸が衝突したときに押し出します。

適用される力の量を計算するには、単に 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 を非表示にします。

ピストル完成

../../../../_images/starter_vr_tutorial_pistol.png

プロジェクトで動作するピストルを使うために必要なことはこれだけです! 先に進み、プロジェクトを実行しましょう。階段を上ってピストルをつかむと、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.gdraycast 変数を置き換えます。ショットガンでは、1つではなく複数の Raycast ノードを処理する必要があるためです。他のすべてのクラス変数は Pistol.gd と同じであり、同じように機能します。一部は、ピストル固有ではないように名前が変更されます。

interact 関数のステップごとの説明

interact(相互作用)関数は、最初に flash_timer がゼロ以下であるかどうかを確認することにより、ショットガンの銃口フラッシュが見えないかどうかを確認します。これにより、ショットガンの発射速度を銃口フラッシュが見える時間で制限できます。これは、プレイヤーが連続発射できる速さを制限する簡単な解決策です。

flash_timer がゼロ以下の場合、flash_timerFLASH_TIME に設定し、ショットガンが再び発砲できるようになるまでに遅延が生じるようにします。その後、flash_mesh.visibletrue に設定します。これにより、flash_timer がゼロよりも大きい間、ショットガンの端の銃口フラッシュが見えるようになります。

次に、物理世界から最新の衝突情報を取得できるように、raycastRaycast ノードで force_raycast_update 関数を呼び出します。次に、is_colliding 関数が true に等しいかどうかを確認することで、raycast が何かにヒットするかどうかを確認します。

次に、forループを使用して raycasts 変数の各子ノードを調べます。これによって、コードは raycasts 変数の子である Raycast ノードのそれぞれを処理します。


各ノードについて、raycastRaycast ノードではないかどうかを確認します。ノードが Raycast ノードでない場合は、単に continue を使用してスキップします。

次に、raycastrotation_degrees 変数をVector3に設定して、10 度の小さな円錐の周りに raycast ノードをランダムに回転させます。XおよびZ軸は -1010 の乱数です。この乱数は rand_range 関数を使用して選択します。

次に、物理世界から最新の衝突情報を取得するために、raycastRaycast ノードで force_raycast_update 関数を呼び出します。次に、is_colliding 関数が true に等しいかどうかを確認することで、raycast が何かにヒットするかどうかを確認します。

残りのコードはまったく同じですが、このプロセスは raycasts 変数の子である各 Raycast ノードに対して繰り返されます。


Raycast が何かに当たった場合、get_collider 関数を介して衝突した PhysicsBody を取得します。ヒットした PhysicsBodybody という変数に割り当てます。

次に、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 である bodyhas_method 関数を使用して damage という関数/メソッドを持っているかどうかをチェックします。PhysicsBodydamage という関数/メソッドがある場合、damage 関数を呼び出し、BULLET_DAMAGE を渡して、衝突する弾丸からのダメージを受けます 。

PhysicsBodydamage 関数があるかどうかに関係なく、bodyRigidBody ベースのノードであるかどうかを確認します。bodyRigidBody ベースのノードである場合、弾丸が衝突したときに押し出します。

適用される力の量を計算するには、単に 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_timerFUSE_TIME よりも小さいかどうかを確認します。もしそうなら、爆弾のヒューズはまだ燃えています。

爆弾のヒューズがまだ燃えている場合は、時間 deltafuse_timer 変数に追加します。次に、delta を追加したので、fuse_timerFUSE_TIME 以上であるかどうかを確認します。fuse_timerFUSE_TIME 以上の場合、ヒューズはちょうど燃え終わったので、爆弾を爆発させる必要があります。

爆弾を爆発させるには、まず fuse_particlesemittingfalse に設定して、ヒューズのパーティクルの放出を停止します。それから、爆発 Particles ノード explosion_particlesone_shottrue に設定することで、単一ショットですべてのパーティクルを放出するように指示します。その後、explosion_particlesemissiontrue に設定し、爆弾が爆発したようすを見せます。爆弾が爆発したように見せるために、bomb_mesh.visiblefalse に設定して、爆弾 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_areaBomb RigidBody 自体を爆発領域内のPhysicsBodyとして検出する可能性があるため、爆弾が誤って爆発しないようにするためです。

PhysicsBody ノード body が爆弾でない場合、まず PhysicsBody ノードに damage という関数があるかどうかを確認します。PhysicsBody ノードに damage という関数がある場合、それを呼び出して EXPLOSION_DAMAGE を渡し、爆発によるダメージを受けます。

次に、PhysicsBody ノードが RigidBody であるかどうかを確認します。bodyRigidBody の場合、爆弾が爆発したときに移動します。

爆弾が爆発したときに RigidBody ノードを移動するには、まず爆弾から RigidBody ノードへの方向を計算する必要があります。これを行うには、爆弾のグローバル位置 global_transform.originRigidBody のグローバル位置から減算します。これにより、爆弾から RigidBody ノードを指す Vector3 が得られます。これを Vector3direction_vector という変数に保存します。

次に、direction_vectorlength 関数を使用して、RigidBody が爆弾からの距離を計算します。距離を bomb_distance と呼ばれる変数に保存します。

次に、COLLISION_FORCEbomb_distance で割って collision_force を掛けることで、爆弾が爆発したときに爆弾が RigidBody ノードに適用される力の量を計算します。これにより、RigidBody ノードが爆弾により近い場合、より遠くに押し出されます。

最後に、apply_impulse 関数を使用して RigidBody ノードを押し出します。Vector3 の位置をゼロにし、collision_forcedirection_vector.normalized を力として掛けます。これにより、爆弾が爆発したときに RigidBody ノードが飛んで行きます。


explosion_area 内のすべての PhysicsBody ノードをループした後、exploded 変数を true に設定し、コードが爆弾を認識して呼び出す``explosion_sound`` で play を実行すると、爆発の音が再生されます。


さて、コードの次のセクションは、まず explodedtrue に等しいかどうかをチェックすることから始まります。

explodedtrue に等しい場合、爆弾は爆発パーティクルが終了するのを待ってから、それ自体を解放/破棄します。爆弾が爆発してからの時間を追跡できるように、explosion_timer に時間 delta を加算します。

delta を追加した後に explosion_timerEXPLOSION_TIME 以上である場合は、ちょうど爆発タイマーが終了しています。

爆発タイマーがちょうど終了した場合、explosion_area.monitoringfalse に設定します。これを行う理由は、monitoring プロパティがtrueのときに Area ノードを解放または削除したときにエラーを出力するバグがあったためです。これが起こらないようにするには、explosion_areamonitoring をfalseに設定するだけです。

次に、controller 変数が null と等しくないかどうかを確認することにより、爆弾がVRコントローラーによって保持されているかどうかを確認します。爆弾がVRコントローラーによって保持されている場合、VRコントローラーの held_object プロパティ controllernull に設定します。VRコントローラーはもはや何も保持していないため、controller.hand_mesh.visibletrue に設定することで、VRコントローラーのハンドメッシュを表示します。次に、VRコントローラーのグラブモードが RAYCAST であるかどうかを確認し、controller.grab_raycast.visibletrue に設定して、grab raycastの レーザーサイト を可視化します。

最後に、爆弾がVRコントローラーに保持されているかどうかに関係なく、queue_free を呼び出して、爆弾のシーンをシーンから解放/削除します。

interact 関数のステップごとの説明

最初に interact 関数が set_physics_process を呼び出して true を渡すので、_physics_process のコードが実行を開始します。これは爆弾のヒューズを開始し、最終的に爆弾の爆発につながります。

最後に、fuse_particles.visibletrue に設定して、ヒューズパーティクルを開始します。

爆弾完成

これで爆弾の準備ができました!オレンジ色の建物で爆弾を見つけることができます。

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 call

  • COLLISION_FORCE: 剣が PhysicsBody と衝突したときに RigidBody ノードに適用される力の量を定義する定数。

  • damage_body: 剣が PhysicsBody ノードを突き刺しているかどうかを検出するために使用される KinematicBody ノードを保持する変数。

  • sword_noise: 剣が何かと衝突したときにサウンドを再生するために使用される AudioStreamPlayer3D ノードを保持する変数。

_ready 関数のステップごとの説明

_ready 関数で行っているのは、Damage_Body KinematicBody ノードを取得し、それを damage_body に割り当てることです。剣が剣のルート RigidBody ノードとの衝突を検出しないようにするため、damage_bodyadd_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_resultsnull と等しくないかどうかを確認します。collision_resultsnull と等しくない場合、剣が何かと衝突していることがわかります。

次に、剣が衝突した PhysicsBodyhas_method 関数を使用して damage という関数/メソッドを持っているかどうかを確認します。PhysicsBodydamage_body という関数がある場合、それを呼び出して、剣が与えるダメージ量 SWORD_DAMAGE を渡します。

次に、剣が衝突した PhysicsBodyRigidBody であるかどうかを確認します。剣が衝突したものが RigidBody ノードである場合、controllernull に等しいかどうかを確認することで、剣がVRコントローラーに保持されているかどうかを確認します。

剣がVRコントローラーによって保持されていない場合、controllernull に等しいため、剣が apply_impulse 関数を使用して衝突した RigidBody ノードを移動します。apply_impulse 関数の position には、collision_resultsKinematicCollision クラス内に格納されている collision_position 変数を使用します。apply_impulse 関数の速度については、collision_normal に剣の RigidBody ノードの linear_velocityCOLLISION_FORCE を掛けた値を使用します。

剣がVRコントローラーによって保持されている場合、つまりcontrollernull と等しくない場合、剣が  apply_impulse 関数を使用して衝突した RigidBody ノードを移動します`。apply_impulse 関数の position には、collision_resultsKinematicCollision クラス内に格納されている collision_position 変数を使用します。apply_impulse 関数の velocity には、collision_normal にVRコントローラーの速度を掛け、さらにCOLLISION_FORCE を掛けます。

最後に、PhysicsBodyRigidBody であるかどうかに関係なく、sword_noiseplay を呼び出すことで、何かと衝突する剣の音を再生します。

完成した剣

../../../../_images/starter_vr_tutorial_sword.png

これで、ターゲットをスライスできます!ショットガンとピストルの間の隅にある剣を見つけることができます。

ターゲット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_timerRESET_TIME 以上になると、global_transformstart_transform にリセットし、リセットボックスが初期位置に戻るようにします。次に、reset_timer0 に設定します。

interact 関数は、単に get_tree().change_scene を使用して Game.tscn シーンをリロードします。これにより、ゲームシーンがリロードされ、すべてがリセットされます。

最後に、dropped 関数は global_transformstart_transform の初期変換にリセットするので、リセットボックスは初期位置/回転を保持します。次に、reset_timer0 に設定され、タイマーがリセットされます。

リセット ボックスが完了しました

これが完了したら、リセットボックスをつかんで操作すると、シーン全体がリセット/再起動され、すべてのターゲットを再び破壊できます!

注釈

なんらかの移行をせずにシーンを突然リセットすると、VRに不快感が生じる可能性があります。

最終ノート

../../../../_images/starter_vr_tutorial_pistol.png

ふう!これは大変な仕事でした。

これで、複数の異なるタイプの特別な RigidBody ベースのノードが使用および拡張できる、完全に機能するVRプロジェクトができました。これがGodotでフル機能のVRゲームを作成するための入門書として役立つことを願っています!このチュートリアルで詳しく説明するコードと概念を拡張して、パズルゲーム、アクションゲーム、ストーリーベースのゲームなどを作成できます!

警告

You can download the finished project for this tutorial series on the OpenVR GitHub repository, under the releases tab!