imog

主にUnityとかの事を書いています

Unityでシーンをキャッシュする仕組み

2時間遅れの公開になります。遅れてしまった・・。

こちらはUnityアドベントカレンダー、12/5の記事です。

qiita.com

前回は凹さんの「Unity で Windows のデスクトップ画面をテクスチャとして表示するプラグインを作ってみた」でした。

tips.hecomi.com

シーンを保持したい問題

Unityでよく悩む問題として、シーン遷移時に前の状態に戻したいということがたびたびあります。

  • シーンAからシーンBに遷移
  • Bからキャンセル等の動作でAに戻る
  • Aのカーソル位置や状態が初期状態に戻る

RPGのフィールド画面 -> 戦闘画面 -> フィールド画面とか パーティゲームのキャラ選択 -> ステージ選択キャンセル -> キャラ選択に戻る

とか。 つまり、前の状態を保持したままシーン遷移を行いたくなることが多いです。 やり方としては、パッと2パターンくらい浮かびました。

  • シーン遷移時に必要な情報をシリアライズしてシーンを破棄、再読み込み時に流し込む
  • シーンを破棄せずまるごと非アクティブ化。再読み込み時にアクティブにする

前者の方がメモリを食わずに済むので便利そうですが、何をシリアライズするか具体的に決まっていないと作れなさそうな気がしました。 後者は汎用的に使える仕組みではありますが、シーンが残ったままなのでメモリが心配です。特にモバイルだと処理落ちしないかちょっと心配。

今回はこんなやり方どうよというところを紹介したいので、後者を実装します。

シーンのキャッシュ

結論から言うとこんな感じにできました。

以下のことができます。

  • 任意のシーンにロードして以前のシーンを非アクティブ化。キャッシュがある場合はシーンをアクティブ化。
  • 任意の複数のシーンをロードして合成、一つのシーンとしてキャッシュする

追加したクラスは2つで、シーンのロードを担当する SceneManagerWithCache と、シーンの状態を管理する SceneProperty です。

シーンの状態を持つSceneProperty

using UnityEngine;
using UniRx;

public class SceneProperty : MonoBehaviour
{
    public bool cacheable;
    public string sceneName { get { return gameObject.scene.name; } }

    private Subject<Unit> sceneEnabledStream = new Subject<Unit>();
    public IObservable<Unit> sceneEnabledAsObservable
    {
        get { return sceneEnabledStream.AsObservable(); }
    }

    private Subject<Unit> sceneDisabledStream = new Subject<Unit>();
    public IObservable<Unit> sceneDisabledAsObservable
    {
        get { return sceneDisabledStream.AsObservable(); }
    }

    public void Disable()
    {
        sceneDisabledStream.OnNext(Unit.Default);
        foreach (var root in gameObject.scene.GetRootGameObjects())
        {
            root.SetActive(false);
        }
    }

    public void Enable()
    {
        sceneEnabledStream.OnNext(Unit.Default);
        foreach (var root in gameObject.scene.GetRootGameObjects())
        {
            root.SetActive(true);
        }
    }
}

ScenePropertyはシーンのRootのオブジェクトに貼ります。 役割は以下です。

  • シーンがキャッシュできるかどうかの設定を持つ
  • シーンの有効化、無効化処理のインタフェース

例えば絶対に使いまわししないであろうシーン(スコアをサーバに送信したりとか)の場合はcacheableのチェックを外しておき、シーンが使われなくなった時点で削除するようにします。 Enable,Disableメソッドを外部から叩いて、シーン全体のアクティブ、非アクティブ化を行います。 Rootオブジェクトに貼り付けてるのに GetRootGameObjects でルートを取得するのは、後述の複数シーン合成に対応するためです。 ちなみに、アクティブ、非アクティブのタイミングでストリームを発行するので各種シーン側でスクリプトを噛ませてごにょごにょできます。

シーンのロードを担当する SceneManagerWithCache

using System.Collections;
using System.Linq;
using UnityEngine;
using UniRx;
using UnityEngine.SceneManagement;

public class SceneManagerWithCache : MonoBehaviour
{
    [SerializeField]
    private int sceneCacheCapacity = 3;

    public ReactiveCollection<SceneProperty> scenes = new ReactiveCollection<SceneProperty>();

    private void Awake()
    {
        var sceneAddAsObservable = scenes.ObserveAdd()
                                         .Publish()
                                         .RefCount();
        sceneAddAsObservable
            .Where(s => s.Index >= sceneCacheCapacity)
            .Subscribe(s =>
            {
                var unloadScene = scenes.First();
                scenes.Remove(unloadScene);
                SceneManager.UnloadSceneAsync(unloadScene.gameObject.scene);
            });

        sceneAddAsObservable
            .Select(currentScene => scenes.Where(s => !(s.cacheable | s.Equals(currentScene.Value))))
            .Where(unCachedScens => unCachedScens.Count() > 0)
            .Subscribe(unCachedScens =>
            {
                for(int i = 0; i < unCachedScens.Count(); i++)
                {
                    var s = unCachedScens.ToList()[i];
                    scenes.Remove(s);
                    SceneManager.UnloadSceneAsync(s.sceneName);
                }
            });
    }

    public IObservable<SceneProperty> LoadMergedScene(string baseSceneName, bool useCache, params string[] subSceneNames)
    {
        if (useCache)
        {
            IObservable<SceneProperty> propertyObservable = GetCacheScene(baseSceneName);
            if (propertyObservable != null)
            {
                return propertyObservable;
            }
        }

        return LoadScene(baseSceneName, useCache)
            .Take(1)
            .Select(p => p.gameObject.scene)
            .Concat(Observable.FromCoroutine<Scene>(observer => LoadSubSceneCoroutine(observer, subSceneNames)))
            .Do(loadedScene =>
            {
                var baseScene = SceneManager.GetSceneByName(baseSceneName);
                SceneManager.MergeScenes(loadedScene, baseScene);
            })
            .Select(_ => scenes.Last(s => s.sceneName == baseSceneName));
    }

    private IEnumerator LoadSubSceneCoroutine(IObserver<Scene> observer, params string[] subScenes)
    {
        foreach (var s in subScenes)
        {
            yield return SceneManager.LoadSceneAsync(s, LoadSceneMode.Additive)
                .AsObservable()
                .ToYieldInstruction();
            observer.OnNext(SceneManager.GetSceneByName(s));
        }
        observer.OnCompleted();
    }

    public IObservable<SceneProperty> LoadScene(string name, bool useCache = true)
    {
        if (useCache)
        {
            IObservable<SceneProperty> propertyObservable = GetCacheScene(name);
            if (propertyObservable != null)
            {
                return propertyObservable;
            }
        }

        return SceneLoadedAsObservable(name);
    }

    private IObservable<SceneProperty> GetCacheScene(string sceneName)
    {
        var scene = scenes.LastOrDefault(s => s.sceneName == sceneName && s.cacheable);
        if (scene == null)
        {
            return null;
        }
        scene.Enable();
        var beforeScene = scenes.Last();
        if (beforeScene != null)
        {
            beforeScene.Disable();
        }
        scenes.Remove(scene);
        scenes.Add(scene);
        return Observable.Return<SceneProperty>(scene);
    }

    private IObservable<SceneProperty> SceneLoadedAsObservable(string name)
    {
        return SceneManager.LoadSceneAsync(name, LoadSceneMode.Additive)
                  .AsObservable()
                  .Select(_ => SceneManager.GetSceneByName(name))
                  .Select(scene => scene.GetRootGameObjects().First().GetComponent<SceneProperty>())
                  .Do(property =>
                  {
                      var beforeScene = scenes.LastOrDefault();
                      if (beforeScene != null)
                      {
                          beforeScene.Disable();
                      }
                      scenes.Add(property);
                  });
    }
}

100行超えてつらい。

外部から呼べるメソッドは二つです。

  • LoadScene
  • LoadMergedScene

LoadSceneSceneManager.LoadScene と同じ感覚で、シーン名を指定してロードします。 通常時と違うのは、キャッシュを使うか否かbooleanで渡せるところです。 useCache = trueで渡すと、すでにシーンが非アクティブで存在する場合そちらを有効化します。 見つからなかった場合は通常のシーン読み込みが行われます。

内部的にはロード時は以下のフローです

  • キャッシュが無い場合SceneManager.LoadSceneAsync でシーンロード
  • キャッシュがある場合、該当シーンから ScenePropertyを取得して有効化
  • 直前のシーンを無効化
  • シーン管理リストに追加
  • キャッシュ数上限を超える or キャッシュしないシーンをリストから削除、 SceneManager.UnLoadAsyncヒエラルキーから削除

呼び出し元には IObservable<SceneProperty> を返して、ロード完了後にイベントを発行します。 完了後のなにかしらはシーン側にお任せします。

キャッシュ数上限は sceneCacheCapacity が保持しており、インスペクタ上から設定できます。

LoadMergedScene は、シーン名を指定してロードするのは同じですが、こちらは複数ロードすることができます。 ベースとなるシーンAと、可変長引数のシーン名配列を渡すことで、該当のシーンをすべてロードした上でシーンAに統合してくれます。

これは、1つのシーンを機能ごとに分割して管理をしている場合に有効です。 例えばデザイナと分担するためにシステムとUIをシーン分けて作業しているけど、実際に使うときは完全に統合したいんだよみたいなときとか。 どういうシチュエーションだろうか、というところは前回のエントリなどを見ていただけるとイメージがつかめるかもしれません。

adarapata.hatenablog.com

こちらも最終的な返り値は IObservable<SceneProperty> なので通常のシーン読み込みと同じように扱えます。

ちなみに呼び出し側はこんな感じ。

GetComponent<ObservableEventTrigger>()
    .OnPointerClickAsObservable()
    .Subscribe(_ => {
        var manager = FindObjectOfType<SceneManagerWithCache>();
        manager.LoadScene(sceneName).Subscribe(s => print("loaded: " + s.sceneName));
    }

とりあえず、この2つのスクリプトでキャッシュ機構は実現できそうです。

作ってみて

どのプロジェクトにも持っていけるので結構便利。 重さに関しては、少なくともPCで動かして処理落ちなどはないです。 ただモバイルは未検証。正直きついのでは・・と思う。

処理がかなりUniRxゴリゴリ書いているんだけど、Do多用して副作用多かったりだいぶ辛い感じなのでアドバイスいただきたいところ・・。

次は utibenkeiさんです。 qiita.com

UnityFukuoka12をやりました、発表しました。

5月くらいに11をやったので半年ぶり。

atnd.org

僕の発表はこちら speakerdeck.com

主に、マルチシーンを使うと便利だぞという話と、Prefabでの管理をSceneにできないかという話。

以下、スライドの補足。

今回なんでマルチシーンをテーマにしたかというとしたかというと実際にやってみたら楽だったから今後はスタンダードになるだろうと思ったのと、それにしてはその辺の話をした記事が調べても出てこなかったから。

このスライドはその辺の未来にもちょっと言及していたので共感できた。

www.slideshare.net

個人的にはPrefabは再利用するための仕組みであって、オブジェクトを保存する仕組みではないと考えている。 なので、複数のシーンにおいたりInstantiateしないのであれば直接シーンファイルの中だけに存在していた方が不要な小さいミスが減らせるんじゃないかなと思ってる。 例えばPrefab化しつつシーンに配置していたとき、以下の問題が起こりうる

  • PrefabとHierarchy上で微妙にパラメータが違ってることに気づかずウォォォ
  • 逆に誰かが知らずにPrefab側の値を変えてScene内で影響出ててウォォォ

小さいエラーで、修正もすぐできるんだけど頻発する可能性はあるし発生しないに越したことはない。 どちらも結局HierarchyとPrefabの二重管理が引き起こす問題だと思う。 特定のシーンでしか使わないならシーンにだけ存在した方が気持ちが楽になりそう。

多分この気持ちはみんな持っていてあまりPrefabを量産しない気概はあると思うんだけど、そうなると1シーンをエンジニア・デザイナが触る状況が発生してしまい、そこでコンフリクトしないように「Prefabにしてシーンに持っていきますね」ということになってるんだと思う。 そこで、じゃあそのPrefabをシーンにしませんか?という話をしたのが今回の発表ですた。

ただ、これを実現しようとしたときやはりシーン間のオブジェクトの依存関係をどうするかという問題が出てくる。 Prefabだとドラッグ&ドロップで参照できたのにどうしてもスクリプトを書かなくてはならなかったり。 ここをいい感じにできる方法がないものか。

ちなみに、リビングデイブでのシーン間のオブジェクトの依存は以下

  • ゾンビがデイブを追いかける(ゾンビはRoom, デイブはHumanControlUI)
  • デイブのライフが0になるとゲームオーバー(デイブはHumanControlUI、ゲームオーバー処理はMainGame)

依存先がどちらもユニークだったので、今回は FindObjectOfType で取ってきている。 これが複数のオブジェクトのうちの一個とかだとFindで決め打ちだのなんだのちょっと面倒な形になりそう。

ただ、個々のオブジェクトでFindだーとか書きまくってると管理に漏れが発生しそうなので一元管理した方がいいのだろうかとか思ったりした。どうやるかは考えてない。

シーン間依存問題とか絶対起きてるだろうからみんなのベストプラクティスをくれ!

ArborでUniRxを使いやすくするArborRxを作った

ということでArborRxというアセットをgithubに公開しました。

github.com

Arborとは

ケットシーウェアさんがアセットストアで販売しているアセットです。

https://www.assetstore.unity3d.com/jp/#!/content/47081

GUIで状態遷移を簡単に実装できるのが特徴です。 基本機能が揃っていているのと、結構ガシガシ自分でコード書けるので自分は愛用しています。

UniRxとは

UnityでReactiveExtensionsを実現するアセットです。

github.com

説明するとものすごく長くなるので割愛します。

ArborRxとは

その名の通り、ArborとUniRxを連携させるアセットです。 具体的に連携とは何かというと、まだ1機能しかないです。

  • GUI上でストリームを購読して遷移させる

リポジトリにも貼ってるやつですがこんな感じです。

ObserveTransitionは、同じState内のスクリプトを全部読んで IObservable<Unit> を返すメソッドを列挙します。 エディタ上で指定したメソッドが返すIObservable<Unit> をObserveTransitionが購読して、発行されたら任意のStateに遷移する仕組みです。 それまでもStateBehaviour内部でRxで書いてはいたけど、遷移条件が増えるほどTransition(state)を書き加えていく必要があったので、外出しできないかな?とやってみたのが今回です。 リフレクションする必要があったので若干面倒かと思ったが意外とさっくりできた。

これによって何が嬉しいかというと、StateBehaviourが遷移条件とか一切気にしなくて済むことです。 遷移はすべてObserveTransitionに任せて本来の処理だけを書いていけるので少し見晴らしがよくなります。 とはいえ、ストリームを定義するのは処理側なので実際のところはまだ気にする必要はあるんですけど。

今後はObserveTransitionにオペレータを付けれるようにしたいところ。 あとはUnit以外の型も許容するとか。個人的には通知してもらえればよいのでUnitでいいと思っており、やるかは微妙。

みなさまのプルリクお待ちしております。

第43回 八時間耐久作品制作会(仮) に行ってきた

(仮)までが正式名称 atnd.org

今回もゆったりとした雰囲気だった。

自分は前回から機能のパッケージ化が面白くなってきたので、今回も小さい機能を作っていた。

できたのがこちら。ミッコと呼びます。 github.com

Mikko is

Mikkoは、プラットフォームを気にすることなくタッチ処理を実装できるパッケージです。

Unityでスマホ向け開発を行うときに、タッチの挙動を調べるために Input.GetTouch などでTouch構造体を取り出すのが一般的だと思う。

しかし、このタッチの状態は実機だと問題ないけどエディタ上で再生したときに取得できないため、エディタ上での場合と実機の場合を両方書く、もしくはUnityRemoteなどで実機のみで確認する、みたいな対応が多かった。

ちなみにそれを解決するアセットはあるっぽいし、UnityもClossPlatformInputと言うものを提供している。

Unity5版Standard Assetsを使って仮想ジョイスティックを実装 - Qiita

前者は有料だったりするのと、後者はちょっと大袈裟すぎる時もあったのでシンプルなものを作ってみた。

使い方

タッチ関連の処理をエディタとスマホ実機で動かす場合、ざっくりとこんな感じ。

using UnityEngine;
using System.Collections;

public class Hoge : MonoBehaviour {
    void Update () {
        if(Application.isEditor){
            if(Input.GetMouseButtonDown(0)) {
                // タッチした瞬間の処理
            }
            else if(Input.GetMouseButtonUp(0)) {
                // 放した瞬間の処理
            }                
        }
        else if(Input.touchCount > 0) {
            var t = Input.GetTouch(0);
            if(t.phase == TouchPhase.Began) {
                // タッチした瞬間の処理                
            }else if(t.phase == TouchPhase.Ended){
                // 放した瞬間の処理                
            }
        }
    }
}

Mikkoを使うとこんな感じ

using UnityEngine;
using System.Collections;
using GuP;

public class Hoge : MonoBehaviour {
    void Update () {
        ITouch t = Mikko.input.touch;
        if(t.info == TouchInfo.Began) {
            // タッチした瞬間の処理
        } else if(t.info == TouchInfo.Ended) {
            // 放した瞬間の処理 
        }
    }
}

エディタだろうがスマホだろうがお構いなしにできます。

内部的にはMikkoはシングルトンになっており、シーン上に一つだけDontDestroyOnLoadで生成されます。

現在取れるのは三つだけ

  • position: タッチした座標
  • deltaPosition: 前回との差分ベクトル
  • info: タッチの状態

必要になったらガシガシ追加していこうと思う。 UnityPackageはこちらに

https://github.com/adarapata/Mikko/releases/download/0.1.0/Mikko_0_1_0.unitypackage

八耐感想

Vive使ったガンシューティングが面白かった。 www.instagram.com

難易度は高めだったけど、もっとのめり込めるFPSみたいになってて満足度高かった。

だけど後輩が、HMD装着して周りが見えない僕の頭皮を積極的に撮影しようとしていたのでVRは危険だと強く感じた。

Unityでシンプルなポップアップを実装する

スマホゲーとかでよく出るやつを作ってみた。

f:id:adarapata328:20160725232256g:plain

github.com

最近だと横からニュッと出るやつとか下からシュッと出るやつとかあるけど、今回は昔からあるダイアログ形式のやつを実装してみた。

まずは、シンプルに任意の時間でオブジェクトを拡大縮小させるTweenScaleクラスを作る。

UniRxがインポートされている前提なのでいますぐアセットストアから落としてこよう。

TweenScale.cs

using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;
using UnityEngine.Events;
using System;

[Serializable]
public class TweenScale {
    public Vector3 from, to;
    public float duration;
    public AnimationCurve curve = new AnimationCurve(new Keyframe(0,0), new Keyframe(1,1));
    public UnityEvent onBegin, onEnd;
    private GameObject target;

    private Subject<Unit> scaleStartStream = new Subject<Unit>();
    public IObservable<Unit> scaleStartAsObservable { get { return scaleStartStream.AsObservable(); } }

    private Subject<Unit> scaleEndStream = new Subject<Unit>();
    public IObservable<Unit> scaleEndAsObservable { get { return scaleEndStream.AsObservable(); } }

    public void Setup(GameObject t)
    {
        target = t;
        scaleStartAsObservable.Subscribe(_ => onBegin.Invoke());
        scaleEndAsObservable.Subscribe(_ => onEnd.Invoke());
        scaleEndAsObservable.Subscribe(_ => target.transform.localScale = Vector3.Lerp(from, to, curve.Evaluate(1.0F)));
    }

    public void Play()
    {
        scaleStartStream.OnNext(Unit.Default);
        Observable.EveryFixedUpdate()
            .Take(System.TimeSpan.FromSeconds(duration))
            .Select(_ => Time.fixedDeltaTime)
            .Scan((acc, current) => acc + current)
            .Subscribe(time => {
                float t = time / duration;
                target.transform.localScale = Vector3.Lerp(from, to, curve.Evaluate(t));
            },
            _ => {},
            () => scaleEndStream.OnNext(Unit.Default)
            ).AddTo(target);
    }
}

結構ゴチャゴチャしてるけど、実際の拡大縮小処理はこのストリームにまとまっている

scaleStartStream.OnNext(Unit.Default);
Observable.EveryFixedUpdate()
    .Take(System.TimeSpan.FromSeconds(duration))
    .Select(_ => Time.fixedDeltaTime)
    .Scan((acc, current) => acc + current)
    .Subscribe(time => {
        float t = time / duration;
        target.transform.localScale = Vector3.Lerp(from, to, curve.Evaluate(t));
    },
    _ => {},
    () => scaleEndStream.OnNext(Unit.Default)
    ).AddTo(target);

呼ばれた時点で scaleEndStream.OnNext(Unit.Default) を呼んでスケーリング開始時のイベントを発行する。 画像だとその時点で暗転用のオブジェクトをActiveにしている。

線形補間でスムーズにスケーリングさせるので、経過時間(time) / スケーリングにかける時間(duration)で現在の位置tを出す。

tを出したら挙動はAnimationCurveが持ってるので curve.Evalute(t) で現在の値を返すようにする。

duration秒経過したらストリームが終了するのでその時点で scaleEndStream.OnNext(Unit.Default) を呼んで終了時のイベントを発行する。

で、開くときと閉じる時の挙動は別々で用意したいことが多い気がしたので、上記クラスを二つ持たせたPopupコンポーネントを定義する。

Popup.cs

using UnityEngine;
using System.Collections;
using UniRx;

public class Popup : MonoBehaviour {
    public enum State { Open, Close, UnUsed }

    public State state { get; private set; }
    public TweenScale open, close;
    void Start()
    {
        open.Setup(gameObject);
        open.scaleEndAsObservable.Subscribe(_ => state = State.Open);
        close.Setup(gameObject);
        close.scaleEndAsObservable.Subscribe(_ => state = State.Close);
    }

    public void Open()
    {
        open.Play();
    }
    public void Close()
    {
        close.Play();
    }

    public void Toggle() {
        switch(state)
        {
        case State.UnUsed:
        case State.Close:
            open.Play();
            break;
        case State.Open:
            close.Play();
            break;
        }
    }
}

このコンポーネントはOpenとCloseしかできないシンプルなコンポーネント。 Toggleは開いてたら閉じる、閉じてたら開くけどあんまり使わないかもしれない。

実際にポップアップ使うとなると、ただオブジェクトが飛び出てくるだけでなくそれに付随した様々な処理が入ってくると思う。サンプルみたいに他の部分をタッチできないように暗幕を置いたりするなど。

その辺を色々考慮し始めるとキリがないので、UnityEventsを使ってインスペクタから別のオブジェクトにメッセージ飛ばせるようにしている。

https://i.gyazo.com/f7b488f32f4cc744dafe6154da94fa57.png

こっちの方がデザイナが演出とかの組み込みがやりやすそう。

スクリプトで書きたいなら scaleStartAsObservablescaleEndAsObservable プロパティがあるのでそこから購読できる。

ちなみにUnityEventsを使ってはいるが、最終的には scaleStartStreamscaleEndStream の二本のストリームに集約しているので若干コードはすっきりした。

https://i.gyazo.com/dac543bd6dabb94eb69350503f247ec7.gif

入れ子みたいにしても気持ちいい。

UnityPackageにしたのでご自由にお使いください。 https://github.com/adarapata/SimplyPopup/releases/download/0.1.0/SimplyPopup_0_1.unitypackage

おまけ

生成したポップアップオブジェクトのscaleEndAsObservableを購読して数珠繋ぎに消す

Japan VR Hackathon2016に参加してきました in Fukuoka

ブログ書くまでがハッカソンらしい。 土日でやってきた。

Japan VR Hackathon(JVRH)とは

こちらに All Japan VR Hackathon

  • 二日間でVRを利用したアプリを作るぞ
  • みんなでチーム組んでやるぞ

というイベントです。 VRというテーマなので、会場である程度のデバイスが用意されている。 OculusとかViveなど、この手のデバイスはそれなりに高価なのでそれが好きに使えるのはとても嬉しい。 全国7カ所で同時開催されており、今回は地元の福岡会場にお邪魔してきた。

ちなみにVRでの開発はやったことないけどノリでなんとかなった

福岡会場の様子

どういう経緯かはよくわからないけど会場が個人の家になっていて、個人宅に13人くらいが押しかけてハッカソンが行われた。 東京が300人入る会場で開催している中、僕たちは居間と和室と作業部屋で、机も足りないから時にはダンボール箱を駆使し開発に専念していた。

夜にピザとビール買ってきて宴会したのも福岡会場だけだと思う。

作ったもの

  • チーム名: ソースコ
  • タイトル: VR HANASAKA
  • メンバー: プログラマ*2、 デザイナ*1、サウンド*1

発表資料 VRHanasaka.key - Google ドライブ

実行ファイル(Windows) vrhanasaka.zip - Google ドライブ Myo2台とHMDがないと遊べません!

動画は発表資料の中に埋め込まれています。緑の服着て腕をブンブン振ってるのが僕です。

テーマが「日本らしさ」だったのでそこから広げて最終的に「花咲か爺さんになって枯れ木に花を咲かせるゲーム」となった。

左手を広げると花を咲かせる粉を発射し、右手を広げると鬼を滅する豆を発射。 鬼が果敢に桜の木を自爆して壊そうとするので、豆で滅しながら粉で桜を咲かせたらクリア。

VR使ってるけど、鬼と桜と演出、だいたい2Dなのが他のゲームとは違うポイント。ペラッペラの板ポリをVRで見るのは割と面白い。

あと、流れてるBGM3曲は全て今回のハッカソンで作られたもの。すごい

使ったデバイ

www.oculus.com

もはや説明不要のHMD。ハコスコとかCardBoardを使ったことはあるけど、やはりOculusの没入感は一歩抜きん出てた。

  • Myo (179$)

www.myo.com

腕に装着するタイプのデバイス。筋電によるジェスチャーコントロールができる。

サンプルの時点でグー、パー、ウェーブイン、ウェーブアウトが検知できる。生データも取れるのでやろうと思えば何でもできるみたい。

今回はこちらを二つ借りて両腕を検知した。贅沢の極みである。

チームの話

ハッカソンなので、その場でサクッとチーム決め。 今回は全員ほぼ初対面というところでアイスブレイクから始まった。 デザイナ・サウンドがいてプログラマが二人、且つプログラマは片方がVR経験者で片方がUnity得意と分野が綺麗に分かれていたので綺麗な作業分断ができた。

自分はゲーム本体の部分を開発して、もう一人はMyoデバイスの動作検証と導入を担当した。 Oculusの検証時間もいるかなと思ってたが、導入して一切問題も何も起きなかったので不要だった。Oculus優秀。

また、メンバーも全員優秀で特に詰まるみたいな状況にならなかったので比較的やりやすかった。 発表資料もデザイナさんがサクサクっとやってくれたし非常に良いチームだったなと思う。

VR使った開発やってみて

だいたいみっつ。

没入感がすごい

そもそもこれがウリなんだから当たり前なんだろうけど。 開発初期でとりあえずと画面上に青鬼(ホラー系だから自分でググってね!)の画像を板ポリに貼りつけてたんだけど、普通に悲鳴を上げてしまうくらい恐怖を感じた。

画面とは違うなんか一線を超えている感じがすごくやばい。 特にホラー系はメチャクチャ面白くなりそう。僕はやりたくない。

ゲームデザインが違う

HMDはあんまり長く装着すると辛い。自分も酔う方なので3分くらいが限界だった。 そうなると、気持ち悪くならないために1プレイのゲーム時間を結構意識する必要が出てきた。 VR-HANASAKAは割と画面が動くゲームなので、1プレイが長くても1分くらいで終わるような時間にしている。 それ以上超えると自分が辛い。

なので、死ぬまで頑張るみたいな耐久系ゲームは本当に耐久になってしまうのでは感じた。

あと、UI周りがいつもと比べてかなり苦労することになった。

通常のゲームだと、プレイヤーのパラメータやスコアなどは画面四隅に表示しておくというやり方がスタンダードだが、これがVRで見ると全然しっくりこなかった。まず、四隅を見ない。真ん中にしか意識が向かない。目線だけ向ければ見れるんだけど非常に辛かった。

現実世界で、オブジェクトが常に視界の固定位置にいることがしっくりこない。その方向に首を動かしても位置関係が常に変わらないというのが違和感の正体かもしれない。特に背けることもできないのがきつい。

ということでVR-HANASAKAでは看板のようなオブジェクトにスコアを書いて、顔を上に向ければ見えるくらいの位置に常駐させておくという実装にした。 「今何点だろう」という確認で上を向くのはしっくりきた。

デバッグが辛い

動作確認の度にOculusを装着するのは目の負担が凄まじく、普段のハッカソンの2倍は疲れていた。

後半は装着せずに頭に乗っけたまま首を動かしてコントロールするスキルを身につけた。

所感

打ち上げで南部さんとちょろっと話したけど、「ハッカソンは何をするかより誰とやるか」という南部さんの話は僕も同意で、今まで会ったことない人たちとその場で何かを作り、作ったものを語るという行為が面白いんじゃないかと思ってる。

福岡のハッカソンに来る人たち、基本的に良い意味でなんかおかしいので考えとか発想が面白い人が多い印象がある。 まず、隣で開発してたチームから「VRで鼻を伸ばして、その鼻を切り落としてミサイルのように飛ばしてバトルする」ゲームを作っていてよくわからなかったし。

チーム内の話もそうだけど他チームの人に何で作ったのかとかを聞くのも楽しくて、それがハッカソンの醍醐味だと思う。 なので、今回のような個人宅で酒を飲みながら開発するタイプのハッカソンは、個人宅故に全員が近く、終始なごやかな空気だったので割と良いのではないかと思った。 またやってほしい。

打ち上げのイカが美味しかったです。 www.instagram.com