クラスター開発者ブログ

HTC ViveでThe Labのような瞬間移動の実装

こんにちは! クラスターでインターンをしている橋本です。 (@Miya04_NeT)

「The Lab」ライクな移動方法を自分なりに実装してみたので、初心者向けに解説を交えて紹介したいと思います。

今回実装した仕様は下記の通りです。

  • トリガーを引いて放物線を表示
  • トリガーを離して放物線の終着点に移動

TheLabDemo

移動方法のメリットとデメリット

メリット

  • 歩いて移動できなところにも移動できる
  • プレイ環境が狭くても移動できる

デメリット

  • 敵などを追っていると見失いやすい
  • 瞬間移動が自然でないマルチプレイには向かない
  • 今回実装したものだと
    • 地面がフラットでないといけない
    • 壁を通り抜けて移動できてしまう

その他もろもろあるとは思いますがパッと思いついたもので…

サンプルプロジェクト

開発環境

  • Windows 10
  • Unity 5.3.4p5

実装したクラス

  • Teleportation
  • DestinationPointer
    以上の二つだけです。

UniRxを使っていますが、初歩的なことしかしていないので詳しい使い方についてはUniRx オペレータ逆引き - Qiita を参照してみてください。

クラスの概要

Teleportation

コントローラのトリガーを押して離した時、TargetとなるGameObject(以下Target)の位置にPlayerを移動させる

DestinationPointer

自分で設定した初速やコントローラーの位置、傾きをもとに放物線を表示させて、終着地にTargetを設置する

ソースコード

Teleportation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;

/// <summary>
/// ターゲットの位置に瞬間移動するクラス
/// </summary>

public class Teleportation : MonoBehaviour
{

    [SerializeField] GameObject ownPlayer;
    [SerializeField] GameObject targetMarker;
    SteamVR_TrackedObject trackedObj;

    void Start()
    {
        targetMarker.SetActive(false);
        //任意のViveコントローラーから取得
        trackedObj = GetComponent<SteamVR_TrackedObject>();
        //デバイスの入力受け付け
        var device = SteamVR_Controller.Input((int)trackedObj.index);

        this.UpdateAsObservable()                                               //Updateが呼ばれた時
            .Where(_ => device.GetTouch(SteamVR_Controller.ButtonMask.Trigger)) //コントローラーのトリガーを押している間
            .Subscribe(_ => targetMarker.SetActive(true));

        this.UpdateAsObservable()
            .Where(_ => device.GetTouchUp(SteamVR_Controller.ButtonMask.Trigger)) //コントローラーのトリガーを離したとき
            .Subscribe(_ => moveToPoint());                                       //ターゲットマーカーの位置へ移動
    }

    /// <summary>
    /// 移動先の位置を調整して移動
    /// </summary>
    void moveToPoint()
    {
        ownPlayer.transform.position = targetMarker.transform.position;
        targetMarker.SetActive(false);
    }
}

DestinationPointer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 放物線を表示するクラス
/// ラインレンダラーの点の数と
/// 初速を自由に設定して使用
/// </summary>

public class DestinationPointer : MonoBehaviour
{

    [SerializeField] GameObject targetMarker;
    [SerializeField] LineRenderer lineRenderer;
    [SerializeField] float vertexCount = 25;
    [SerializeField] float initialVelocity = 1;
    List<Vector3> vertexes = new List<Vector3>();
    static readonly float Gravity = 9.81F;

    void Start()
    {
        //コントローラの入力の後に読みたい
        this.LateUpdateAsObservable()
            .Where(_ => targetMarker.activeSelf) //ターゲットマーカーが表示されているとき
            .Subscribe(_ => showOrbit());        //放物線を表示させる

        this.LateUpdateAsObservable()
            .Where(_ => !targetMarker.activeSelf)          //ターゲットマーカーが非表示のとき
            .Subscribe(_ => lineRenderer.enabled = false); //放物線を非表示にする
    }

    /// <summary>
    /// 放物線を表示する関数
    /// </summary>
    void showOrbit()
    {
        lineRenderer.enabled = true;
        //コントローラの向いている角度(x軸回転)をラジアン角へ
        var angleFacing = -Mathf.Deg2Rad * transform.eulerAngles.x;
        var h = transform.position.y;
        var v0 = initialVelocity;
        var sin = Mathf.Sin(angleFacing);
        var cos = Mathf.Cos(angleFacing);
        var g = Gravity;

        //地面に到達する時間の式 :
        //  t = (v0 * sinθ) / g + √ (v0^2 * sinθ^2) / g^2 + 2 * h / g
        var arrivalTime = (v0 * sin) / g + Mathf.Sqrt((square(v0) * square(sin)) / square(g) + (2F * h) / g);

        for (var i = 0; i < vertexCount; i++)
        {
            //delta時間あたりのワールド座標(ラインレンダラーの節)
            var delta = i * arrivalTime / vertexCount;
            var x = v0 * cos * delta;
            var y = v0 * sin * delta - 0.5F * g * square(delta);
            var forward = new Vector3(transform.forward.x, 0, transform.forward.z);
            var vertex = transform.position + forward * x + Vector3.up * y;
            vertexes.Add(vertex);
        }
        //ターゲットマーカーを頂点の最終地点へ
        targetMarker.transform.position = vertexes.Last();
        //LineRendererの頂点の設置
        lineRenderer.SetVertexCount(vertexes.Count);
        lineRenderer.SetPositions(vertexes.ToArray());
        //リストの初期化
        vertexes.Clear();
    }

    /// <summary>
    /// 引数の2乗を返す関数
    /// </summary>
    /// <param name="num">Number.</param>
    static float square(float num)
    {
        return Mathf.Pow(num, 2);
    }
}

クラスの使い方

[CameraRig]の子オブジェクトにある任意のコントローラー(今回はController (left))に以下のコンポーネントをアタッチします。

  • LineRenderer
  • Teleportation
  • DestinationPointer

TeleportationクラスのフィールドのownPlayer[CameraRig]TargetMarkerAssets > PrefabsにあるTargetMarkerをアタッチ。
DestinationPointerクラスのフィールドのTargetMarkerには上のと同じもの、LineRendererにはアタッチしたものを指定します。なおLineRendererの線の太さはデフォルト値だと太すぎるため、Parametersから調整してください。
VertexCountLineRendererが屈折する頂点数のことで、値を増やすとより綺麗な曲線となりますが処理は重くなります。Initial Velocityは斜方投射の初速の値で、これを変更することで一度に移動できる最大距離が変化します。

詰まった点・困った点

開発をMacでデバックをWindowsのPCでやっていたためデバック用の仮のコントローラーオブジェクトと実際のコントローラーで行った時の挙動若干違ったこと(Rotation周り)

所感

放物線の出しかたについてですが、ほかにも前方にRayを飛ばしてそこに向けて放物線を描くとか色々方法あると思います。その辺はコンテンツの内容や用途によって色々変えていきたいとこですね。

参考

使ったアセット

このエントリーをはてなブックマークに追加