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

はじめに

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

このチュートリアルでは、Godotで初心者向けのVRゲームプロジェクトを作成する方法を説明します。

VRコンテンツを作成する際に最も重要なことの1つは、アセットの規模を正確にすることです ! これを正しく行うには多くの練習と反復が必要になりますが、簡単にするためにできることがいくつかあります:

  • VRでは、1単位は通常1メートルと見なされます。 その標準を中心に据えてアセットを設計すれば、頭痛の種を大幅に減らすことができます。
  • 3Dモデリングプログラムで、現実世界の距離を測定して使用する方法があるかどうかを確認します。 Blenderでは、MeasureItアドオンを使用できます。 Mayaでは、測定ツールを使用できます。
  • Google Blocks のようなツールを使用して大まかなモデルを作成し、別の3Dモデリングプログラムで改良できます。
  • アセットは、VRとフラットスクリーンで劇的に異なって見える可能性があるので、頻繁にテストしてください!

このチュートリアルでは、次のことを説明します:

  • VRで実行するようにGodotに伝える方法。
  • VRコントローラを使用するテレポーテーション移動システムの作成方法。
  • VRコントローラを使用した人工的な運動移動システムの作り方。
  • VR コントローラを使用してRigidBodyノードをピックアップ、ドロップ、およびスローできる RigidBody ベースのシステムを作成する方法。
  • 単純な破壊可能なターゲットを作成する方法。
  • ターゲットを破壊できる特別な RigidBody ベースのオブジェクトを作成する方法 。

ちなみに

このチュートリアルは初心者でも完了することができますが、Godotやゲーム開発に慣れていない場合は、最初のゲーム を完了することを強くお勧めします。

このチュートリアルシリーズを読む前に、3Dゲームを作成した経験が必要です。このチュートリアルでは、Godotエディタ、GDScript、および基本的な3Dゲーム開発の経験があることを前提としています。OpenVR対応ヘッドセットと2つのOpenVR対応コントローラーが必要です。

このチュートリアルは、Windows Mixed Realityヘッドセットとコントローラーを使用して作成およびテストされています。 このプロジェクトはHTC Viveでもテストされています。 Oculus Riftなどの他のVRヘッドセットでは、コードの調整が必要になる場合があります。

このチュートリアルのGodotプロジェクトは、OpenVR GitHubリポジトリ にあります。 このチュートリアルのスターターアセットは、GitHubリポジトリのリリースセクションにあります。 スターターアセットには、このチュートリアル用に設定された3Dモデル、サウンド、スクリプト、シーンが含まれています。

注釈

提供されたアセットのクレジット:

  • 空のパノラマは、CGTuts によって作成されました。
  • 使用しているフォントはTitillium-Regular
    • このフォントは SIL Open Font License, Version 1.1 でライセンスされています。
  • 使用しているオーディオはいくつかの異なるソースからのもので、すべて Sonniss #GameAudioGDC Bundle からダウンロードされます(ライセンスPDF)
    • オーディオファイルが保存されているフォルダの名前は、Sonniss audio bundle のフォルダと同じです。
  • OpenVRアドオンは Bastiaan Olij によって作成され、MITライセンスの下でリリースされています。Godot Asset LibraryGitHub の両方にあります。OpenVRアドオンで使用されるサードパーティのコードとライブラリは、異なるライセンスの下にある場合があります。
  • 最初のプロジェクト、3Dモデル、およびスクリプトは、TwistedTwigleg によって作成され、MITライセンスの下でリリースされています。

ちなみに

完成したプロジェクトは、``OpenVR GitHub リポジトリ <https://github.com/GodotVR/godot_openvr_fps>`_ で見つけることができます。

すべてを準備する

まだ行っていない場合は、OpenVR GitHubリポジトリ にアクセスし、releasesから "Starter Assets" ファイルをダウンロードします。 スターターアセットをダウンロードしたら、Godotでプロジェクトを開きます。

注釈

このチュートリアルで提供されるスクリプトを使用するために、スターターアセットは必要ありません。 スターターアセットには、チュートリアル全体で使用されるいくつかの作成済みのシーンとスクリプトが含まれています。

プロジェクトが最初にロードされると、Game.tscnシーンが開きます。 これがチュートリアルで使用されるメインシーンになります。 シーン全体にすでに配置されているいくつかのノードとシーン、いくつかのバックグラウンドミュージック、およびいくつかのGUI関連の MeshInstance ノードが含まれています。


GUI関連の MeshInstance ノードには既にスクリプトがアタッチされています。 これらのスクリプトは、Viewport ノードのテクスチャを MeshInstance ノードのマテリアルのアルベドテクスチャに設定します。これは、VRプロジェクト内のテキストを表示するために使用されます。 必要に応じて、スクリプト GUI.gd をご覧ください。このチュートリアルでは、MeshInstance ノードでUIを表示するために Viewport ノードを使用する方法については説明しません。

MeshInstance ノードでUIを表示するために Viewport ノードを使用する方法に興味がある場合は、ビューポートをテクスチャとして使用する チュートリアルを参照してください。Viewport をレンダリングテクスチャとして使用する方法と、そのテクスチャを MeshInstance ノードに適用する方法について説明します。


チュートリアルに進む前に、VRに使用されるノードがどのように機能するかについて少し話をしましょう。

ARVROrigin ノードは、VRトラッキングシステムの中心点です。ARVROrigin の位置は、VRシステムが床の「中心」点と見なす位置です。ARVROrigin には、VRシーン内のユーザーのサイズに影響を与える world scale プロパティがあります。 このチュートリアルでは、世界はもともと大きなものだったため、1.4 に設定されています。 前述したように、VRではスケールを比較的一定に保つことが重要です。

ARVRCamera は、プレイヤーのヘッドセットであり、シーンを表示します。ARVRCamera は、VRユーザーの高さによってY軸上でオフセットされます。これは、テレポート移動を追加するときに重要になります。 VRシステムが部屋のトラッキングをサポートしている場合、プレイヤーが移動すると ARVRCamera も移動します。これは、ARVRCameraARVROrigin ノードと同じ位置にあることが保証されていないことを意味します。

ARVRController ノードはVRコントローラーを表します。ARVRController は、ARVROrigin ノードに対するVRコントローラーの位置と回転に従います。 VRコントローラーへのすべての入力は、ARVRController ノードを介して行われます。1ID を持つ ARVRController ノードは左のVRコントローラーを表し、2ID を持つ ARVRController コントローラーは右のVRコントローラーを表します。

To summarize:

  • ARVROrigin ノードはVRトラッキングシステムの中心であり、床に配置されています。
  • ARVRCamera は、プレイヤーのVRヘッドセットであり、シーンを見ることができます。
  • ARVRCamera ノードは、ユーザの高さによってY軸上でオフセットされます。
  • VR システムがルームトラッキングをサポートしている場合、ARVRCamera ノードはプレイヤーの移動に合わせてX軸とZ軸でオフセットされる可能性があります。
  • ARVRController ノードは VR コントローラーを表し、VR コントローラーからの入力をすべて処理します。

VRの開始

VRノードを確認したので、プロジェクトの作業を始めましょう。Game.tscnGame ノードを選択し、Game.gd という新しいスクリプトを作成します。Game.gd ファイルに次のコードを追加します:

extends Spatial

func _ready():
    var VR = ARVRServer.find_interface("OpenVR")
    if VR and VR.initialize():
        get_viewport().arvr = true

        OS.vsync_enabled = false
        Engine.target_fps = 90
        # Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
        # run at the same frame rate as the display, which makes things look smoother in VR!
using Godot;
using System;

public class Game : Spatial
{
    public override void _Ready()
    {
        var vr = ARVRServer.FindInterface("OpenVR");
        if (vr != null && vr.Initialize())
        {
            GetViewport().Arvr = true;

            OS.VsyncEnabled = false;
            Engine.TargetFps = 90;
            // Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
            // run at the same frame rate as the display, which makes things look smoother in VR!
        }
    }
}

このコードの機能について説明します。


_ready 関数では、最初に ARVRServerfind_interface 関数を使用してOpenVR VRインターフェースを取得し、それを VR という変数に割り当てます。ARVRServer がOpenVRという名前のインターフェイスを見つけると、それを返します。それ以外の場合は null を返します。

注釈

OpenVR VRインターフェイスは、デフォルトではGodotに含まれていません。Asset Library または GitHub からOpenVRアセットをダウンロードする必要があります。

次に、コードは2つの条件を組み合わせます。1つは VR 変数がnullではない (if VR) かどうかをチェックし、もう1つはOpenVRインターフェイスが初期化できたかどうかに基づいてブール値を返す初期化関数を呼び出します 。 これらの条件の両方がtrueを返す場合、メインのGodot Viewport をARVRビューポートに変換できます。

If the VR interface initialized successfully, we then get the root Viewport and set the arvr property to true. This will tell Godot to use the initialized ARVR interface to drive the Viewport display.

最後に、VSyncを無効にして、1秒あたりのフレーム数(FPS)がコンピューターモニターによって制限されないようにします。 この後、ほとんどのVRヘッドセットの標準である 90 フレーム/秒でレンダリングするようにGodotに指示します。 VSyncを無効にしないと、通常のコンピューターモニターはVRヘッドセットのフレームレートをコンピューターモニターのフレームレートに制限する場合があります。

注釈

プロジェクト設定の Physics->Common タブで、物理FPSが 90 に設定されています。 これにより、物理エンジンがVRディスプレイと同じフレームレートで実行され、VRでの物理反応がより滑らかに見えます。


Godotがプロジェクト内でOpenVRを起動するために必要なことはこれだけです! 必要に応じて試してみてください。 すべてが機能すると仮定すると、世界中を見ることができるようになります。 ルームトラッキングを備えたVRヘッドセットをお持ちの場合は、ルームトラッキングの制限内でシーン内を移動できます。

コントローラーの作成

../../../_images/starter_vr_tutorial_hands.png

Right now all that the VR user can do is stand around, which isn't really what we are going for unless we are working on a VR film. Lets write the code for the VR controllers. We are going to write all the code for the VR controllers in one go, so the code is rather long. That said, once we are finished you will be able to teleport around the scene, artificially move using the touchpad/joystick on the VR controller, and be able to pick up, drop, and throw RigidBody-based nodes.

最初に、VRコントローラーに使用するシーンを開く必要があります。Left_Controller.tscn または Right_Controller.tscn。ではシーンのセットアップ方法について簡単に説明します。

VRコントローラーシーンの設定方法

どちらのシーンでもルート ノードはARVRControllerノードです。唯一の違いは、Left_Controller シーンの Controller Id プロパティが 1 に設定されているのに対し、Right_Controller プロパティには 2 が設定されている点です。

注釈

ARVRServer は、左右のVRコントローラーにこれら2つのIDを使用しようとします。3つ以上のコントローラー/追跡オブジェクトをサポートするVRシステムの場合、これらのIDの調整が必要になる場合があります。

次は Hand MeshInstance ノードです。このノードは、VRコントローラーが :ref: RigidBody <class_RigidBody>` ノードを保持していないときに使用されるハンドメッシュを表示するために使用されます。Left_Controller シーンの手は左手で、Right_Controller シーンの手は右手です。

Raycast という名前のノードは Raycast ノードで、VRコントローラーがテレポートするときにテレポートする場所を狙うために使用されます。Raycast の長さはY軸で -16 に設定され、手のポインターの指の外側を指すように回転します。Raycast ノードには、単一の子ノード Mesh があります。これは MeshInstance です。これは、テレポート Raycast が狙っている場所を視覚的に示すために使用されます。

Area という名前のノードは、Area ノードで、VRコントローラーのグラブモードが AREA に設定されている場合、RigidBody ベースのノードを取得するために使用されます。Area ノードには、球体 CollisionShape を定義する単一の子ノード CollisionShape があります。VRコントローラーがオブジェクトを保持していない状態でグラブボタンを押すと、Area ノード内の最初の RigidBody ベースのノードが選択されます。

次は Grab_Pos と呼ばれる Position3D ノードです。これは、RigidBody ノードを取得し、VRコントローラーによって保持される位置を定義するために使用されます。

Sleep_Area という名前の大きな Area ノードは、CollisionShape 内のRigidBodyノードのスリープを無効にするために使用されます。これは、RigidBody ノードがスリープ状態になると、VRコントローラーがそれを取得できなくなるためです。Sleep_Area を使用することで、ノード内のすべての RigidBody ノードがスリープできないようにし、VRコントローラーがそれを取得できるようにするコードを記述できます。

AudioStreamPlayer3D と呼ばれる AudioStreamPlayer3D ノードには、オブジェクトがVRコントローラーによってピックアップ、ドロップ、またはスローされたときに使用するサウンドがロードされています。これはVRコントローラーの機能には必要ありませんが、オブジェクトをつかんだり落としたりすることがより自然に感じられます。

最後に、最後のノードは Grab_Cast ノードと唯一の子ノードである Mesh です。Grab_Cast ノードは、VRコントローラーのグラブモードが RAYCAST に設定されている場合、RigidBody ベースのノードを取得するために使用されます。これにより、VRコントローラーは、レイキャストを使用してわずかに手の届かないオブジェクトを取得できます。Mesh ノードは、テレポート Raycast の照準を視覚的に示すために使用されます。

VR コントローラーシーンの設定方法と、ノードを使用してそれらの機能を提供する方法の概要を簡単に説明します。VR コントローラーのシーンを見てきたので、それらを駆動するコードを記述しましょう。

VRコントローラーのコード

シーンのルートノードである Right_Controller または Left_Controller を選択し、VR_Controller.gd という新しいスクリプトを作成します。両方のシーンで同じスクリプトが使用されるため、どちらを最初に使用してもかまいません。VR_Controller.gd を開いた状態で、次のコードを追加します:

ちなみに

このページからコードをコピーして、スクリプト エディタに直接貼り付けることができます。

If you do this, all the code copied will be using spaces instead of tabs.

スクリプトエディタでスペースをタブに変換するには、[編集]メニューをクリックし、[インデントをタブに変換]を選択します。これにより、すべてのスペースがタブに変換されます。タブをスペースに戻すには、[インデントをスペースに変換]を選択します。

var controller_velocity = Vector3(0,0,0)
var prior_controller_position = Vector3(0,0,0)
var prior_controller_velocities = []

var held_object = null
var held_object_data = {"mode":RigidBody.MODE_RIGID, "layer":1, "mask":1}

var grab_area
var grab_raycast

var grab_mode = "AREA"
var grab_pos_node

var hand_mesh
var hand_pickup_drop_sound

var teleport_pos = Vector3.ZERO
var teleport_mesh
var teleport_button_down
var teleport_raycast

# A constant to define the dead zone for both the trackpad and the joystick.
# See https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html
# for more information on what dead zones are, and how we are using them in this project.
const CONTROLLER_DEADZONE = 0.65

const MOVEMENT_SPEED = 1.5

const CONTROLLER_RUMBLE_FADE_SPEED = 2.0

var directional_movement = false


func _ready():
    # Ignore the warnings the from the connect function calls.
    # (We will not need the returned values for this tutorial)
    # warning-ignore-all:return_value_discarded

    teleport_raycast = get_node("RayCast")

    teleport_mesh = get_tree().root.get_node("Game/Teleport_Mesh")

    teleport_button_down = false
    teleport_mesh.visible = false
    teleport_raycast.visible = false

    grab_area = get_node("Area")
    grab_raycast = get_node("Grab_Cast")
    grab_pos_node = get_node("Grab_Pos")

    grab_mode = "AREA"
    grab_raycast.visible = false

    get_node("Sleep_Area").connect("body_entered", self, "sleep_area_entered")
    get_node("Sleep_Area").connect("body_exited", self, "sleep_area_exited")

    hand_mesh = get_node("Hand")
    hand_pickup_drop_sound = get_node("AudioStreamPlayer3D")

    connect("button_pressed", self, "button_pressed")
    connect("button_release", self, "button_released")


func _physics_process(delta):
    if rumble > 0:
        rumble -= delta * CONTROLLER_RUMBLE_FADE_SPEED
        if rumble < 0:
            rumble = 0

    if teleport_button_down == true:
        teleport_raycast.force_raycast_update()
        if teleport_raycast.is_colliding():
            if teleport_raycast.get_collider() is StaticBody:
                if teleport_raycast.get_collision_normal().y >= 0.85:
                    teleport_pos = teleport_raycast.get_collision_point()
                    teleport_mesh.global_transform.origin = teleport_pos


    if get_is_active() == true:
        _physics_process_update_controller_velocity(delta)

    if held_object != null:
        var held_scale = held_object.scale
        held_object.global_transform = grab_pos_node.global_transform
        held_object.scale = held_scale

    _physics_process_directional_movement(delta);


func _physics_process_update_controller_velocity(delta):
    controller_velocity = Vector3(0,0,0)

    if prior_controller_velocities.size() > 0:
        for vel in prior_controller_velocities:
            controller_velocity += vel

        controller_velocity = controller_velocity / prior_controller_velocities.size()

    var relative_controller_position = (global_transform.origin - prior_controller_position)

    controller_velocity += relative_controller_position

    prior_controller_velocities.append(relative_controller_position)

    prior_controller_position = global_transform.origin

    controller_velocity /= delta;

    if prior_controller_velocities.size() > 30:
        prior_controller_velocities.remove(0)


func _physics_process_directional_movement(delta):
    var trackpad_vector = Vector2(-get_joystick_axis(1), get_joystick_axis(0))
    var joystick_vector = Vector2(-get_joystick_axis(5), get_joystick_axis(4))

    if trackpad_vector.length() < CONTROLLER_DEADZONE:
        trackpad_vector = Vector2(0,0)
    else:
        trackpad_vector = trackpad_vector.normalized() * ((trackpad_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE))

    if joystick_vector.length() < CONTROLLER_DEADZONE:
        joystick_vector = Vector2(0,0)
    else:
        joystick_vector = joystick_vector.normalized() * ((joystick_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE))

    var forward_direction = get_parent().get_node("Player_Camera").global_transform.basis.z.normalized()
    var right_direction = get_parent().get_node("Player_Camera").global_transform.basis.x.normalized()

    # Because the trackpad and the joystick will both move the player, we can add them together and normalize
    # the result, giving the combined movement direction
    var movement_vector = (trackpad_vector + joystick_vector).normalized()

    var movement_forward = forward_direction * movement_vector.x * delta * MOVEMENT_SPEED
    var movement_right = right_direction * movement_vector.y * delta * MOVEMENT_SPEED

    movement_forward.y = 0
    movement_right.y = 0

    if (movement_right.length() > 0 or movement_forward.length() > 0):
        get_parent().global_translate(movement_right + movement_forward)
        directional_movement = true
    else:
        directional_movement = false


func button_pressed(button_index):
    if button_index == 15:
        _on_button_pressed_trigger()

    if button_index == 2:
        _on_button_pressed_grab()

    if button_index == 1:
        _on_button_pressed_menu()


func _on_button_pressed_trigger():
    if held_object == null:
        if teleport_mesh.visible == false:
            teleport_button_down = true
            teleport_mesh.visible = true
            teleport_raycast.visible = true
    else:
        if held_object is VR_Interactable_Rigidbody:
            held_object.interact()


func _on_button_pressed_grab():
    if teleport_button_down == true:
        return

    if held_object == null:
        _pickup_rigidbody()
    else:
        _throw_rigidbody()

    hand_pickup_drop_sound.play()


func _pickup_rigidbody():
    var rigid_body = null

    if grab_mode == "AREA":
        var bodies = grab_area.get_overlapping_bodies()
        if len(bodies) > 0:
            for body in bodies:
                if body is RigidBody:
                    if !("NO_PICKUP" in body):
                        rigid_body = body
                        break

    elif grab_mode == "RAYCAST":
        grab_raycast.force_raycast_update()
        if (grab_raycast.is_colliding()):
            var body = grab_raycast.get_collider()
            if body is RigidBody:
                if !("NO_PICKUP" in body):
                    rigid_body = body


    if rigid_body != null:

        held_object = rigid_body

        held_object_data["mode"] = held_object.mode
        held_object_data["layer"] = held_object.collision_layer
        held_object_data["mask"] = held_object.collision_mask

        held_object.mode = RigidBody.MODE_STATIC
        held_object.collision_layer = 0
        held_object.collision_mask = 0

        hand_mesh.visible = false
        grab_raycast.visible = false

        if held_object is VR_Interactable_Rigidbody:
            held_object.controller = self
            held_object.picked_up()


func _throw_rigidbody():
    if held_object == null:
        return

    held_object.mode = held_object_data["mode"]
    held_object.collision_layer = held_object_data["layer"]
    held_object.collision_mask = held_object_data["mask"]

    held_object.apply_impulse(Vector3(0, 0, 0), controller_velocity)

    if held_object is VR_Interactable_Rigidbody:
        held_object.dropped()
        held_object.controller = null

    held_object = null
    hand_mesh.visible = true

    if grab_mode == "RAYCAST":
        grab_raycast.visible = true


func _on_button_pressed_menu():
    if grab_mode == "AREA":
        grab_mode = "RAYCAST"
        if held_object == null:
            grab_raycast.visible = true

    elif grab_mode == "RAYCAST":
        grab_mode = "AREA"
        grab_raycast.visible = false


func button_released(button_index):
    if button_index == 15:
        _on_button_released_trigger()


func _on_button_released_trigger():
    if teleport_button_down == true:

        if teleport_pos != null and teleport_mesh.visible == true:
            var camera_offset = get_parent().get_node("Player_Camera").global_transform.origin - get_parent().global_transform.origin
            camera_offset.y = 0

            get_parent().global_transform.origin = teleport_pos - camera_offset

        teleport_button_down = false
        teleport_mesh.visible = false
        teleport_raycast.visible = false
        teleport_pos = null


func sleep_area_entered(body):
    if "can_sleep" in body:
        body.can_sleep = false
        body.sleeping = false


func sleep_area_exited(body):
    if "can_sleep" in body:
        # Allow the CollisionBody to sleep by setting the "can_sleep" variable to true
        body.can_sleep = true

これはかなりの量のコードです。コードがステップごとに行うことを見ていきましょう。

VRコントローラーコードの説明

First, let's go through all the class variables in the script:

  • controller_velocity: VRコントローラーの速度の大まかな近似値を保持する変数。
  • prior_controller_position: 3D空間でVRコントローラーの最後の位置を保持する変数。
  • prior_controller_velocities: 最後に計算された30個のVRコントローラー速度を保持する配列。これは、時間の経過とともに速度計算を滑らかにするために使用されます。
  • held_object: VRコントローラーが保持しているオブジェクトへの参照を保持する変数。VRコントローラーがオブジェクトを保持していない場合、この変数は null になります。
  • held_object_data: VRコントローラーによって保持されている RigidBody ノードのデータを保持するdictionary。これは、RigidBody のデータが保持されなくなったときにリセットするために使用されます。
  • grab_area: VRコントローラーでオブジェクトを取得するために使用される Area ノードを保持する変数。
  • grab_raycast: VRコントローラーでオブジェクトを取得するために使用される Raycast ノードを保持する変数。
  • grab_mode: VRコントローラーが使用しているグラブモードを定義する変数。このチュートリアルでオブジェクトを取得するモードは、AREARAYCAST の2つだけです。
  • grab_pos_node: 保持されたオブジェクトの位置と回転を更新するために使用されるノードを保持する変数。
  • hand_mesh: VRコントローラーのハンドメッシュを含む MeshInstance ノードを保持する変数。このメッシュは、VRコントローラーが何も保持していないときに表示されます。
  • hand_pickup_drop_sound: ピックアップ/ドロップサウンドを含む AudioStreamPlayer3D ノードを保持する変数。
  • teleport_pos: VRコントローラーがプレイヤーをテレポートするときにプレイヤーがテレポートされる位置を保持する変数。
  • teleport_mesh: プレイヤーのテレポート先を示すために使用される MeshInstance ノードを保持する変数。
  • teleport_button_down: コントローラーのテレポートボタンが押されているかどうかを追跡するために使用される変数。これは、このVRコントローラーがプレイヤーをテレポートしようとしているかどうかを検出するために使用されます。
  • teleport_raycast: テレポート位置の計算に使用される Raycast ノードを保持する変数。このノードには、照準の「レーザーサイト」 として機能する MeshInstance もあります。
  • CONTROLLER_DEADZONE: トラックパッドとVRコントローラーのジョイスティックの両方のデッドゾーンを定義する定数。詳細については、以下の注を参照してください。
  • MOVEMENT_SPEED: トラックパッド/ジョイスティックを使用して人為的に移動するときにプレイヤーが移動する速度を定義する定数。
  • CONTROLLER_RUMBLE_FADE_SPEED: VRコントローラーのランブルフェードの速度を定義する定数。
  • directional_movement: このVRコントローラーがタッチパッド/ジョイスティックを使用してプレイヤーを動かしているかどうかを保持する変数。

注釈

ジョイパッド / コントローラーのデッドゾーンの処理方法について、すべてを説明する素晴らしい記事がここにあります (英語)。

VRコントローラーのジョイスティック/タッチパッド用にその記事で提供されているスケーリングされた放射状デッドゾーンコードの翻訳バージョンを使用しています。この記事は素晴らしい読み物です。ぜひご覧ください!

これはかなりの数のクラス変数です。それらのほとんどは、コード全体で必要なノードへの参照を保持するために使用されます。次に _ready 関数から始めて、関数を見てみましょう。


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

まず、 connect 関数によって返された値を使用しないことに関する警告を黙らせるようにGodotに指示します。このチュートリアルでは戻り値は必要ありません。

次に、テレポートの位置を決定するために使用する Raycast ノードを取得し、それを teleport_raycast 変数に割り当てます。次に、プレイヤーがテレポートする場所を示すために使用する MeshInstance ノードを取得します。テレポートに使用しているノードは、Game シーンの子です。これにより、テレポートメッシュノードがVRコントローラーの変更の影響を受けなくなり、テレポートメッシュを両方のVRコントローラーで使用できるようになります。

次に、teleport_button_down 変数がfalseに設定され、teleport_mesh.visibleteleport_raycast.visible が共に false に設定されます。これは、プレイヤーをテレポートするのではなく、初期状態にプレイヤーをテレポートするための変数を設定します。

次に、コードは grab_area ノード、grab_raycast ノード、および grab_pos_node ノードを取得し、それらをすべて後で使用するためにそれぞれの変数に割り当てます。

次に grab_modeAREA に設定されているため、VRコントローラーのグラブ/グリップボタンが押されると、VRコントローラーは grab_area で定義された Area ノードを使用してオブジェクトをつかもうとします。また、grab_raycast ノードの visible プロパティを false に設定して、grab_raycastlaser sight 子ノードが表示されないようにします。

その後、VRコントローラーの Sleep_Area ノードからの body_entered および body_exited シグナルを sleep_area_entered および sleep_area_exited 関数に接続します。sleep_area_entered および sleep_area_exited 関数は、VRコントローラーの近くでスリープすることができない RigidBody ノードを作成するために使用されます。

次に、hand_mesh および hand_pickup_drop_sound ノードが取得され、後で使用するためにそれぞれの変数に割り当てられます。

最後に、VRコントローラーが拡張する ARVRController ノードの button_pressed および button_release シグナルは 、それぞれ button_pressed および button_released 関数に接続されます。つまり、VRコントローラーのボタンが押されたり離されたりすると、このスクリプトで定義されている button_pressed または button_released 関数が呼び出されます。

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

まず、rumble 変数がゼロ以上かどうかを確認します。ARVRController ノードのプロパティである rumble 変数がゼロより大きい場合、VRコントローラーが鳴ります。

rumble 変数がゼロより大きい場合、 CONTROLLER_RUMBLE_FADE_SPEED にデルタを掛けることにより、毎秒 CONTROLLER_RUMBLE_FADE_SPEED によってrumbleを減らします。次に、rumble がゼロ未満かどうかをチェックする if 条件式があり、その値がゼロ未満の場合に rumble をゼロに設定します。

この小さなコードのセクションは、VRコントローラーの振動を減らすために必要なすべてです。 ``rumble``に値を設定すると、このコードは時間とともに自動的にフェードします。


コードの最初のセクションでは、teleport_button_down 変数が true に等しいかどうかを確認します。これは、このVRコントローラーがテレポートしようとしていることを意味します。

teleport_button_downtrue に等しい場合、force_raycast_update 関数を使用して teleport_raycast Raycast ノードを強制的に更新します。force_raycast_update 関数は、Raycast ノード内のプロパティを物理世界の最新バージョンで更新します。

次に、コードは teleport_raycastis_colliding 関数をチェックすることにより、teleport_raycast が何かと衝突したかどうかを確認します。Raycast が何かと衝突した場合、レイキャストが衝突した PhysicsBodyStaticBody かどうかを確認します。次に、レイキャストによって返されたコリジョン法線ベクトルがY軸の `` 0.85`` 以上であるかどうかを確認します。

注釈

これは、ユーザーがRigidBodyノードにテレポートできないようにし、床のような表面にプレイヤーがテレポートできるようにするためです。

これらすべての条件が満たされている場合、teleport_raycastget_collision_point 関数に teleport_pos 変数を割り当てます。これは、レイスペースがワールド空間で衝突した位置に teleport_pos を割り当てます。それから teleport_meshteleport_pos に保存されているワールド位置に移動します。

コードのこのセクションは、プレイヤーがテレポートレイキャストで狙っている位置を取得し、テレポートメッシュを更新し、テレポートボタンを離したときにユーザーがテレポートする場所を視覚的に更新します。


コードの次のセクションでは、最初に、ARVRController で定義される get_is_active 関数を介してVRコントローラーがアクティブかどうかを確認します。VRコントローラーがアクティブな場合、_ physics_process_update_controller_velocity 関数を呼び出します。

_physics_process_update_controller_velocity 関数は、位置の変化を通してVRコントローラーの速度を計算します。それは完全ではありませんが、このプロセスはVRコントローラーの速度の大まかな概念を取得します。これは、このチュートリアルの目的には適しています。


コードの次のセクションでは、held_object 変数が null と等しくないかどうかを確認して、VRコントローラーがオブジェクトを保持しているかどうかを確認します。

VRコントローラーがオブジェクトを保持している場合、最初に held_scale と呼ばれる一時変数にそのスケールを保存します。次に、保持されたオブジェクトの global_transformheld_object ノードの global_transform に設定します。これにより、保持されたオブジェクトは、ワールド空間の grab_pos_node ノードと同じ位置、回転、スケールになります。

ただし、保持されたオブジェクトが取得されたときにスケールが変更されないようにするには、held_object ノードの scale プロパティを held_scale に戻す必要があります。

コードのこのセクションは、保持されたオブジェクトをVRコントローラーと同じ位置と回転に保ち、VRコントローラーとの同期を維持します。


Finally, the last section of code simply calls the _physics_process_directional_movement function. This function contains all the code for moving the player when the touchpad/joystick on the VR controller moves.

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

最初に、この関数は controller_velocity 変数をゼロにリセットします Vector3


次に、prior_controller_velocities 配列に保存/キャッシュされたVRコントローラーの速度が保存されているかどうかを確認します。size() 関数が 0 より大きい値を返すかどうかを確認することでこれを行います。prior_controller_velocities 内にキャッシュされた速度がある場合、for ループを使用して、保存されている各速度を反復処理します。

キャッシュされた速度のそれぞれについて、その値を controller_velocity に加算するだけです。コードが prior_controller_velocities のキャッシュされたすべての速度を通過したら、controller_velocityprior_controller_velocities 配列のサイズで除算します。これにより、合成された速度値が得られます。これにより、以前の速度が考慮され、コントローラーの速度の方向がより正確になります。


次に、最後の _physics_process 関数呼び出し以降にVRコントローラーが取った位置の変化を計算します。これを行うには、VRコントローラーのグローバル位置 global_transform.origin から prior_controller_position を減算します。これにより、prior_controller_position の位置からVRコントローラーの現在の位置を指す Vector3 が得られ、これを relative_controller_position という変数に保存します。

次に、位置の変更を controller_velocity に加算して、位置の最新の変更が速度計算で考慮されるようにします。次に、relative_controller_positionprior_controller_velocities に加算して、次回のVRコントローラーの速度計算で考慮できるようにします。

次に、prior_controller_position がVRコントローラーのグローバル位置 global_transform.origin で更新されます。次に、controller_velocitydelta で除算し、速度を上げて、期待通りの結果をもたらしますが、経過時間との相対的な関係を維持します。これは完全な解決策ではありませんが、ほとんどの場合、結果は適切に見えます。このチュートリアルの目的には十分です。

最後に、関数は、size() 関数が 30 より大きい値を返すかどうかをチェックすることにより、prior_controller_velocities30 を超える速度をキャッシュしているかどうかを確認します。prior_controller_velocities30 を超えるキャッシュ速度が保存されている場合、remove 関数を呼び出してインデックス位置 0 を渡すことで、最も古いキャッシュ速度を削除します。


この関数が最終的に行うことは、最後の30回の _physics_process 呼び出しでのVRコントローラーの相対的な位置の変化を計算することにより、VRコントローラーの速度の大まかなアイデアを得るということです。これは完全ではありませんが、VRコントローラーが3D空間でどれだけ速く動いているかについての適切なアイデアを提供します。

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

最初に、この関数はトラックパッドとジョイスティックの軸を取得し、それらをそれぞれ trackpad_vector および joystick_vector と呼ばれる Vector2 変数に割り当てます。

注釈

VRヘッドセットとコントローラーによっては、ジョイスティックやタッチパッドのインデックス値の再マッピングが必要になる場合があります。このチュートリアルの入力は、Windows Mixed Realityヘッドセットのインデックス値です。

次に、trackpad_vectorjoystick_vector のデッドゾーンを考慮します。このコードは以下の記事で詳しく説明されていますが、コードがC#からGDScriptに変換されたので若干の変更が加えられています。

trackpad_vector 変数と joystick_vector 変数のデッドゾーンが考慮されると、コードは ARVRCamera のグローバルトランスフォームに関連する順方向および右方向のベクトルを取得します。これが行うことは、ワールド空間でユーザーカメラ ARVRCamera の回転に対して前方および右側を指すベクトルを提供することです。これらのベクトルは、ローカル空間モード ボタンを有効にしてGodotエディタでオブジェクトを選択すると、青と赤の矢印の同じ方向を指します。順方向ベクトルは forward_direction と呼ばれる変数に保存され、右方向ベクトルは right_direction と呼ばれる変数に保存されます。

次に、コードは trackpad_vector 変数と joystick_vector 変数を一緒に加算し、normalized 関数を使用して結果を正規化します。これにより、両方の入力デバイスの移動方向が結合されるため、ユーザーを移動するために単一の Vector2 を使用できます。合成された方向を movement_vector と呼ばれる変数に割り当てます。

次に、forward_direction に保存されている前方方向を基準にして、ユーザーが前方に移動する距離を計算します。これを計算するために、forward_directionmovement_vector.xdeltaMOVEMENT_SPEED を掛けます。これにより、トラックパッド/ジョイスティックを前方または後方に押したときにユーザーが前(後)に移動する距離がわかります。これを movement_forward という変数に割り当てます。

right_direction に保存されている右の方向を基準にして、ユーザーが右に移動する距離についても同様の計算を行います。ユーザーが右に移動する距離を計算するには、right_directionmovement_vector.ydeltaMOVEMENT_SPEED を乗算します。これにより、トラックパッド/ジョイスティックを右または左に押したときにユーザーが(左)右に移動する距離がわかります。これを movement_right という変数に割り当てます。

次に、movement_forwardmovement_rightY 軸上の動きを、それらの Y 値を 0 に割り当てることで削除します。これは、ユーザーがトラックパッドまたはジョイスティックを動かすだけで飛行/落下できないようにするためです。これを行わないと、プレイヤーは向いている方向に飛ぶことができます。

最後に、movement_right または movement_forwardlength 関数が 0 より大きいかどうかを確認します。そうである場合、ユーザーを移動する必要があります。ユーザーを移動するには、get_parent().global_translate を使用して ARVROrigin <class_ARVROrigin> ノードへのグローバル変換を実行し、``movement_forward` 変数を加算した movement_right 変数を渡します。これにより、VRヘッドセットの回転に対して、トラックパッド/ジョイスティックが指している方向にプレイヤーが移動します。また、directional_movement 変数を true に設定して、このVRコントローラーがプレイヤーを動かしていることをコードが認識できるようにします。

movement_right または movement_forwardlength 関数が 0 以下の場合、directional_movement 変数を false に設定するだけで、コードはこのVRコントローラーがプレイヤーを動かしていないことを認識します。


この関数が最終的に行うことは、VRコントローラーのトラックパッドとジョイスティックから入力を受け取り、プレイヤーが押している方向にプレイヤーを移動させることです。動きはVRヘッドセットの回転に関連するため、プレイヤーが前方に押して頭を左に回すと、左に動きます。

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

この関数は、押されたばかりのVRボタンがこのプロジェクトで使用されているVRボタンのいずれかと等しいかどうかを確認します。button_index 変数は、_ready 関数で接続した ARVRControllerbutton_pressed シグナルによって渡されます。

このプロジェクトで探しているボタンは、トリガーボタン、グラブ/グリップボタン、メニューボタンの3つだけです。

注釈

VRヘッドセットとコントローラーによっては、これらのボタンインデックス値を再マッピングする必要がある場合があります。このチュートリアルの入力は、Windows Mixed Realityヘッドセットのインデックス値です。

最初に、button_index15 に等しいかどうかを確認します。これはVRコントローラーのトリガーボタンにマップする必要があります。押されたボタンがトリガーボタンである場合、_on_button_pressed_trigger 関数が呼び出されます。

button_index2 に等しい場合、グラブボタンが押されたばかりです。押されたボタンがグラブボタンの場合、_on_button_pressed_grab 関数が呼び出されます。

最後に、button_index1 に等しい場合、メニューボタンが押されたばかりです。押されたボタンがメニューボタンの場合、_on_button_pressed_menu 関数が呼び出されます。

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

最初に、この関数は held_objectnull に等しいかどうかを確認することにより、VRコントローラーが何かを保持していないかどうかを確認します。 VRコントローラーが何も保持していない場合、VRコントローラーのトリガープレスはテレポート用であると想定します。次に、teleport_mesh.visiblefalse に等しいことを確認します。これを使用して、他のVRコントローラーがテレポートしようとしているかどうかを確認します。他のVRコントローラーがテレポートしている場合は teleport_mesh が表示されるためです。

teleport_mesh.visiblefalse に等しい場合、このVRコントローラーでテレポートできます。teleport_button_down 変数を true に設定し、teleport_mesh.visible および teleport_raycast.visible を共に true``に設定します。これにより、\ ``_physics_process のコードにこのVRコントローラーがテレポートすることを通知し、teleport_mesh を表示して、ユーザーがテレポートする場所を認識させ、 teleport_raycast を表示して、プレイヤーがテレポート位置を狙うために使用できる「レーザーサイト」を表示します。


held_objectnull と等しくない場合、VRコントローラーは何かを保持しています。次に、保持されているオブジェクト held_objectVR_Interactable_Rigidbody というクラスを拡張しているかどうかを確認します。まだ VR_Interactable_Rigidbody を作成していませんが、VR_Interactable_Rigidbody は、プロジェクトのすべての special/custom RigidBody ベースのノードで使用するカスタムクラスになります。

ちなみに

ご心配なく。VR_Interactable_Rigidbody については、このセクションの後で取りあげます!

held_objectVR_Interactable_Rigidbody から拡張される場合、interact 関数を呼び出すため、トリガーが押され、オブジェクトがVRコントローラーによって保持されているときに、その保持されたオブジェクトが行うべきことを実行できます。

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

最初に、この関数は teleport_button_downtrue に等しいかどうかを確認します。もしそうなら、それは return を呼び出します。これは、ユーザーがテレポート中にオブジェクトを取得できないようにするためです。

次に、held_objectnull に等しいかどうかを確認することにより、VRコントローラーが現在何も保持していないかどうかを確認します。 VRコントローラーが何も保持していない場合、_pickup_rigidbody 関数が呼び出されます。VRコントローラーが何かを保持している場合、held_objectnull と等しくない場合、_throw_rigidbody 関数が呼び出されます。

最後に、ピックアップ/ドロップサウンドは、hand_pickup_drop_soundplay 関数を呼び出すことで再生されます。

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

最初に、関数は rigid_body と呼ばれる変数を作成します。これは、ピックアップするRigidBodyがあると仮定して、VRコントローラーがピックアップする RigidBody を格納するために使用します。


次に、関数は grab_mode 変数が AREA と等しいかどうかを確認します。そうである場合、get_overlapping_bodies 関数を使用して、grab_area 内のすべての PhysicsBody ノードを取得します。この関数は PhysicsBody ノードの配列を返します。PhysicsBody の配列を bodies と呼ばれる新しい変数に割り当てます。

次に、bodies 変数の長さが 0 を超えているかどうかを確認します。そうである場合、forループを使用して bodyPhysicsBody ノードのそれぞれを調べます。

PhysicsBody ノードにごとに、if body is RigidBody を使用して RigidBody ノードもしくは、それを拡張したものかを確認し、PhysicsBody ノードが :ref` RigidBody <class_RigidBody> ` ノードもしくは、それを拡張したものの場合は true を返します。オブジェクトが RigidBody の場合、ボディに NO_PICKUP という変数/定数が定義されていないことを確認します。これは、ピックアップできない RigidBody ノードが必要な場合、NO_PICKUP という定数/変数を定義するだけで、VRコントローラーがそれを拾えなくなります。RigidBody ノードに NO_PICKUP という名前で定義された変数/定数がない場合、refid_body 変数を RigidBody ノードに割り当て、forループを中断します。

コードのこのセクションは、grab_area 内のすべてのphysicsボディを通過し、NO_PICKUP という名前の変数/定数を持たない最初の RigidBody ノードを取得します。それを rigid_body 変数に割り当てるので、この関数の後半で追加の後処理を行うことができます。


grab_mode 変数が AREA と等しくない場合、代わりに RAYCAST と等しいかどうかを確認します。RAYCAST と等しい場合、force_raycast_update 関数を使用して grab_raycast ノードを強制的に更新します。force_raycast_update 関数は、物理世界の最新の変更内容で Raycast を更新します。次に、 is_colliding 関数を使用して grab_raycast ノードが何かと衝突したかどうかを確認します。これは、Raycast が何かにヒットした場合にtrueを返します。

grab_raycast が何かにヒットした場合、get_collider 関数を使用して PhysicsBody ノードにヒットします。次にコードは、ノードヒットが PhysicsBody の場合に if body is RigidBody を使用して、PhysicsBody ノードが RigidBody ノード、もしくはそれを拡張したノードの場合に true を返します。次に、コードは RigidBody ノードに NO_PICKUP という名前の変数がないかどうかを確認し、ない場合は rigid_body 変数に RigidBody ノードを割り当てます。

コードのこのセクションが行っていることは、grab_raycast Raycast ノードを送信し、NO_PICKUP という名前の変数/定数を持っていない RigidBody ノードと衝突したかどうかをチェックすることです。`` NO_PICKUP`` なしのRigidBodyと衝突した場合、そのノードを rigid_body 変数に割り当てるため、この関数で後から追加の後処理を行うことができます。


コードの最後のセクションでは、最初に rigid_bodynull と等しくないかどうかを確認します。rigid_bodynull に等しくない場合、VRコントローラーは RigidBody ベースのノードを検出できます。

ピックアップするVRコントローラーがある場合は、rigid_body に保存されている RigidBody ノードに held_object を割り当てます。次に、modelayer、および mask をそれぞれの値のキーとして使用して、RigidBody <class_RigidBody>`` ノードの modecollision_layer、および collision_maskheld_object_data に保存します。これは、後でオブジェクトがVRコントローラーによってドロップされたときにそれらを再適用できるようにするためです。

次に、RigidBody のmodeを MODE_STATIC に設定します。これは collision_layer をゼロに設定し、collision_mask もゼロに設定します。これにより、保持されている RigidBody がVRコントローラーで保持されている場合、物理世界の他のオブジェクトと対話できなくなります。

次に、visible プロパティを false に設定することにより、hand_mesh MeshInstance を非表示にします。これは、手がホールドされたオブジェクトの邪魔にならないようにするためです。同様に、grab_raycast``「レーザーサイト」は、\ ``visible プロパティを false に設定することで非表示になります。

次に、コードは、保持されているオブジェクトが VR_Interactable_Rigidbody というクラスを拡張しているかどうかを確認します。その場合、held_objectcontroller という変数を self に設定し、held_objectpicked_up 関数を呼び出します。まだ VR_Interactable_Rigidbody を作成していませんが、これが行うことは、picked_up 関数を呼び出すことで、controller 変数に保存されているVRコントローラーによってコントローラーへの参照が 保持されていることを VR_Interactable_Rigidbody クラスに伝えることです。

ちなみに

ご心配なく。VR_Interactable_Rigidbody については、このセクションの後で取りあげます!

このチュートリアルシリーズのパート2を完了した後、コードはより意味のあるものになるはずです。そこでは実際に VR_Interactable_Rigidbody を使用します。

コードのこのセクションは、もしも gurb Area または Raycast を使用して RigidBody を検出した場合、VRコントローラーでそれを運べるようにセットアップします。

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

最初に、関数は held_object 変数が null に等しいかどうかを確認することにより、VRコントローラーがオブジェクトを保持していないかどうかを確認します。そうである場合は、単に return を呼び出すため、何も起こりません。必ず、オブジェクトが保持されている場合にのみ _throw_rigidbody 関数を呼び出す必要があります。このチェックは、奇妙なことが発生した場合に、この関数が期待どおりに反応することを保証します。

VRコントローラーがオブジェクトを保持しているかどうかを確認した後、それを想定し、保存された RigidBody データを保持されたオブジェクトに戻します。held_object_data dictionaryに保存されている modelayer および mask データを取得し、それを held_object のオブジェクトに再適用します。これにより、RigidBody がピックアップされる前の状態に戻ります。

次に、held_objectapply_impulse を呼び出して、RigidBody がVRコントローラーの速度 controller_velocity の方向にスローされるようにします。

次に、保持されているオブジェクトが VR_Interactable_Rigidbody というクラスを拡張しているかどうかを確認します。もしそうなら、 held_objectdropped と呼ばれる関数を呼び出し、held_object.controllernull に設定します。まだ VR_Interactable_Rigidbody を作成していませんが、これは droppped 関数を呼び出すので、RigidBody はドロップ時に必要なことを何でも行うことができ、RigidBody が保持されていないことがわかるように、controller 変数を null に設定します。

ちなみに

ご心配なく。VR_Interactable_Rigidbody については、このセクションの後で取りあげます!

このチュートリアルシリーズのパート2を完了した後、コードはより意味のあるものになるはずです。そこでは実際に VR_Interactable_Rigidbody を使用します。

held_objectVR_Interactable_Rigidbody を拡張しているかどうかに関係なく、held_objectnull に設定して、VRコントローラーがもう何も保持していないことを認識します。 VRコントローラーはもう何も保持していないので、hand_mesh.visible をtrueに設定することで hand_mesh を可視化します。

最後に、grab_mode 変数が RAYCAST に設定されている場合、grab_raycast.visibletrue に設定するため、grab_raycastRaycast「レーザーサイト」が表示されます。

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

最初に、この関数は grab_mode 変数が AREA と等しいかどうかを確認します。もしそうなら、それは grab_modeRAYCAST に設定します。次に、held_objectnull に等しいかどうかを確認することにより、VRコントローラーが何も保持していないかどうかを確認します。 VRコントローラーが何も保持していない場合は、grab_raycast.visibletrue に設定されるため、grabレイキャストの「レーザーサイト」が表示されます。

grab_mode 変数が AREA と等しくない場合、変数が RAYCAST と等しいかどうかを確認します。そうである場合は、grab_modeAREA に設定し、grab_raycast.visiblefalse に設定して、grubレイキャストの「レーザーサイト」が表示されないようにします。

コードのこのセクションは、グラブ/グリップボタンが押されたときにVRコントローラーが RigidBody ベースのノードを保持する方法を単に変更します。grab_modeAREA に設定されている場合、grab_areaArea ノードは RigidBody ノードの検出に使用されますが、grab_modeRAYCAST に設定されている場合、grab_raycastRaycast ノードは RigidBody ノードの検出に使用されます。

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

この関数のコードの唯一のセクションは、リリースされたばかりのボタンのインデックス button_index15 に等しいかどうかを確認します。これはVRコントローラーのトリガーボタンにマップする必要があります。 button_index 変数は、ARVRControllerbutton_release シグナルによって渡されます。これは _ready 関数で接続しています。

トリガーボタンがちょうど離されたときに、_on_button_released_trigger 関数が呼び出されます。

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

この関数のコードの唯一のセクションは、最初に teleport_button_down 変数が true に等しいかどうかを確認することにより、VRコントローラーがテレポートしようとしているかどうかを確認します。

teleport_button_down 変数が true に等しい場合、コードはテレポート位置が設定されているかどうか、テレポートメッシュが表示されているかどうかをチェックします。これは、teleport_posnull と等しくないかどうか、および teleport_mesh.visibletrue と等しいかどうかを確認することで行います。

テレポート位置セットがあり、テレポートメッシュが表示されている場合、コードはカメラから ARVROrigin ノードへのオフセットを計算します。これはVRコントローラーの親ノードと見なされます。オフセットを計算するために、Player_Camera ノードのグローバル位置(global_transform.origin)から ARVROrigin のグローバル位置が減算されます。これにより、ARVROrigin から ARVRCamera を指すベクトルが生成され、これを camera_offset という変数に格納します。

オフセットを知る必要がある理由は、一部のVRヘッドセットはルームトラッキングを使用しているためです。プレイヤーのカメラは ARVROrigin ノードからオフセットできます。このため、テレポートするとき、ルームトラッキングによって作成されたオフセットを保持して、プレイヤーがテレポートするときに、ルームトラッキングによって作成されたオフセットが適用されないようにします。これがないと、部屋に移動してテレポートした場合、テレポートしたい位置に現れるのではなく、ARVROrigin ノードからの距離によって位置がオフセットされます。

VRカメラからVR原点までのオフセットがわかったので、Y 軸の違いを取り除く必要があります。これは、ユーザーの身長に基づいてオフセットしたくないためです。これを行わなかった場合、プレイヤーの頭をテレポートするとき、地面と水平になります。

その後、ARVROriginノードのグローバル位置(global_transform.origin)を teleport_pos に格納された位置に設定し、そこから camera_offset を減算することにより、プレイヤーを「'テレポート」できます。これにより、プレイヤーがテレポートされ、ルームトラッキングオフセットが削除されるため、ユーザーはテレポート時に希望する場所に正確に表示されます。

最後に、VRコントローラーがユーザーをテレポートしたかどうかに関係なく、テレポート関連の変数をリセットします。teleport_button_downfalse に設定され、teleport_mesh.visiblefalse に設定されているため、メッシュは非表示になり、teleport_raycast.visiblefalse に設定され、そして teleport_posnull に設定されます。

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

この関数のコードの唯一のセクションは、Sleep_Area ノードに入った PhysicsBody ノードに can_sleep という変数があるかどうかを確認します。存在する場合、can_sleep 変数を false に設定し、sleeping 変数を false に設定します。

これを行わないと、VRコントローラーが PhysicsBody ノードと同じ位置にある場合でも、PhysicsBody ノードをVRコントローラーで取得することはできません。これを回避するには、VRコントローラーの近くにある PhysicsBody ノードを単に 「ウェイクアップ」します。

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

この関数のコードの唯一のセクションは、Sleep_Area ノードに入った PhysicsBody ノードに can_sleep という変数があるかどうかを確認します。存在する場合、can_sleep 変数を true に設定します。

これにより、Sleep_Area から出る RigidBody ノードが再びスリープ状態になり、パフォーマンスが節約されます。


わかりました、ふう!これは大量のコードでした!同じスクリプト VR_Controller.gd を他のVRコントローラーシーンに追加して、両方のVRコントローラーが同じスクリプトを持つようにします。

あとは、プロジェクトをテストする前に1つのことを行うだけです!現在、VR_Interactable_Rigidbody というクラスを参照していますが、まだ定義していません。このチュートリアルでは VR_Interactable_Rigidbody を使用しませんが、プロジェクトを実行できるようにすばやく作成してみましょう。

対話可能なVRオブジェクトの基本クラスを作成する

Script タブを開いたまま、VR_Interactable_Rigidbody.gd という新しいGDScriptを作成します。

ちなみに

[ファイル] -> [新規スクリプト...] を押すことで、Script タブでGDScriptを作成できます。

VR_Interactable_Rigidbody.gd を開いたら、次のコードを追加します:

extends RigidBody

# (Ignore the unused variable warning)
# warning-ignore:unused_class_variable
var controller = null


func _ready():
    pass


func interact():
    pass


func picked_up():
    pass


func dropped():
    pass

このスクリプトを簡単に見ていきましょう。


まず、class_name VR_Interactable_Rigidbody でスクリプトを開始します。これは、このGDScriptが VR_Interactable_Rigidbody と呼ばれる新しいクラスであることをGodotに伝えます。これにより、スクリプトを直接ロードしたり特別なことをしたりする必要もなしに、すべての組み込みGodotクラスと同様に、ノードを他のスクリプトファイルの VR_Interactable_Rigidbody クラスと突き合せることができます。

次は controller と呼ばれるクラス変数です。controller は、現在オブジェクトを保持しているVRコントローラーへの参照を保持するために使用されます。VRコントローラーがオブジェクトを保持していない場合、controller 変数は null になります。 VRコントローラーへの参照が必要な理由は、保持されたオブジェクトが controller_velocity のようなVRコントローラー固有のデータにアクセスできるようにするためです。

最後に、4つの関数があります。_ready 関数はGodotによって定義され、オブジェクトが VR_Interactable_Rigidbody のシーンに追加されたときに行うべきことは何もないので、単に pass を持つだけです。

interact 関数は、オブジェクトが保持されているときにVRコントローラーの対話ボタン(この場合はトリガー)が押されたときに呼び出されるスタブ関数です。

ちなみに

スタブ関数は、定義されているがコードを持たない関数です。スタブ関数は通常、上書きまたは拡張されるように設計されています。このプロジェクトでは、スタブ関数を使用しているため、すべての対話可能な RigidBody オブジェクト全体で一貫したインターフェイスがあります。

picked_up および dropped 関数は、オブジェクトがVRコントローラーによってピックアップおよびドロップされるときに呼び出されるスタブ関数です。


今のところ私たちが行うべきことはそれだけです!このチュートリアルシリーズの次のパートでは、相互作用可能な特別な RigidBody オブジェクトの作成を開始します。

基本クラスが定義されたので、VRコントローラーのコードが機能するはずです。先に進み、もう一度ゲームを試してください。タッチパッドを押すとテレポートでき、グラブ/グリップボタンを使用してオブジェクトをつかんで投げることができます。

さて、トラックパッドやジョイスティックを使用して移動してみたくなるかもしれませんが、乗り物酔いになる場合があります!

この乗り物酔いを感じる主な理由の1つは、体は動いていないのに、視力は動いていることを伝えて来るからです。この信号の衝突により、気分が悪くなることがあります。 VRでの移動中の乗り物酔いを軽減するために、ビネットシェーダーを追加しましょう!

乗り物酔いの軽減

注釈

VRで乗り物酔いを減らす方法はたくさんありますが、乗り物酔いを減らす完璧な方法はありません。移動と乗り物酔いの軽減を実装する方法の詳細については、``Oculus Developer Centerのこのページ <https://developer.oculus.com/design/latest/concepts/bp-locomotion/>`_ を参照してください。

移動中の乗り物酔いを軽減するために、プレイヤーが移動している間のみ表示されるビネットエフェクトを追加します。

まず、すぐに Game.tscn に切り替えます。ARVROrigin ノードの下に、Movement_Vignette という子ノードがあります。このノードは、プレイヤーがVRコントローラーを使用して移動しているときに、単純なビネットをVRヘッドセットに適用します。これは乗り物酔いの軽減に役立つはずです。

Scenes フォルダにある Movement_Vignette.tscn を開きます。シーンは、カスタムシェーダーを備えた ColorRect ノードです。必要に応じて、カスタムシェーダーをご覧ください。これは、Godotデモリポジトリ にあるビネットシェーダーのわずかに変更されたバージョンです。

プレイヤーが動いているときにビネットシェーダーを表示するコードを記述しましょう。Movement_Vignette ノードを選択し、Movement_Vignette.gd という名前の新しいスクリプトを作成します。次のコードを追加します:

extends Control

var controller_one
var controller_two


func _ready():
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")

    var interface = ARVRServer.primary_interface

    if interface == null:
        set_process(false)
        printerr("Movement_Vignette: no VR interface found!")
        return

    rect_size = interface.get_render_targetsize()
    rect_position = Vector2(0,0)

    controller_one = get_parent().get_node("Left_Controller")
    controller_two = get_parent().get_node("Right_Controller")

    visible = false


func _process(_delta):
    if (controller_one == null or controller_two == null):
        return

    if (controller_one.directional_movement == true or controller_two.directional_movement == true):
        visible = true
    else:
        visible = false

このスクリプトはかなり短いため、スクリプトの機能について簡単に説明します。

ビネットのコードの説明

controller_onecontroller_two の2つのクラス変数があります。これらの変数は、左右のVRコントローラーへの参照を保持します。


_ready 関数では、最初に yield を使用して4つのフレームを待ちます。 4つのフレームを待機している理由は、VRインターフェイスの準備ができており、アクセスできるようにするためです。

待機後、ARVRServer.primary_interface を使用してプライマリVRインターフェースが取得され、interface という変数に割り当てられます。次に、コードは interfacenull と等しいかどうかを確認します。interfacenull に等しい場合、_process は、値が falseset_process を使用して無効にされます。

interfacenull でない場合、ビネットシェーダーの rect_size をVRビューポートのレンダリングサイズに設定して、画面全体を占めるようにします。VRヘッドセットごとに解像度とアスペクト比が異なるため、これを行う必要があります。したがって、それに応じてノードのサイズを変更する必要があります。 また、ビネットシェーダーの rect_position をゼロに設定して、画面に対して正しい位置に配置します。

次に、左右のVRコントローラーが取得され、それぞれ controller_onecontroller_two に割り当てられます。 最後に、ビネットシェーダーは、デフォルトで visible プロパティを false に設定することで非表示になります。


_process では、コードはまず controller_one または controller_twonull と等しいかどうかをチェックします。いずれかのノードが null に等しい場合は、return が実行されるため、何も起こりません。

次に、コードは、controller_one または controller_twodirectional_movementtrue に等しいかどうかを確認することにより、VRコントローラーのいずれかがタッチパッド/ジョイスティックを使用してプレイヤーを動かしているかどうかを確認します。どちらかのVRコントローラーがプレイヤーを動かしている場合、ビネットシェーダーは visible プロパティを true に設定することで自身を可視にします。 どちらのVRコントローラーもプレイヤーを動かしていないので、両方のVRコントローラーで directional_movementfalse である場合は、ビネットシェーダーは、その visible プロパティを false に設定することで自身を非表示にします。


これがスクリプトの全てです! コードを作成したので、トラックパッドやジョイスティックで動いてみましょう。 以前よりも乗り物酔いが少ないことに気付くはずです!

注釈

前述のように、VRで乗り物酔いを軽減する方法はたくさんあります。 移動を実装し、乗り物酔いを軽減する方法の詳細については、``Oculus Developer Center <https://developer.oculus.com/design/latest/concepts/bp-locomotion/>`_ の「この件に関するページ」をご覧ください。

最終ノート

../../../_images/starter_vr_tutorial_hands.png

これで、環境内を移動して RigidBody ベースのオブジェクトとやり取りできる完全に機能するVRコントローラーが完成しました。このチュートリアルシリーズの次のパートでは、プレイヤーが使用する特別な RigidBody ベースのオブジェクトを作成します!

警告

このチュートリアルシリーズの完成したプロジェクトは、リリースタブの下のGodot OpenVR GitHubリポジトリからダウンロードできます!