Unity3D+シューティングゲームのオブジェクトプールのチュートリアル

Blogをいつもご覧いただきありがとうございます。

シューティングゲームを作った時に、武器の弾のシステムは様々な書き方がありますが、一番シンプルな書き方は、恐らく弾のオブジェクトをインスタンスして、敵にヒットしたら(コード側ではコライダーやポジションでどちらでも可能です。)、敵と弾一緒に消滅すると思います。

弾を生成する場合の例:

using UnityEngine;
using System.Collections;

public class BulletInstance : MonoBehaviour {
public GameObject BulletPrefab;
// Update is called once per frame
void Update () {                                                                                                            
 GameObject bullet = Instantiate(BulletPrefab, Vector3.zero, Quaternion.identity) as GameObject;   //弾をインスタンス 
}

弾と敵を消滅する場合の例:

using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour {

void OnTriggerEnter(Collider other)
 {
   if (other.tag.CompareTo("Bullet") == 0)
    {
       Destroy(other.gameObject);//弾が消される
       Destroy(this.gameObject);//敵自分が消される
    }
 }
}

この書き方は普通に動くはずですが、でも、常に弾のPrefabをインスタンスしたり、Destroyしたりしたら、メモリー上にとってあまり優しいとはいえません。正直に言うとPC用としてはまだしも、スマートフォンゲームの場合はかなりきびしいものになると思います。
オブジェクトをずっと増やし続けるとゲームのFPSが落ちたり、重くなったり、スマートフォンが熱くなったりするのもおかしくないです。

それをさける為に、考えを逆にするのはどうでしょうか?

プログラム側では、オブジェクトのInstanceやDestroyの場合にリソースを消費しているとしたら、なら、常にInstanceやDestroyしないシステムを作ればいいじゃんないか?

それでは、オブジェクトプールを勉強しましょう!

·オブジェクトプールの概念

オブジェクトプールとは、この先使えるオブジェクトを溜めておく用の倉庫のような存在で、オブジェクトが必要になったら取り出し、使い終わったら戻すことで、なるべく少数のオブジェクト使い回してオブジェクト数及び生成・消滅のコストを抑えるものです。

それでは、実際にUnity3D側で作っていきましょう。

·実装

Step1 Unityの環境作り

Step2 弾のプールシステム

ここで二つのスクリプトが必要です。

  • BulletPool.cs  このスクリプトはオブジェクトプールで、オブジェクトにドラッグドロップする必要はありません。
  • Bullets.cs     このスクリプトは弾のPrefabにドラッグドロップするスクリプトです,弾の方向やスピードをコントロールしています。
実装コード:

BulletPool.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletPool : MonoBehaviour {
    //オブジェクトのプールとしてのリストを作る
    List pools = new List();
    // start pos
    Vector3 firstPosition;
    //シングルトン
    private static BulletPool instance;
    private BulletPool() { }
    public static BulletPool GetInstance()
    {
        if (instance == null)
        {
          
            instance = new GameObject("BulletPool").AddComponent();
           
        }
        return instance;

    }
   
    //オブジェクトプールからオブジェクトを取得する
    public GameObject MyInstantiate(GameObject name)
    {
        //オブジェクトプールは空だから、インスタンスする
        if (pools.Count == 0)
        {
            return Instantiate(name, Vector3.zero, Quaternion.identity) as GameObject;
        }
        else 
        {
           
            //もしプールでは空じゃない場合は「0」を取る
            GameObject go = pools[0];
           
            go.SetActive(true);
            //オブジェクトプールから消される
            pools.Remove(go);
            return go;

        }
    }
    //オブジェクトプールを作成
    public void DelayInstantiate(GameObject name)
    {
        //オブジェクトを隠す
        name.SetActive(false);
        //オブジェクトプールに入れる
        pools.Add(name);
    }
}

Bullets.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullets : MonoBehaviour
{
    Transform target;
    private void OnEnable()
    {
        //ポジションを初期化する
        target = GameObject.Find("Hero").transform;
        transform.position = target.position;
        //二秒の後でCoroutineを実行する
        StartCoroutine(DelayObject(2f));
    }
    private void Update()
    {
        //弾の移動
        transform.Translate(Vector3.up * Time.deltaTime * 100);
    }
   
    IEnumerator DelayObject(float time)
    {
        yield return new WaitForSeconds(time);
        //弾のプールリストに入れる
        BulletPool.GetInstance().DelayInstantiate(gameObject);
    }

}

 

Step3 Playerのコントロール

弾のプールを実装完了したら、次は発砲の機能や移動の機能が入ってる”PlayerControlManager.cs”を「Hero」のオブジェクトにドラッグドロップしてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerControlManager : MonoBehaviour {

    float _hor_Speed = 1f;    //水平移動の速度
    float _ver_Speed = 1f;  //垂直移動の速度 
    float _topMax = 300f;//Player移動制御の変数
    float _bottomMax = -300f;//Player移動制御の変数
    float _sideMax =177f;//Player移動制御の変数
    float _turnAngle = 45;//回転の角度
    GameObject bulletPrefab;
    void Awake(){
        bulletPrefab = Resources.Load("Prefabs/Bullet") as GameObject;

    }
    void Update()
    {
        Move();
        TurnBody();
        Limit();
        Fire();
    }

    void Fire()//発砲
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            BulletPool.GetInstance().MyInstantiate(bulletPrefab);
        }
    }
    
    void Limit()//移動制御
    {
        if (transform.localPosition.x > _sideMax)
        {
            transform.localPosition = new Vector2(_sideMax, transform.localPosition.y);
        }
        if (transform.localPosition.x <  -_sideMax)
        { 
            transform.localPosition = new Vector2(-_sideMax, transform.localPosition.y); 
        } 
        if (transform.localPosition.y > _topMax)
        {
            transform.localPosition = new Vector2(transform.localPosition.x, _topMax);
        }
        if (transform.localPosition.y < _bottomMax)
        {
        transform.localPosition = new Vector2(transform.localPosition.x, _bottomMax);
        }
    }

    void TurnBody()//機体を回転する
    {
        if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
        {
            transform.Rotate(new Vector3(0, _turnAngle, 0));
        }
        if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
        {
            transform.Rotate(new Vector3(0, -_turnAngle, 0));
        }
        if (Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.LeftArrow))
        {
            transform.Rotate(new Vector3(0, -_turnAngle, 0));
        }
        if (Input.GetKeyUp(KeyCode.D) || Input.GetKeyUp(KeyCode.RightArrow))
        {
            transform.Rotate(new Vector3(0, _turnAngle, 0));
        }
    }
    void Move()//Playerの移動
    {
        float ver = Input.GetAxis("Vertical");
        float hor = Input.GetAxis("Horizontal");
        transform.position +=new Vector3(hor * _hor_Speed, ver * _ver_Speed, 0);
    }

 }

   

上記の流れに沿って実装ができたら、プロジェクトをPlayしてください。

こんな感じですね

·注意点:

  • オブジェクトプールは非常に便利です。使う場面として繰り返し利用するオブジェクトを生成する場合に有効です。今回の場合は弾や敵などです。
  • 使う場合によっては逆効果が出る可能性もあります。実際にゲームのジャンルやシステム、オブジェクトの特徴などによって、どのような形式のオブジェクトプールを使うのか考える必要があると思います。
  • オブジェクトを破棄する代わりに隠す(表示しないようにする)ことで、表示的には破棄と同等の効果実現してます。そのおかげでオブジェクトをインスタンス化する際のリソースの無駄な消費を抑えられると思います。

※補足情報としてオブジェクトプールを利用することで、オブジェクトのインスタンス化をできるだけ削減しましたが、既にインスタンスされたオブジェクトはSetActive(false)になっても、まだメモリーの中に存在し、メモリーの容量を占有しています。メモリの占有が完全になくなるわけではありません。

ご覧いただきありがとうございました。

(Unity3D)ダイナミックボタンアクション

あるゲームのUIを作っている間にボタンに関する問題に遭遇しました。選択されたエンティティによって、表示されているボタンが変わるようにしたいと思いました。

もちろん一つずつのエンティティのGameObjectを準備して場合によってSetActive()trueかfalseにしてもいいですが、スケーラビリティは?エンティティを追加しようとする毎に新しいGameObjectを準備しますか?そんな面倒なことは勘弁してほしいですよね。

ということでスケーラビリティのため、エンティティを選択する時にボタンが編集され、ちょっとしたアルゴリズムを書きました。 

こういうシステムが入っているUIを作る人がいて参考になればと思い、分かりやすいバージョンを準備して下記にそのケースを紹介します。 

想定ケース: 

1.エンティティによる、ボタンの数が異なる場合でもOK。 

2.ボタンの見た目を編集可能にする。 

3.ボタンのアクションがstringの引数を受け取れる。 

4.簡単にするため、ボタンの最大数は10にします。 

先ずはボタンの情報をまとめるクラスActivityButtonをつくります 

public class ActionButton {
    public UnityAction<string> theAction;
    public string argument;
    public Sprite sprite;
    public ActionButton(UnityAction<string> act, Sprite sprite, string arg=""){
        theAction = act;
        argument=arg;
        this.sprite = sprite;
    }
}

UIマネージャーにはButton、Image、GameObjectの配列を必要です。パブリックなGameObjectの配列を作ってシーンからボタンをドラッグして、Start()でButtonとImageの配列を作りました 

public class MyEpicUIManager : MonoBehaviour {
    Button[] actionBtns;
    Image[] actionImgs;
    public GameObject[] actionBtnObjs;

    void Start () { 
        // Action Panel Setup
        actionBtns = new Button[actionBtnObjs.Length];
        actionImgs = new Image[actionBtnObjs.Length];
        for(int i=0; i<actionBtnObjs.Length; i++){
            actionBtns[i]=actionBtnObjs[i].GetComponent<Button>();
            actionImgs[i]=actionBtnObjs[i].GetComponent<Image>();
        } 
    }
}

ボタンをゲットしたので、次はActionButtonのリストを取得しボタンを編集するメソッドを作りました。そしてボタンをクリアするメソッドも必要です。 

public void SetActions(List<ActionButton> btnActions){
    ClearActions();
    for(int i=0; i<btnActions.Count; i++){
        actionBtnObjs[i].SetActive(true); 
        UnityAction<string> action = btnActions[i].theAction;
        string arg = btnActions[i].argument;
        actionBtns[i].onClick.AddListener(()=>action(arg)); 
        actionImgs[i].sprite = btnActions[i].sprite; 
    } 
}

public void ClearActions(){
    for(int i = 0; i<actionBtnObjs.Length; i++){
        actionBtns[i].onClick.RemoveAllListeners();
        actionBtnObjs[i].SetActive(false);
    }
}

これでUIマネージャーが完了です。Start()でClearActionsを呼べば、ゲームを起動する際にボタンクリアできます 

エンティティにはそれそれに自分のActionButtonリストが必要です。UIマネージャーのSetActionsを呼べるようにする必要があります。そしてアクション自体も必要です。

public class MyEntity1 : MonoBehaviour {
 
    public MyEpicUIManager ui;
    List<ActionButton> uiActions = new List<ActionButton>();

    void Start(){
        uiActions.Add(new ActionButton(Action1,Resources.Load<Sprite>("Sprites/E1A1"))); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E1A2"), "Test argument 1")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E1A2"), "Test argument 2")); 
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E1A3"), "3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E1A3"), "5")); 
    }

    // An action that doesn't really use the string provided 
    public void Action1(string s =""){
        print("Entity 1 Action 1");
    }

    // An action that uses the string
    public void Action2(string s =""){
        print("Entity 1 Action 2 Argument: " + s);
    }

    // An action that changes the string to an int
    public void Action3(string s =""){
        string msg = "Entity 1 Action 3 ->"+s+" hashes: ";
        int n = 0;
        int.TryParse(s, out n);
        for(int i=0; i <n; i++) msg+="#";
        print(msg);
    }

    public void OnSelect(){
        ui.SetActions(uiActions);
    }
} 
public class MyEntity2 : MonoBehaviour {
 
    public MyEpicUIManager ui;
    List<ActionButton> uiActions = new List<ActionButton>();

    void Start(){
        uiActions.Add(new ActionButton(Action1,Resources.Load<Sprite>("Sprites/E2A1"))); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 1")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 2")); 
        uiActions.Add(new ActionButton(Action2,Resources.Load<Sprite>("Sprites/E2A2"), "Test argument 3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "3"));
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "5")); 
        uiActions.Add(new ActionButton(Action3,Resources.Load<Sprite>("Sprites/E2A3"), "7")); 
    }

    // An action that doesn't really use the string provided 
    public void Action1(string s =""){
        print("Entity 2 Action 1");
    }

    // An action that uses the string
    public void Action2(string s =""){
        print("Entity 2 Action 2 Argument: " + s);
    }

    // An action that changes the string to an int
    public void Action3(string s =""){
        string msg = "Entity 2 Action 3 ->"+s+" asteriscs: ";
        int n = 0;
        int.TryParse(s, out n);
        for(int i=0; i <n; i++) msg+="*";
        print(msg);
    }

    public void OnSelect(){
        ui.SetActions(uiActions);
    }
}

こちらはエンティティのサンプルです。OnSelectはキーボードかシーン上のボタンで試してみてください。このサンプルを動かすため “Assets/Resources/Sprites”に、正しい名前のスプライトを入れてください。そしてスプライトのImport SettingsのTexture Typeは “sprite (2D and UI)”にする必要があります。 

シーンに必要なものはCanvasと7つ以上のボタンです。UIマネージャーをシーンに追加して、インスペクターでボタンをドラッグします。エンティティもシーンに追加してインスペクターでUIマネージャーをドラッグしてください。 

それで完了です。短くて簡単な例となりますが、これでエンティティを追加ときにいちいち新しいパネルを準備する必要がなくなります。そしてアクションを追加したり削除する場合でも、リストから追加したり削除することで実現できます。

Stingray(VRテンプレート)の持ち上げ処理を調べてみる
(離す処理)


前回、ついに『持ち上げる処理』の実装を全て見終えました。
しかし、これまで見た処理はあくまで『持ち上げる』というだけの処理です。

今回は、『持ち上げたものを離す』処理を最後まで見ていきます。



■ Un-link – Release



赤の部分は『トリガーが離された時』に実行されています。
画像だとやや見づらいので、
この画像を更に黄色・オレンジに区切って詳細に見ていきます。

①離したコントローラがリンクされているかチェック



まずは、持ち上げている状態かどうかを表す変数『is_linked』
true(持ち上げている状態)なのかをチェック、
『ユニットを持ち上げているコントローラ』『トリガーを離したコントローラ』が同一かもチェックします。

②コントローラとのリンクを外し、加速度を設定する



続いて、『SteamVR Unlink Node From Tracker』ノードで
ユニットとリンクしている対象(コントローラの持つ持ち上げ関数)とのリンクを外します。
その後は『SteamVR Set Actor Kinematic』で自由に動かせないようにします。

直後の『Set Actor Velocity』ノードには
前々回に登場した変数『wand_velocity』の値をセットしています。

『Set Actor Velocity』ノードは引数のユニットのアクタに対して引数の分だけの速度を設定します。

変数『wand_velocity』はコントローラの持つ加速度を表しているため、
離した時点での加速度がそのままユニットの速度になっているということになります。

加速度の設定が終わったら変数『is_linked』をfalseに変更、
ユニット自身の『Unlinked』イベントを実行しています。

『持ち上げたものを離す』という処理はこのように実装されていました。

一応、この先にもいくつか処理が残ってはいますが、『離す』処理はここで完了です。



■ Hide – Unhide the Controller



ここでの処理は『物を離した後、コントローラを可視状態にする』というものです。

これはVRテンプレートのバットのユニットフローを見ないとわからないのですが、
一部のユニットは『linked』イベントの処理で
『コントローラを不可視にする』
という実装がされています。
ここで可視状態にするという処理を行っていない場合、
バットを離した後でもコントローラが見えないままという問題が発生するため、
この実装が行われています。

ちなみに、ここで使用されている『Set Unit Visibility』は以前使用した時とは異なり、
ユニットそのものを引数で設定しているため、ユニット全体の可視・不可視を変更できています。



■ set_invisable



その下にある『Event for Hiding the Wand』から続く処理は
『External In Event』ノードから始まる、独立したイベントです。

先ほどの説明で出てきた『コントローラを不可視にする』という処理はここで行われています。
つまり、
コントローラによってバットが持ち上げられる
→バットの持つlinkedイベントが実行される
→バットのlinkedイベントの中でコントローラのset_invisableイベントが実行される
という処理の流れになっています。

一旦両方のコントローラを可視状態にしてから、
不可視に設定するコントローラだけ不可視にしています。
使用しているノードは可視の時から変わりません。



以上で、コントローラの持ち上げ処理は全て見終えることができました。
残っているコントローラ関連の処理はテレポートのみとなります。
残念ながら、StingrayのVRテンプレート調査はこれで終わりですが、
ここまで基礎を理解することができれば、
きっとテレポートの処理を理解することも簡単だと思います。

今まで、ありがとうございました。

Stingray(VRテンプレート)の持ち上げ処理を調べてみる
(前準備編)

今回からは、前回の最後に登場した『function_pickup』ユニットのユニットフローを見ていきます。

コントローラのユニットフローを見てもわかる通り、
『function_pickup』ユニットには『物を持ち上げる』という処理が実装されています。

早速、function_pickupユニットを見ていきましょう。

アセットブラウザから『vr_steam』->『models』->『functionality_pickup』の中にあるユニットを開きましょう。



なお、画像の赤四角のようなアイコンはユニットフロー単体を表しています。
こちらのアイコンをダブルクリックして開くとユニットエディタとは異なる
ユニットフローを編集するための『FlowEditor』が開かれます。

ユニットフロー以外の要素がなく、
ユニットエディタを開く必要がない場合はこちらを開いてもいいかもしれません。

そうして開かれた『functionality_pickup』のユニットフローが以下の画像です。



非常に膨大な処理ではありますが、少しずつ進めていきます。

今回、説明していく箇所は、以下の画像で赤・青・緑の枠の部分です。



■ 『Set Variables on Spawn』

青色の枠の箇所です。



『Unit Spawned』から始まっていることからわかるかもしれませんが、
ここでは一番最初に行うべき『変数の設定』を行っています。

以下の変数がここで設定されています。

————————————————
スコープ:型:名前
————————————————
ローカル:bool:touch_tracker:False
ローカル:bool:laser_on:False
ローカル:string:my_global_var:wand_extras_1 or 2
グローバル:unit:wand_extras_1 or 2:unit
グローバル:bool:keep_my_rotation_and_position:True
————————————————



■ 『Wand Velocity』

赤色の枠の箇所です。



『Level Update』にて常に行うべき処理が実装されています。

『Get Unit World Position』を使用して自分のワールド座標を取得し、
その結果を変数『wand_current_frame_pos』に設定しています。
変数『wand_current_frame_pos』は現在位置を表しています。

この部分では変数『wand_current_frame_pos』から
変数『wand_last_frame_pos』の値を減算した値に
『Get Last Delta Time』で取得した時間を除算、
その結果をユニットが持つスクリプトデータの『wand_velocity』
変数の『wand_velocity』に代入しています。

この値はコントローラの加速度を表しています。

ちなみに、まだ値をセットしている個所の説明をしていませんが、
変数『wand_last_frame_pos』直前のコントローラの位置をセットしています。

また、『Get Last Delta Time』
前回の更新から今回の更新までの間に経過した秒数を返します。

つまり、ここの計算は直前と現在のコントローラ位置を比較し、
その差の分を経過秒数で割り、1秒あたりの加速度を計算しています。

最後に、現在のコントローラ位置を
変数『wand_last_frame_pos』にセットしています。



■ 『Collision Events and Variables』



緑色の枠の箇所です。
『コントローラが特定のユニットに触れている間の処理』が実装されています。

ここの処理の始まりは『Physics Trigger』というノードです。

『Physics Trigger』ノードは『引数に指定したメッシュが特定の対象に触れた時』に呼ばれます。
また、その際に触れているユニットなどを返します。

ここでは、特定のユニットに触れた場合、下記の処理を一気に実行しています。

————————————————
触れた対象が持つ『hilight_on』イベントの実行、
触れている状態を示す変数『wand_is_touching』をTrueに変更、
触れているユニットをユニット変数『touching_unit』に格納、
触れているコントローラを表すグローバル変数『controller_touch_index』を設定。
————————————————

また、ユニットから離れた場合も下記の処理を実行しています。

————————————————
触れた対象が持つ『hilight_off』イベントの実行、
触れている状態を示す変数『wand_is_touching』をFalseに変更。
————————————————

これは、触れているユニットが目で見てわかるようにするための処理となっています。
触れている状態かどうかのフラグもここで立てているため、
ここまで実装できてようやく『物を掴む』処理の準備が整ったことになります。



次回はいよいよ本題となる『物を掴む処理』について見ていきます。

Stingray(VRテンプレート)のコントローラ処理を調べてみる
(リンクとスポーン)

前々回前回と続いてきた
StingrayVRテンプレートにおけるコントローラの実装調査ですが、
いよいよ今回で最後です。


最後ということで、キーになることも多いです。
ただし、ここで覚えることが
StingrayにおけるVR開発において最も重要なことになります。

最後はこの3つのグループになりますが、
実際は右2つのグループB,Cは左のグループAに繋がっているので、
大きな1つのグループと見ることができます。



一見ややこしいように見えますが、処理の内容自体はシンプルです。



※『link controllers』



画像内のグループAで、ここでの処理が終わると、
下で説明している2つのグループB,Cへと続きます。

①外部イベントの作成

ここの処理の始まりとなっているのは『External In Event』です。
このノードは独自にイベントを作ることができ、
外部からこのイベントが呼ばれた際に以後の処理が呼ばれるようになります。
(UnrealEngine4のカスタムイベントなどが近いです)

つまり、
『ここより後の処理はコントローラのユニットフロー単体では動作せず、
外部よりこのイベント(link_controller)が呼ばれた時にのみ実行される』

という事になります。

ちなみに、このVRテンプレートの場合、link_controllerの呼び出し元は
『steam_vr』という名前のLuaスクリプトになります。



それ以外でも『Level Unit』などのノードからも呼び出すことができます

②ユニットとコントローラのリンク

続いては『SteamVR Link Node To Tracker』ノードです。
このノードは『Link to』に設定されたHMDやコントローラの位置に
引数に指定したユニットのメッシュをリンクさせる(くっつける)処理です。

これまで、何気なくVR画面でコントローラを動かしていましたが、
VR画面でコントローラが見えていたのはこの処理があったおかげだったのです。

この先ではグループB,グループCに処理が分かれますが、
『どちらかを行う』などではなく、両方の処理を行います。



※『Spawn Pickup Unit to get the Pickup Functionality』



画像内のグループBです。
ここでは『物を持ち上げる為の前準備』を行っています。

①スクリプトデータをチェック

最初は『Branch』ノードが実行されます。
『Branch』ノードは引数として受け取ったbool型の結果によって
TrueかFalseに処理を分岐させるというノードです。
一般的なプログラムにおけるif文であり、
他のビジュアルスクリプティングにおいても同名で使用されているなど
これなしでプログラムは成り立たないほどの基本的なノードです。

今回はTrueなら処理を続け、Falseなら処理が終了する、といった形になっています。

②持ち上げ処理のスポーン

Trueから続く処理は『Spawn Unit on Position』ノードです。
このノードは引数のUnitに指定したユニットをスポーンさせるという処理を行います。
PositionやRotationをセットするとスポーンさせる位置や向きを指定できます。

ここでスポーンしているのは『functionality_pickup』というユニットになります。
このユニットについてはまた後で説明しますが、
簡単に言うと『対象を持ち上げる処理』になります。

スポーンしたユニットに『Set Unit String Data』を使用してユニット変数controller_indexを設定します。

③スポーンしたユニットとリンク

最後に使用するノードは『SteamVR Link Node To Tracker』です。
今回はコントローラではなく、
先ほどスポーンしたfunctionality_pickupとリンクさせています。

つまり、ここでの処理は
『持ち上げる為の処理をスポーンさせ、コントローラの位置にリンクさせる』
という物です。



※『Spawn Teleport Unit to get the Teleport Functionality』



画像内のグループCです。
こちらのグループでは『プレイヤーのテレポート処理の前準備』を行っています。

①~③までの処理は②でスポーンしているユニットが
『functionality_teleport』である以外は全く同じです。

④キー入力の取得

更に下側の処理は『Level Update』によって実行されています。
キー入力を取得しており、前々回でも登場した『SteamVR Touch』を使用しています。
ただし、今回は入力された座標が欲しいわけではないため、使用する返り値は
入力した時/離した時を表すPressed/Releasedのみです。

⑤外部イベントの呼び出し

Pressed/Releasedの両方から、『Unit Flow Event』に繋がっています。
『Unit Flow Event』はユニットが持つ外部イベントを呼び出すノードです。

つまり、
Pressedの場合はfunctionality_teleportが持つ『press_teleport』イベント
Releasedの場合は同じくfunctionality_teleportが持つ『release_teleport』イベント
へと処理が続きます。



これで、コントローラのユニットフローの処理は一通り見ることができました。
しかし、今回の調査で新しくfunctionality_pickupfunctionality_teleportという新しいユニットが出てきました。
次回からはこの2つのユニットフローについて、ご説明していきます。

Stingray(VRテンプレート)のコントローラ処理を調べてみる
(初期化とトリガー)


前回から引き続き、今回もコントローラの内部処理(ユニットフロー)を見ていきます。

今回はこの2つのグループの処理を見ていきます。



※『Create Unit Variable on Spawn – wand_1 or wand_2』



このグループではコントローラが『一番最初に行うべき処理』が実装されています。

①スポーン直後に待機する

処理の開始位置となっている『Unit Spawned』『ユニットが生成されたタイミング』を表しています。
この場合であれば『Viveコントローラが認識され、レベルに出てきたタイミング』です。

『Unit Spawned』からは『Delay』ノードに繋がっています。
Delayノードは引数に設定された時間だけ処理を止めるという処理です。

無くてもいいと感じるかもしれませんが、
この先のフローが何らかの形で本来先に実行される処理よりも
先に実行されてしまうと、問題が発生する場合などに用いられます。

②ユニット変数を作成

続いて実行される『Set Unit Variable』
ユニットを格納するための変数を作成します。
変数名は前回にも触れたcontroller_indexに『wand_』という文字列を繋げた物です。

③ボールを不可視に

最後は『Set Unit Visibility』で最初は表示されたままだったボールを不可視にしています。

つまり、ここでは
『一番最初に、コントローラによってユニットに
識別用に異なる名前を付け、トラックパッド上のボールを不可視にする』

という初期化の処理をしています。



※『pull trigger』



このグループは、前回のトラックボールと同じように入力に関係した処理を行っています。

①トリガーの入力を取得する

基本的な入力の取得方法は前回と同じです。

ただし、今回は使用するノードが『SteamVR Touch』ではなく、『SteamVR Button』になっています。
この二つの違いは、入力を取得する対象が『トラックパッドの位置』か、それとも『それ以外のボタン入力』かという点のみです。
(一応Buttonの方でもトラックパッドの入力は取得できますが、位置までは拾えません)

②トリガーの入力を基に回転値の計算

続いて、『SteamVR Button』のValue値を乗算しています。
これも前回と同じですね。
ただし、『SteamVR Button』の入力範囲は0~1なので、
計算する際には注意してください。
(Touchの場合はX/Yそれぞれ-1~1の範囲)

ここでの乗算は-30ですが、これで最小値0、最大値-30の結果が得られます。
最後にこの結果を『Rotation From Components』のXにセットして、回転値にします。
(ここでは一方向にだけ回転させることが目的なので、X以外の値は0で固定です。)

③回転値のセット

最後に『Set Unit Local Rotation』でこのユニットが持つ『trigger』メッシュのローカル回転値を②の結果に設定しています。

ここまで来ればわかるかもしれませんが、ここでの処理は
『コントローラのトリガーがどれだけ引かれているかを取得し、
引かれている分だけ画面内のトリガーを回転させる(引く)』

という処理になります。



今回はここまでです。
いつもよりも簡単な箇所でしたので物足りなかったかもしれません。

次回はコントローラのユニットフローを最後まで見ていきます。

Stingray(VRテンプレート)のコントローラ処理を調べてみる
(トラックパッドとユニットフロー)

前回は『Flow』による簡易的なプログラムを組んでみました。
今回からは本格的な要素に入ります。


StingrayのVRテンプレートでは
非常に軽い建築デモ程度であればそのまま使えてしまえるような機能が最初から実装されています。

それらの機能がどのように実装されているのか、少しずつ処理の解説を交えつつ、見ていきましょう。

今回、見ていく処理は『コントローラの処理』になります。

さて、前回ではFlowの一種である『レベルフロー』を見ていきましたが、
いくらレベルフローを見てみたとしても、コントローラの処理と思しきものはどこにもありません。

なぜなら、コントローラの処理はコントローラ自身にあるためです。

まずは、どこでコントローラ自身の処理が見られるのか、についてです。

エディタの画面の下側にあるタブを見てみましょう。

これは『アセットブラウザ』と呼ばれ、
現在開いているプロジェクトの中にあるモデルや音などのアセットを見ることができます。

この中に、コントローラのモデルがあります。
アセットブラウザの左側にあるリストから
『VR_steam』->『models』->『controller』を選択します。
すると、コントローラのモデルが見つかるはずです。



このモデルをダブルクリックしましょう。



※ユニットエディタとユニット

モデルをダブルクリックすると、画像のような画面が開かれます。



この画面は『ユニットエディタ』といい、『ユニット』を編集する画面です。

『ユニット』とは、レベルに配置されるものの名前であり、
一つ一つがメッシュや各種プロパティを持っています。

また、ユニット自身がFlowを持つこともでき、
ユニットが所持するFlowは『ユニットフロー』と呼ばれます。

そして、ユニットエディタではユニットのプロパティの変更や、
ユニットフローの編集などを行うことができます。

画面の下側にある『Unit Flow』タブをクリックして開いてください。



こうして開かれるのがユニットフローの画面です。
細かな点や用途に違いはありますが、基本的な部分はレベルフローと変わりません。

今回は、ここに実装されている処理の内容を調べていきましょう。

コントローラのユニットフローを見てみると、
複数のノードが一つのブロックに囲まれているのがわかるかと思います。

これは『グループ化』といい、いくつかのノードを一つのグループにまとめ、
一括で削除したり、移動したりできるようにする機能です。
また、グループには名前を付けることができ、
Flowの可読性を向上させるという事もできます。

コントローラのユニットフローでは6つのグループがあります。

この記事では、このうちの1つのグループの処理について調べていきます。



※『Tracking dot on the track pad.』



このグループでは『トラックパッド上のボールの移動』が実装されています。



プレイ中、コントローラを見てみるとトラックパッドを操作している間、
指を触れている個所にボールが出現しているはずです。

試しに、このグループを削除してからプレイしてみると、ボールが出現しなくなります。
ちなみに、グループをまるごと削除するためには、グループのタイトルの部分をクリックしてからDeleteキーを入力します。

①トラックパッドの入力を取得



『Get Unit String Data』ノードは
ユニットに設定されているKeyと同名のスクリプトデータを取得します。

『スクリプトデータ』とはユニットがそれぞれ持つ数値などで、
プログラムでいうところの『変数』のような役割を持ちます。

ユニットエディタの右側にある『Script Data』から、追加や削除ができます。

コントローラのスクリプトデータを確認してみると、
『Get Unit String Data』で指定されている
『controller_index』というキーが見つかるはずです。
このキーはコントローラの番号を示しています。

controller_indexの値は『Level Update』に接続された
『SteamVR Touch』ノードの引数に用いられています。

これで『番号で指定されたコントローラのトラックパッドの入力を取得する』という処理になります。

②ボールの出現位置の計算
『SteamVR Touch』のX/Yの値は、トラックパッドで入力されている座標を1~-1の範囲で表しています。
この値を『Multiplication』ノードで乗算しています。
これは、そのままX/Yの座標を使用してしまうと、値が大きすぎてトラックパッドからボールが離れてしまうためです。
乗算の結果は『Vector From Components』で使用され、ベクトルになります。
このベクトルは最終的にボールの出現位置のローカル座標となりますが、
ここではYの値をYではなくZに代入する必要があるため要注意です。

最終的なYの値になるのは、先ほどの計算結果に更に乗算を重ねたものとなります。
この結果がトラックパッド上のボールの高さとなります。

③ボールのメッシュの可視・不可視を変更
『トラックパッドに指が触れた』事を表す『Touched』と接続しているのは
『Set Unit Visibility』ノードです。
このノードは引数のユニット内の指定したグループに該当するメッシュの可視性を変更するという処理を行います。
この場合の『グループ』は『ビジビリティグループ』の事であり、ユニットフローのノードの『グループ』とは異なります


『ビジビリティグループ』とは、
ユニットが持つメッシュをひとまとめにした物であり、
メッシュの可視・不可視を一括で変更したりする場合に役立ちます

ビジビリティグループはユニットエディタ右下の『Visibility Groups』から
変更や新規追加、確認が可能です。
また、ユニットが持つメッシュは画面左側の『Outliner』から確認できます。

コントローラのビジビリティグループを確認してみると、
『trackpad_touch』というグループだけが見つかるはずです。
グループ名をクリックすると、そのグループに含まれるメッシュが確認できます
『trackpad_touch』のグループには
『trackpad_touch』というメッシュだけが含まれています。

画面左側のアウトライナから『trackpad_touch』というメッシュをクリックし、
画面下側の『Viewport』タブを選択すると、
『trackpad_touch』がトラックパッド上のボールを示していることがわかります。



再度ユニットフローを見てみると、同じノードが『トラックパッドから指が離れた』事を表す『Untouched』と接続されています。
先ほどの『Set Unit Visibility』と異なるのは、引数のVisibleがこちらではFalseになっているという点です。

この時点で、『指が触れるとボールが出現し、離れるとボールが消える』という処理になっていることがわかるはずです。

④ボールを移動
最後にボールを出現させる処理から繋がっている『Set Unit Local Position』です。
このノードは引数に設定されたユニットのオブジェクトのローカル座標をPositionの値に変更します。
今回は、trackpad_touchメッシュを②の結果に変更しています。
この部分で『ボールを指に触れた場所へ移動させる』という処理が実装されています。

つまり、このグループ全体では
『トラックパッドに指が触れている間、ボールを可視にし、指が触れている箇所にボールを移動させる。
トラックパッドから指が離れたら、ボールを不可視にする』

という実装が組まれていることになります。



若干中途半端ですが、今回はここまでです。
次回は、このコントローラのユニットフローの中で、
2つか3つのグループの処理を説明していきます。

迷路の自動生成アルゴリズム「穴掘り法」をCoffeeScriptとenchant.jsで実装してみた

前回のブログで、「enchant.jsでさくっとアルゴリズミックなゲームを作る時なんかに良さそうです。」と書いたCoffeeScriptですが、早速enchant.jsとの組み合わせで、代表的なアルゴリズムである「穴掘り法」(正確には穴掘り法に自分なりの解釈を加えたもの)を実装してみました。

分かったことですが、CoffeeScriptはやっぱりとってもコードが書きやすい。

ネスト(ループの入れ子)が多いところも書きやすい。

JavaScriptはカッコが多い言語なので、CoffeeScriptのようにカッコなしでかけるのは本当に気持ちがいいし、とても捗りますね。

やっぱり、こうしたものをコーディングするには丁度良さそうです。

最初に考えたこと

コーディングにかかる前に、迷路生成のアルゴリズムを実装する時にどのように組んでいったらいいか、ざっと考えてみました。

・最初
・・どれかひとつの座標をランダムで選ぶ(ただし、XYともに偶数偶数)
・・「基点の座標一覧」にそこを追加
・・掘る

・掘る
・・ランダムに1方向選び、2マス先の座標が掘れるかたしかめる
・・掘れれば、「基点の座標一覧」にそこの座標を追加し
・・1マス先と2マス先を掘る

・掘れなかったら、それ以外の方向の2マス先の座標が掘れるかたしかめる
・・掘れれば、「基点の座標一覧」にそこの座標を追加し
・・1マス先と2マス先を掘る

・それでも掘れなかったら、それら以外の方向の2マス先の座標が掘れるかたしかめる
・・掘れれば、「基点の座標一覧」にそこの座標を追加し
・・・・「基点の座標一覧」から現在の座標を削除し、1マス先と2マス先を掘る

・それでも掘れなかったら
・・「基点の座標一覧」から現在の座標を削除し、
・・もし「基点の座標一覧」にひとつでもあれば
・・・「基点の座標一覧」からどれかひとつをランダムで選ぶ
・・・その座標で掘る
・・「基点の座標」がもうなかったら
・・・終わり

いよいよ実装へ

digという関数はちょっと凝りました。結局、穴掘り法にちょっと変更を加えたものになりました。

numR = 39
numC = 39
posArr = []
pos = []
directions =[[2,0],[-2,0],[0,2],[0,-2]]

# 地図生成---------------------------
for row in [0...numR]
    pos[row] = []
    for col in [0...numC]
      if row is 0 or
         row is numR-1
          pos[row][col] = 2
      else if col is 0 or
          col is numC-1
              pos[row][col] = 2
      else
          pos[row][col] = 0
# 地図生成おわり---------------------

# 掘る-----------------------------
dig = (tC,tR) ->
  posArr.push([tC,tR])
  directions = diShuffle(directions)
  for direction,i in directions
    if pos[tC + directions[i][0]][tR + directions[i][1]] is 0
      #console.log "hi"
      pos[tC + directions[i][0]/2][tR + directions[i][1]/2] = 1
      pos[tC + directions[i][0]][tR + directions[i][1]] = 1
      dig(tC + directions[i][0],tR + directions[i][1])

#方向決定用に配列の順番をシャッフル
diShuffle = (arr) ->
  e = arr.length
  while e
    num = Math.floor(Math.random() * e)
    qwe = arr[--e]
    arr[e] = arr[num]
    arr[num] = qwe
  arr

#最初の一回目はランダムで選ぶ
randomPick = (numR,numC) ->
  ranR = Math.floor(Math.random() * ((numR-1) / 2 + 1)) * 2
  ranC = Math.floor(Math.random() * ((numC-1) / 2 + 1)) * 2
  if ranR is 0
    randomPick(numR,numC)
  else if ranR is (numR-1)
    randomPick(numR,numC)
  else if ranC is 0
    randomPick(numR,numC)
  else if ranC is (numC-1)
    randomPick(numR,numC)
  else
    return [ranR,ranC]

#いよいよ実行
pickRes = randomPick(numR,numC)
pos[pickRes[0]][pickRes[1]] = 1
dig(pickRes[0],pickRes[1])

# enchant.js部分-------
enchant()
window.onload = ->
  game = new Game(312, 312)
  game.preload "chara5.png","map0.png"
  game.onload = ->
    for mapy,iy in pos
      for mapx,ix in mapy
        map = new Sprite(16,16)
        map.x = ix * 8
        map.y = iy * 8
        map.scaleX = 1
        map.scaleY = 1
        map.image = game.assets["map0.png"]
        if mapx is 1
          map.frame = 2;
        else
          map.frame = 3;
        game.rootScene.addChild map
    game.rootScene.addEventListener "enterframe", ->
  game.start()
  console.log game
  return