20201201のUnityに関する記事は14件です。

Unity DOTween 入門

この記事はUnity #2 Advent Calendar 2020の5日目の記事です

はじめに

  • DOTweenというライブラリを使うとオブジェクトをアニメーションさせたり、マテリアルの色を徐々に変化させたい場合に非常に便利です
  • アニメーションクリップを作ることなく動きを作成できるので、プログラマだけでアニメーションを実装することができます
    • デザイナーにアニメーションを作ってもらうまでのモックとして利用することもできます
  • 今回は基本的な使い方、よく使う拡張、コールバック、SequenceをDOTween入門として紹介します

参考

DOTween Asset Store
https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676?locale=ja-JP

公式ドキュメント
http://dotween.demigiant.com/

セットアップ

Asset Storeでdotweenと検索して入手します
PRO版は内部のソースコードが見れたりしますが、基本的には無料版で問題ありません。
image.png

Importが完了したら初期セットアップを行います
Open DOTween Utility Panelを開き
image.png

Setup DOTween...を選択し、Applyでセットアップは完了です
image.png
image.png

基本編

まず動かしてみる

3秒かけて(5,0,0)へ移動するというコードを書いてみます。
UpdateやCoroutineを使って書こうとすると大変ですが、DOTweenを使えば1行で書くことができます。
このコードから拡張していろいろな機能を紹介していきます
move.gif

using DG.Tweening;  //DOTweenを使うときはこのusingを入れる
using UnityEngine;

public class MoveTest : MonoBehaviour
{
    void Start()
    {
        // 3秒かけて(5,0,0)へ移動する
        this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f);
    }
}

繰り返す SetLoops

SetLoopsを使うと、動作を繰り返すことができます
繰り返す回数(loops)と繰り返し方法(loopType)を設定します
loopsに-1を入れるとずっと繰り返す挙動になります

loop.gif

void Start()
{
    //(5,0,0)へ1秒で移動するのを3回繰り返す
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).SetLoops(3,LoopType.Restart);
}

なお、Loop Typeパラメータ別の挙動は下記のようになっています

Loop Type 説明
Yoyo 移動後、最初の位置に戻るように動きます
Restart 移動後、最初の位置から再び移動を開始します
Increment 移動後、その位置から再び移動を開始します

loop2.gif

遅延 SetDelay

SetDelayを使うことで動作が開始するのを遅延させることができます

void Start()
{
    //3秒待ってから(5,0,0)へ1秒で移動する
    this.transform.DOMove(new Vector3(5f,0f,0f), 1f).SetDelay(3f);
}

Ease

SetEaseを使うと始点と終点をどのように繋ぐか設定することができます

ease.gif

void Start()
{
    //(5,0,0)へ1秒でリニア移動する
    this.transform.DOMove(new Vector3(5f,0f,0f), 1f).SetEase(Ease.Linear);
}

Easeには様々な種類があります。
実際使う際はいろいろなパラメータを変えながら、作りたい動きを探すのがいいでしょう。
image.png
https://github.com/Nightonke/WoWoViewPager/blob/master/Pictures/ease.png

なおSetEaseを使わない場合はOutQuadがデフォルトで設定されるようです。

全部一緒に使う

これらのパラメータは一度に設定して使用することができます

void Start()
{
    //2秒待ってから(5,0,0)へ3秒で移動するのを4回(2往復) OutBounceで行う
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f).SetDelay(2f).SetLoops(4, LoopType.Yoyo).SetEase(Ease.OutBounce);
}

読みやすくするためにドットで改行して書くことも可能です

void Start()
{
    //2秒待ってから(5,0,0)へ3秒で移動するのを4回(2往復) OutBounceで行う
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f)
                    .SetDelay(2f)
                    .SetLoops(4, LoopType.Yoyo)
                    .SetEase(Ease.OutBounce);
}

止める Kill

DOTweenの実行を止める方法を4つ紹介します。
シチュエーションに合った止め方を選択してください。
Killするとオブジェクトは移動中であってもその場で停止します。

Tween tween;

void Start()
{
    this.tween = this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).SetLoops(-1, LoopType.Yoyo);
}

void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
        //返り値を保存しておいて止める方法
        this.tween.Kill();
    }

    if(Input.GetKeyDown(KeyCode.S))
    {
        //参照元を指定して止める方法
        this.transform.DOKill();
    }

    if(Input.GetKeyDown(KeyCode.D))
    {
        //Objectを指定して止める方法
        DOTween.Kill(this.transform);
    }

    if(Input.GetKeyDown(KeyCode.F))
    {
        //全ての実行を止める方法
        DOTween.KillAll();
    }
}

いろいろな拡張

移動させるDOMove以外の拡張を紹介します

ローカル座標での移動 DOLocalMove

//ローカル座標の(5,0,0)へ3秒で移動する
this.transform.DOLocalMove(new Vector3(5f, 0f, 0f), 3f);

X,Y,Zの移動量を指定して移動する

//現在の座標からX+5の座標へ3秒で移動する
this.transform.DOMoveX(5f, 3f);

//現在の座標からY+5の座標へ3秒で移動する
this.transform.DOMoveY(5f, 3f);

//現在の座標からZ+5の座標へ3秒で移動する
this.transform.DOMoveZ(5f, 3f);

ジャンプして移動する DOJump

jump.gif

//(5,0,0)の位置に4秒で2回ジャンプして移動する
this.transform.DOJump(new Vector3(5f, 0f, 0f), jumpPower: 3f, numJumps: 2, duration: 4f);}

jumpPowerの値を調整するとジャンプの高さが変わります

回転 DORotate

rotate.gif

//1秒でRotationが(0,180,0)になるように回転する
this.transform.DORotate(Vector3.up * 180f, 1f)

第3引数に回転方法を設定できます

public enum RotateMode
{
    //
    // 概要:
    //     Fastest way that never rotates beyond 360°
    Fast = 0,
    //
    // 概要:
    //     Fastest way that rotates beyond 360°
    FastBeyond360 = 1,
    //
    // 概要:
    //     Adds the given rotation to the transform using world axis and an advanced precision
    //     mode (like when using transform.Rotate(Space.World)).
    //     In this mode the end value is is always considered relative
    WorldAxisAdd = 2,
    //
    // 概要:
    //     Adds the given rotation to the transform's local axis (like when rotating an
    //     object with the "local" switch enabled in Unity's editor or using transform.Rotate(Space.Self)).
    //     In this mode the end value is is always considered relative
    LocalAxisAdd = 3
}

FastFastBeyond360の比較すると図のようになります。
どちらもYが0度から350度へ回しています
fast.gif

this.transform.DORotate(Vector3.up * 350f, 1f, mode: this.rotateMode)

WorldAxisAddLocalAxisAddは現在の角度から何度回転させるかという操作になります
回転させるという意味ではこちらの方が直感的かもしれません

色を変える DOColor

color.gif

[SerializeField]
Renderer rendererComponent;

void Start()
{
    //1秒で赤色に変える
    this.rendererComponent.material.DOColor(Color.red, 1f);
}

MaterialだけでなくImageにも使うことができます

color2.gif

[SerializeField]
UnityEngine.UI.Image image;

void Start()
{
    //1秒で赤色に変化し元の色に戻るのをずっと繰り返す
    this.image.DOColor(Color.red, 1f).SetLoops(-1,LoopType.Yoyo);
}

アルファを変化させる DOFade

fade.gif

[SerializeField]
UnityEngine.UI.Image image;

void Start()
{
    //1秒でImageのアルファを0にする
    this.image.DOFade(endValue: 0f, duration: 1f);
}

DOFadeはImageだけでなくCanvasGroupやMaterialにもつかうことができます

//1秒でCanvasGroupのアルファを0にする
this.canvasGroup.DOFade(endValue: 0f, duration: 1f);

//1秒でMaterialのアルファを0にする
this.rendererComponent.material.DOFade(endValue: 0f, duration: 1f);

音量のフェードを行う DOFade

また、AudioSourceもDOFadeで操作することができます

[SerializeField]
AudioSource audioSource;

void Start()
{
    //1秒でAudioSourceのVolumeを0にする
    this.audioSource.DOFade(endValue: 0f, duration: 1f);
}

指定したポイントを通過する DOPath

path.gif

Vector3[] path =
{
    new Vector3(0f,0f,10f),
    new Vector3(5f,0f,10f),
    new Vector3(5f,0f,0f),
    new Vector3(0f,0f,0f)
};

//指定したPathを10秒で通り、進行方向を向く
this.transform.DOPath(path, 10f).SetLookAt(0.01f);

DOPathにはVector3の配列でPathを渡すことができます
またSetLookAtで進行方向に向けることができます
また、DOPathの実行中はシーンビュー上にPathが表示されます

コールバックを使う

DOTweenは実行時に各種タイミングでコールバックを呼び出すことができます
代表的なものを紹介していきます

完了時のコールバック OnComplete

//(5,0,0)に2秒で移動し、移動が完了したらログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(() =>
{
    Debug.Log("OnComplete!");
});

ラムダ式を使わず、このように書くこともできます

void Start()
{
    //(5,0,0)に2秒で移動し、移動が完了したらログを出す
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(CallbackFunction);
}

void CallbackFunction()
{
    Debug.Log("CallbackFunction");
}

OnComplete内でさらに新しい動作を登録することもできます

//(5,0,0)に2秒で移動し、移動が完了したらY軸180度に1秒で回転する
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(() =>
{
    this.transform.DORotate(Vector3.up * 180f, 1f);
});

complate.gif

実行開始時のコールバック OnStart

DOTweenの実行時にコールバックが呼び出されます。

//(5,0,0)に2秒で移動し、移動開始時にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnStart(() =>
{
    Debug.Log("OnStart");
});

なお、SetDelayなどを使って遅延させた場合は遅延終了後にOnStartが呼ばれます

Debug.Log(Time.realtimeSinceStartup);

//(5,0,0)に2秒で移動し、移動開始時にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnStart(() =>
{
    Debug.Log("OnStart"+ Time.realtimeSinceStartup);
}).SetDelay(2f);
1.700801
OnStart3.848413

実行中のコールバック OnUpdate

実行中に毎フレーム呼び出されるコールバックです

//(5,0,0)に1秒で移動し、移動中にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).OnUpdate(() =>
{
    Debug.Log($"[{Time.frameCount}] OnUpdate {this.transform.position}");
});
[1] OnUpdate (0.2, 0.0, 0.0)
[2] OnUpdate (0.4, 0.0, 0.0)
...
[60] OnUpdate (5.0, 0.0, 0.0)

停止時のコールバック OnKill

Killが呼び出されたときのコールバックです

void Start()
{
    //(5,0,0)に10秒で移動し、Killされたらログを出す
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).OnKill(() =>
    {
        Debug.Log("OnKill"+transform.position);
    });
}

private void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
        this.transform.DOKill();
    }
}

なお移動完了時もOnKillは呼び出されます
下記のようにOnKillとOnComplete両方を登録した場合
移動完了時に両方のコールバックが呼び出されます。
途中でKillした場合はOnKillのみが呼び出されます

//(5,0,0)に10秒で移動し、Killされたらログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f)
    .OnKill(() =>
    {
        Debug.Log("OnKill"+this.transform.position);
    })
    .OnComplete(()=>
    {
        Debug.Log("OnComplete" + this.transform.position);
    });

Sequence

DoTweenを連続で使用する場合、OnCompketeを使って繋げるのもいいのですが
Sequenceという機能を使うと連続的な挙動を簡単に実装できます

seq1.gif

//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play();

同時に実行する Join

Joinを使うと移動しながらスケールを変えるといった動作が可能になります

seq2.gif

//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));
//Joinはひとつ前の動作と同時に実行される
sequence.Join(this.transform.DOScale(Vector3.one * 2f, 1f));

//Playで実行
sequence.Play();

待機する AppendInterval

AppendIntervalを使うことでSequenceの途中で待機を入れることができます
seq3.gif

//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
//AppendIntervalで途中に待機を入れられる
sequence.AppendInterval(3f);
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play();

止める

SequenceもKillを使って止めます

//メンバ変数でSequenceのインスタンスを作成
Sequence sequence = DOTween.Sequence();

void Start()
{
    //Appendで動作を追加していく
    sequence.Append(this.transform.DOMoveX(5f, 2f));
    sequence.Append(this.transform.DOMoveY(2f, 1f));

    //Playで実行
    sequence.Play();
}

void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
        this.sequence.Kill();
    }
}

コールバック

Sequenceに対してもコールバックを設定することができます

//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play()
    .OnStart(() =>
    {
        //開始時に呼ばれる
        Debug.Log("OnStart");
    })
    .OnUpdate(()=>
    {
        //実行中に毎フレーム呼ばれる
        Debug.Log("OnUpdate");
    })
    .OnComplete(() =>
    {
        //完了時に呼ばれる
        Debug.Log("OnComplete");
    })
    .OnKill(()=>
    {
        //Kill時に呼ばれる
        Debug.Log("OnKill");
    });

まとめ

DOTweenのいろいろな使用方法を見てきました
今回紹介した拡張だけでなくDOTweenには数多くの拡張が存在します。
ぜひいろんな使い方を探ってみてください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity as a LibraryによるUIネイティブ化

こんにちは、mizogucheです。2015年9月にクラスターに入ったのでもう6年目です。

この記事はクラスター Advent Calendar 2020 - Qiitaの2日目の記事です。

Unity UIでの課題

UnityでUIを実装すると普段使っているネイティブアプリの操作性と違った触り心地になります。

ネイティブアプリで簡単に実装できるUIの操作性をUnityで再現するコストはかなり高く、たとえばiOSにおける戻るジェスチャ、Androidにおけるバックボタンなど、OS固有の操作はネイティブUIだと何も実装しなくても実現できる1一方で、Unityではそれぞれのプラットフォームごとに独自に実装していく必要があります。

感覚的なものでいうと、スクロールビューのスクロールの感触なんかもUnity、iOS、Androidでそれぞれ違うので、Unity製のアプリだと他のネイティブアプリと比較して触り心地がよくないのが正直なところです。

Unityのランタイムの起動に時間がかかる分、アプリの起動時間が遅くなってしまうというのも大きな問題でした。

Unity as a Library

Unityでビルドしたアプリを起動したらすぐにUnityのランタイムが実行されますが、Unity as a Library(= UaaL)という技術で、アプリの一部分だけでUnityを利用することができます。

これによりUnityでの再現が難しいネイティブアプリのUIの操作感を実現しつつ、必要なところでUnityの表現力を発揮することができます。

メンテナンスコストの増加

すべてがUnity製のアプリであれば1つのコードからiOS/Android両プラットフォームのアプリをビルドすることができます。

しかし、UaaLを使うことでiOS/Android両プラットフォームのネイティブUI部分を実装する必要が生まれます。

つまり、UaaLの採用によって実質的に2つのアプリを開発・メンテナンスする必要が生まれます。

clusterではUaaLをどう利用したか

ネイティブUIのoutroom/Unityのinroom

ワールド・イベントに入るまでの世界をoutroom、ワールド・イベントに入った後のUnityの世界をinroomと呼んでいます2

モバイルアプリ版clusterで、outroomをネイティブ、inroomでUnityを使うようアップデートしたものが11月にリリースした v1.85です。

これにより、Unity UIでの課題を解決してネイティブの触り心地を実現つつ、ワールド・イベントのクリエイティブな領域ではUnityの表現力を使うことができるようになりました。

この動画をご覧いただくと、普通にネイティブUIで実装し直すだけでどれだけ改善されたかがおわかりいただけるかと思います。

今後

outroomをネイティブUIにして体験を改善することができました。

しかしまだまだ改善することは山積みなため、クラスターではiOS/Androidエンジニアを募集しています。

というわけで2日目の記事は以上です。

明日は__0xyさんがなんかかくそうです。どんなことが書かれるんやろ…… ? お楽しみに!!

クラスター Advent Calendar 2020 - Qiita

参考リンク


  1. Androidのバックボタンは遷移先の制御を考えると何も実装しなくてもいいは過言 :innocent:  

  2. roomというのは古来よりクラスター内部で使われてきた用語です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity UIのタッチ判定範囲を可視化してみる

https://qiita.com/advent-calendar/2020/qualiarts の2日目の記事になります。

今回はUnity UI(uGUI)のタッチ判定を可視化するための実装について書いていきます。

ゴール

次のgifのように、既存のUIがおいてあるシーン(例えば Unityのサンプル として配布されている Menu 3Dシーンとか)に、プレハブをおくだけでタッチ領域が緑の枠線で示されたり、カーソル位置で反応しそうなUIが虹色になったりするようにしてみます。

つらつら実装の説明が長くなるので、とにかく試してみたい、という人は "PackageManager経由で使ってみるには" まで飛ばしていただければと思います。

top400.gif

なお、本投稿ではビルドしたアプリでも同様に機能することを目指して、ずいぶん大仰な実装を紹介しますが、Editor上のSceneViewで確認できれば良い、という要件であればbaba-sさんの Raycast Target が true なゲームオブジェクトの描画範囲を Scene ビューに表示するエディタ拡張 の実装を覗いてみるのがシンプルでわかりやすいです。

1. RaycastTarget領域を緑の枠線で表示してみる

touch-rect.png
この部分です。

1.1. GraphicRaycasterの一覧を取得する

Unity UIにおいてタッチイベントの検出はGraphicRaycaster配下のGraphicコンポーネントに対して行われています。
つまり、GraphicRaycaster配下ではないImageやRawImageについては無視できるということでもあります。
なのでまずはGraphicRaycasterを取得します。
Editor上の処理であれば Object.FindObjectsOfType<GraphicRaycaster>() でも良いかもしれません。
ただしGraphicRaycasterが後からInstantiateされることもあるので、キャッシュすることができないので、高コストですが描画のたびにFindObjectsする必要が出てきます。

そこで、internalなapiですが RaycasterManager.GetRaycasters を利用することで、ずっと効率のいい実装にすることができます。
このGetRaycastersメソッドはListの参照を返す実装なので、
以下のようにListの参照をキャッシュしておけば、一度のリフレクション呼び出しで、以降はListアクセスのコストだけで済みます。

IReadOnlyList<BaseRaycaster> raycasterListCache;
void OnEnable() {
    raycasterListCache = typeof(BaseRaycaster).Assembly
        .GetType("UnityEngine.EventSystems.RaycasterManager")
        .GetMethod("GetRaycasters",
            BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod)
        .Invoke(null, Array.Empty<object>()) as IReadOnlyList<BaseRaycaster>;
}

ただし、このRaycasterManagerにGraphicRaycasterが登録されるのはPlayモードに入った時だけのため、編集中にも動くようにするには、FindObjectsで収集するしかありません。

1.2. GraphicRaycasterからその配下のGraphicの一覧を取得する

GraphicRaycasterは [RequireComponent(typeof(Canvas))] 属性が付与されているので、安全にCanvasをGetComponentすることができます。
そしてCanvasがあれば GraphicRegistry.GetGraphicsForCanvas を使い、次の例のように配下のGraphicの一覧を取得することができます。

// GraphicRaycaster配下のGraphic一覧を取得する例
IList<Graphic> GetGraphicsForRaycaster(GraphicRaycaster raycaster) {
    if (raycaster.TryGetComponent<Canvas>(out var cav) == false) return null;
    return GraphicRegistry.GetGraphicsForCanvas(cav);
}

1.3. Graphicのタッチ判定が有効か調べる

Graphic毎のタッチ判定の詳細な実装は Graphic.Raycast を読むことで調べられます。
この Graphic.Raycast の実装を参考に、Rayを評価している処理を省く(実際にタッチしているかではなく、タッチの領域さえわかればいいので)ことで、そのGraphicがタッチ可能か評価できます。
一例としては、以下のように実装できるでしょう。

// Graphic.Raycastのうち、Rayを使わない部分の処理を抜粋
private bool IsRaycastTarget(Transform graphicTransform)
{
    var t = graphicTransform;
    var ignoreParentGroups = false;
    var continueTraversal = true;
    var components = new List<Component>();
    while (t != null)
    {
        components.Clear();
        t.GetComponents(components);
        for (var i = 0; i < components.Count; i++)
        {
            var cav = components[i] as Canvas;
            if (cav != null && cav.overrideSorting) continueTraversal = false;
            var filter = components[i] as ICanvasRaycastFilter;
            if (filter == null) continue;
            var raycastValid = true;
            var group = components[i] as CanvasGroup;
            if (group != null)
            {
                if (ignoreParentGroups == false && group.ignoreParentGroups)
                {
                    ignoreParentGroups = true;
                    raycastValid = group.blocksRaycasts;
                }
                else if (ignoreParentGroups == false)
                {
                    raycastValid = group.blocksRaycasts;
                }
            }

            if (raycastValid == false)
            {
                components.Clear();
                return false;
            }
        }

        t = continueTraversal ? t.parent : null;
    }

    components.Clear();
    return true;
}

1.4. タッチ判定が有効なGraphicを四角で囲む

矩形の描画の方法はいろいろ(GizmosとかIMGUIとか)ありますが、今回はCanvasとGraphicを使うものとします。

1.4.1. Graphic継承する

以下のような感じで、GraphicかMaskableGraphicを継承すればUnity UI上に任意の形を描画できるようになります。

TouchRectDrawer.cs
[RequireComponent(typeof(CanvasRenderer))] // いつの間にかGraphicから削除されていたCanvasRendererへのRequireComponent
[ExecuteAlways] // 非PlayMode状態でも描画できるように
public class TouchRectDrawer : MaskableGraphic {
    protected override void OnPopulateMesh(VertexHelper vh)
    {
        // なんか描画したい形を定義する処理
    }
}

1.4.2. Graphicのリストの更新とリビルド

Updateメソッドなどで、フレーム毎にタッチ判定の有効なGraphicを収集して、base.SetVerticesDirty を呼び出すことで、自身をリビルド対象とします。(毎フレームSetVerticesDirty呼ぶのは無駄な負荷ですが・・・)

TouchRectDrawer.cs
List<Graphic> _targets = new List<Graphic>();
void Update()
{
#if UNITY_EDITOR
    // 編集モード中は常にRaycastersを取得し直し
    if (Application.isPlaying == false) CacheRaycasterList();
#endif
    _targets.Clear();
    for (var i = 0; i < _raycasters.Count; i++)
    {
        FindTargetGraphics(_raycasters[i] as GraphicRaycaster, _targets);
    }
    // _targetsに変化がなければSetVerticesDirtyを叩かないようにした方が優しい
    base.SetVerticesDirty();
}

void CacheRaycasterList()
{
#if UNITY_EDITOR
    // BaseRaycasterの登録がプレイモード中だけっぽい
    if (Application.isPlaying == false)
    {
        _raycasters = FindObjectsOfType<GraphicRaycaster>()
            .Where(x => x.isActiveAndEnabled)
            .ToArray();
        return;
    }
#endif
    _raycasters = GetRaycasters.Value.Invoke(null, Array.Empty<object>()) as IReadOnlyList<BaseRaycaster>;
}

// Raycaster毎にタッチ判定の有効なGraphicを収集してtargetListに格納していく
void FindTargetGraphics(GraphicRaycaster raycaster, List<Graphic> targetList)
{
    if (raycaster == null) return;
    if (raycaster.TryGetComponent<Canvas>(out var cav) == false) return;
    var graphics = GraphicRegistry.GetGraphicsForCanvas(cav);
    var eventCamera = cav.worldCamera;
    var hasCamera = eventCamera != null;
    var farClip = hasCamera ? eventCamera.farClipPlane : 0f;
    for (var i = 0; i < graphics.Count; i++)
    {
        var graphic = graphics[i];
        var graphicsTransform = graphic.rectTransform;
        // この辺はGraphicRaycasterから拝借
        if (graphic.raycastTarget == false || graphic.canvasRenderer.cull || graphic.depth == -1) continue;
        if (hasCamera && eventCamera.WorldToScreenPoint(graphicsTransform.position).z > farClip) continue;
        if (IsRaycastTarget(graphicsTransform))
        {
            targetList.Add(graphic);
        }
    }
}

ここまで来ればあとは OnPopulateMesh の中で _targets を囲むような四角形を定義してあげればいいでしょう。
(実際には2020からRaycastPaddingが入ったり、CameraやらScreen座標からの変換が若干面倒だったりするのですが、詳細な実装は こちらのAddFrameとか を眺めていただければと)

2. タッチ対象を虹色にしてみる

こんな感じに虹色にしてみます。
スクリーンショット 2020-11-28 23.46.55.png

2.1. タッチ座標を取得する

まずはPCアプリやEditorであればマウスカーソルの座標、スマホやタブレットであればスクリーン上の指の座標を取得します。
Unityではそれらの座標を扱うためのモジュールとして、 Input ManagerInput System が存在します。
それぞれでの座標の取得の仕方を紹介します。

2.1.1. Input ManagerでのmousePositionとtouches

Input Managerとはずっと昔のUnityから存在している Input クラスとその周辺機能を差します。
今のところまだ非推奨にはなっていませんが、後述する Input System で代替されていくかもしれません。
とはいえ、まだまだ現役なので、Input Managerに対応した処理を実装しておいた方がいいでしょう。

次のコードのように Input.mousePositionInput.GetTouch で座標を取得できます。

List<Vector2> _touchPositions = new List<Vector2>();
// `IEnumerable<Vector2>` を返り値型として、 `yield return` する実装の方がすっきりしそうだけど、gcに気をつかってみる
IReadOnlyList<Vector2> GetTouchPositionsFromInputManager()
{
    _touchPositions.Clear();
    _touchPositions.Add(Input.mousePosition);
    for (var i = 0; i < Input.touchCount; i++)
    {
        _touchPositions.Add(Input.GetTouch(i).position);
    }
    return _touchPositions;
}

2.1.2. Input SystemでのmousePositionとtouches

Input System はUnity2018.3以降でPackage Managerから導入することができます。
本投稿ではInput Systemの導入方法は紹介しません。ごめんなさい。

Input Systemでのマウス入力は UnityEngine.InputSystem.Mouse クラス、タッチデバイス入力は UnityEngine.InputSystem.Touchscreen クラスから取得できます。
次のような実装で両方をサポートできるでしょう。

List<Vector2> _touchPositions = new List<Vector2>();
IReadOnlyList<Vector2> GetTouchPositionsFromInputSystem()
{
    _touchPositions.Clear();
    if (Mouse.current != null) _touchPositions.Add(Mouse.current.position.ReadValue());
    if (Touchscreen.current != null)
    {
        var touches = Touchscreen.current.touches;
        for (var i = 0; i < touches.Count; i++)
        {
            if (touches[i] == null) continue;
            if (touches[i].isInProgress == false) continue;
            _touchPositions.Add(touches[i].ReadValue().position);
        }
    }
    return _touchPositions;
}

2.1.3. Input ManagerとInput Systemの両方に対応する

Input Systemを有効にするとScriptDefinesに ENABLE_INPUT_SYSTEM が定義されます。
同様に、Input Managerを有効にすると ENABLE_LEGACY_INPUT_MANAGER が定義されます。
なので、Input Systemだけの処理を書きたい場合は #if ENABLE_INPUT_SYSTEM ~~ #endif で括ってあげればいいということになります。

また、Input SystemとInput Managerを共存させることもできるので、Unity UIのEventSystemがどちらで動いているかの判定も必要になるかもしれません。
それには EventSystem.current.currentInputModule の型で評価できます。
currentInputModuleが StandaloneInputModule型ならばInput Managerで動いていて、
currentInputModuleが InputSystemUIInputModule型ならばInput Systemで動いている、
ということにできます。

次のようなメソッドでラップしておくのがいいかもしれません。

IReadOnlyList<Vector2> GetTouchPositions()
{
#if ENABLE_INPUT_SYSTEM
    if (EventSystem.current == null) return System.Array.Empty<Vector2>();
    if (EventSystem.current.currentInputModule is InputSystemUIInputModule)
    {
        return GetTouchPositionsFromInputSystem();
    }
    else
    {
        return GetTouchPositionsFromInputManager();
    }
#else
    // Input Systemが有効でない時はInput Managerへ
    return GetTouchPositionsFromInputManager();
#endif
}

2.2. 最前面で触れているものを取得する

EventSystem.current.RaycastAll を利用することで、Unity UI的に触れられていると判定されるGraphicを取得できます。
前述した方法で入力座標が取得できていれば、その座標を PointerEventData に設定してRaycastAllを実行するだけです。
タッチデバイスなど、複数同時タップなども対応した方が親切かもしれません。
以下のような実装で、触れられているGraphicの一覧を取得することができます。

List<RaycastResult> GetHits()
{
    // タッチ座標を取得
    var touchPositions = GetTouchPositions();
    var tmp = new List<RaycastResult>();
    var eventData = new PointerEventData(EventSystem.current);
    var hits = new List<RaycastResult>();
    // 各タッチ座標でUnity UI的に衝突するものを列挙
    for (var i = 0; i < touchPositions.Count; i++)
    {
        tmp.Clear();
        eventData.Reset();
        // eventDataのpositionだけ更新して使い回す
        eventData.position = touchPositions[i];
        // 衝突結果は
        EventSystem.current.RaycastAll(eventData, tmp);
        // EventSystem側でソート済みなので先頭を拾ってくればいい
        if (tmp.Count > 0) hits.Add(tmp[0]);
    }
    return hits;
}

2.3. 触れている対象を虹色にする

緑の四角で囲ったように、Graphicの継承で実装してしまいます。

  1. タッチ対象のGraphicのタッチ判定領域(の四角)のScreen座標を取得
  2. Screen座標をワールド座標に変換
  3. ワールド座標を虹色を表示するGraphicからみたローカル座標に変換
  4. 虹色のGraphicの頂点としてAddUIVertexQuad

言葉にするとよくわからなくなりますが、以下のような処理です。

static readonly UIVertex[] Vertices = new UIVertex[4];
void AddQuad(VertexHelper helper, RaycastResult result)
{
    if ((result.gameObject == null) || (result.module == null)) return;
    if (result.gameObject.TryGetComponent(out Graphic graphic) == false) return;
    // graphicのタッチ判定の範囲をワールド空間で取得する
    // https://github.com/QualiArts/ugui-touch-rect-drawer/blob/main/Packages/ugui-touch-rect-drawer/Runtime/DrawerUtils.cs
    var corners = DrawerUtils.GetWorldRaycastCorners(graphic);
    if (corners == null) return;
    var baseColor = color;
    var rt = rectTransform;
    var cam = canvas.worldCamera;
    for (var i = 0; i < corners.Length; i++)
    {
        // graphicの各コーナーのスクリーン座標を取得する
        var pos = RectTransformUtility.WorldToScreenPoint(result.module.eventCamera, corners[i]);
        // スクリーン座標を、虹色の基点からみたローカル座標に変換する
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, pos, cam, out pos);
        Vertices[i].position = new Vector3(pos.x, pos.y, 0f);
        // 虹色っぽい頂点カラーを設定
        Vertices[i].color = GetCornerColor(i) * baseColor;
    }
    // 四角として登録する
    helper.AddUIVertexQuad(Vertices);
}

これで緑の四角や虹のオーバーレイの実装が終わりです。

PackageManager経由で使ってみるには

こちらのリポジトリ にPackageの形にしたものを上げておきました。

manifest.jsonに
"ugui-touch-rect-drawer": "https://github.com/QualiArts/ugui-touch-rect-drawer.git?path=Packages/ugui-touch-rect-drawer"
を記述します。

manifest.json
  "dependencies": {
    "ugui-touch-rect-drawer": "https://github.com/QualiArts/ugui-touch-rect-drawer.git?path=Packages/ugui-touch-rect-drawer",
    ...,
  }

Packageの解決ができたら DrawerContainer をシーンに配置します。
スクリーンショット 2020-11-28 20.05.53.png

Scriptから描画の有無を切り替えるには、上記prefabがシーンにある状態で UGUIRaycastDrawer.DrawerUtils のメソッドを利用できます。

UGUIRaycastDrawer.DrawerUtils.SetTouchRectDrawer(drawTouchRect);
UGUIRaycastDrawer.DrawerUtils.SetTouchResultDrawer(drawTouchTarget, logTouchTarget);

最後に

本投稿に掲載したコードは、説明のし易さの都合のため実際の実装から多少異なったものになっています。詳細を確認したい場合はリポジトリを参照してください。

以上です。
閲覧ありがとうございました。

明日は iyuさん です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MagicOnionから始めるリアルタイム通信 (後編)

Happy Elements Advent Calendar 2020 8日目の記事です。

7日目の記事に引き続き、「メルクストーリア」エンジニアの 岸本 です。

前回の記事では、MagicOnion の基本的な部分や、
サーバー構成 の考え方について、ご紹介させていただきました。

今回は、KubernetesAgones といったインフラ寄りの話題に加えて、
具体的な処理の流れなどをご紹介したいと思います。

本内容ですが、現在開発中のものであり、実際の仕様とは異なる場合があります!

Kubernetes / Agones

さて、 ここまでの内容を実現するために、今度はインフラ面でどうするか問題になってきます。

今回は、KubernetesAgones を用いることで解決します。
(Kubernetes や Agones の細かい説明は割愛します!)

Kubernetes

Kubernetesとは、コンテナ化されたアプリケーションの デプロイスケリーング をお任せできる、
コンテナオーケストレーション のシステムです。

今回のように、サービスディスカバリーリアルタイム通信サーバー といった、
複数のサーバーから構成される マイクロサービス に適しており、非常に強力なシステムですが、
ステートフルな構成リアルタイム通信 を実現する場合に、若干問題があります。

  • サーバーを直接外部に公開できない。
    Kubernetes では、サーバー (Pod) を直接外部に公開することはできず、
    Service というロードバランサーのような仕組みを経由させて公開する必要があり、
    特定のユーザーを特定のサーバーに接続させたい場合、若干不向きな仕組みになっています。

  • スケーリングからの保護ができない。
    ユーザーの常時接続が必要なリアルタイム通信サーバーが、
    Kubernetesスケーリング の対象となって、
    意図しないタイミングでシャットダウンされると困るので、やはり若干不向きな仕組みになっています。

この問題点を解決するため、Agones を利用します。

Agones

Agones とは、Kubernetesステートフルな構成リアルタイム通信サーバー を稼働させることを目的とした、
Kubernetes の拡張システムです。

  • サーバーを直接外部に公開できる!
    Agones では、公開可能な IPアドレス を サーバー (Pod) に割当することで、
    Service を介さず、直接外部に公開できるようになります。

  • スケーリングから保護できる!
    Agones では、サーバー (Pod) のステータスを管理できるようになっていて、
    使用中 (Allocated) なサーバーは、Kubernetesスケーリング から保護されるようになります。

このような特徴から Kubernetes でも ステートフルなリアルタイム通信サーバー を扱いやすくなり、
今回のような リアルタイム通信サーバー の運用に適したシステムとなっています。

全体構成図

1208_01.png

ここまでの仕組みを組み合わせることで、
クラウド上では、大まかにこのような構成となりました。
(運用上、他に必要なものもあるため、これがすべてではありません!)

サービスディスカバリーとリアルタイム通信サーバー

1208_02.png

ソフト的には、このような構成となっています。

サービスディスカバリー は、Kubernetes上で稼働するサーバーであり、
MagicOnionService を利用します。

リアルタイム通信サーバー は、Kubernetes上で稼働するサーバーであり、
MagicOnionStreamingHub を利用します。

ひとつのサーバー内に、複数のポートでホストを立てており、
クライアントから使用する公開用の API とは別に、
サーバー間通信で必要な内部用の API は、別立てして分割しています。

また、KubernetesAgones の API に問い合わせることで、
他のサーバーの情報など、Kubernetes内部 の様々なことを知ることができるようになっています。

下記は、コマンドベースの簡単な例ですが、
Agones で稼働している リアルタイム通信サーバー (ゲームサーバー) の一覧を取得して、
外部に公開可能な IPアドレス と ポート の情報を取得しています。
(これは Minikube環境 での結果となります。)

$ kubectl get gameserver
NAME                    STATE   ADDRESS          PORT   NODE       AGE
real-time-xhxkn-8szj7   Ready   192.168.99.100   7400   minikube   24m
real-time-xhxkn-ff4rp   Ready   192.168.99.100   7101   minikube   24m

C#含めて様々な言語で KubernetesAgonesSDK が公開されており、
それらを通じて、アプリケーション内部からでも上記のような情報を取得できるようになります。

処理の流れ

チャット を例に、具体的な処理の流れを考えてみます。

完全にランダムなチャットルームへの参加ではなく、
パーティーギルド といった、特定のグループ専用のチャットルームへの参加を前提とします。

1208_03.png

  1. ユーザー (クライアント) は、チャットルーム (リアルタイム通信サーバー) の接続先がわからないため、
    サービスディスカバリー (APIサーバー)チャットルーム の接続先を問い合わせます。
  2. サービスディスカバリーは、Kubernetes で稼働している チャットルームのリスト が欲しいため、
    Kubernetes に問い合わせます。
  3. サービスディスカバリー は、チャットルームのリスト を基に割当状況などを確認し、
    ユーザーが要求しているチャットルーム を確保します。
    このとき、チャットルーム は、Agones に対して 使用中 (Allocated) である旨を宣言し、
    Kubernetes のスケーリングから保護します。
  4. サービスディスカバリー は、チャットルーム の接続先を ユーザー に返します。
  5. ユーザー は、サービスディスカバリー から得た チャットルーム に接続します。
    あとは、常時接続でクライアントサーバー間の双方向通信ができるようになるので、
    自由にチャットすることができます。
  6. すべての ユーザー が退室して、チャットルーム が不要になれば、
    Agones に対して シャットダウン可能 である旨を宣言し、スケーリング対象に戻します。

ちょっと長い道のりでしたが、
ここまでできれば、ステートフルな構成リアルタイム通信 を前提とした環境ができあがり、
一通り動かせるようになりました。

あとは、要件に応じてカスタマイズすることで、やりたいことを実装できるかと思います!

おまけ

デプロイの仕組み

AWS での稼働が前提となりますが、
下記のようなフローで、デプロイできる仕組みを構築しています。

1208_04.png

Kubernetes の最も基本的なデプロイ方法として、
対象となるサーバーの Deployment (もしくは Agones の Fleet) で指定している、
コンテナのイメージバージョンを変更することで解決できます。

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: hoge
        image: hoge:v1.0 → hoge:v2.0

この yaml定義 の更新自体を自動化するような仕組みでも可能ですが、
メルクストーリアでは、管理画面からデプロイできる仕組みを構築しており、
アプリケーション内部から DeploymentFleet のパッチをできるようにしています。

ソースコードを GitHub にコミットすれば、CodeBuild で自動的にコンテナビルドし、
それを ECR にプッシュします。
管理画面 から ECR のリポジトリにアクセスして、デプロイしたいイメージバージョンを選択することで、
デプロイできるようになっています。

また、Agones管理下 の使用中の リアルタイム通信サーバー は、
接続中のコネクション切断など、グレースフルシャットダウン が必要になるため、
デプロイバッチ を動かすことで解決しています。

こうすることで、可能な限り手作業を排除し、安全にデプロイできるようにしています。

シークレットの取り扱い

こちらも AWS での稼働が前提となりますが、
下記のようなフローで、シークレットを扱えるようにしています。

1208_05.png

Kubernetes では、Secret という機密情報を扱える仕組みがありますが、
例えば 秘密鍵 のようなものを Secret で扱うには、管理面での注意が必要となります。

そこで、機密情報自体は、AWS の SecretsManager で管理し、
アプリケーション内部から SecretsManagere にアクセスすることで、
KubernetesSecret に反映できるようにしました。

こうすることで、コードへの機密情報埋込をはじめ、
Git にコミットしたり、ローカルで管理したり…、といったことを回避できるようにしています。

まとめ

ということで、MagicOnion を用いた リアルタイム通信 の仕組みづくりについてのご紹介でした!

まだまだ開発中のものなので、ご紹介できる具体例も少なく、
前提として必要になる技術のご紹介も含めたため、
なかなかボリュームのある内容になってしまいましたが、いかがだったでしょうか…?

日本国内のゲームのサーバーサイドの事情として、
MagicOnion を含めて サーバーサイドC# を採用している事例はまだまだ少なく、
弊社カカリアスタジオでも、クライアントサイドは、Unity (C#)
サーバーサイドは、Ruby on Rails (Ruby) が主流となっています。

そんな中、MagicOnion をはじめ、KubernetesAgones などを用いた開発は、
なかなか挑戦的な内容になっているかと思いますので、
リアルタイム通信サーバーサイドC# など、少しでも気になっている方の参考になれば幸いです!

メンバー募集

Happy Elements株式会社 カカリアスタジオでは、
いっしょに【熱狂的に愛されるコンテンツ】をつくっていただけるメンバーを大募集中です!

もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください。
Happy Elements株式会社 採用特設サイト

引き続き、Happy Elements Advent Calendar 2020 をお楽しみください!
ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MagicOnionから始めるリアルタイム通信 (前編)

Happy Elements Advent Calendar 2020 7日目の記事です。

メルクストーリア」エンジニアの 岸本 と申します。

今回は、メルクストーリア で現在開発の進めている リアルタイム通信 の取り組みについて、
ご紹介したいと思います。

基本的な部分も含めてのご紹介になるため、
MagicOniongRPCKubernetesAgones などなど、
マシマシな感じでかなりボリュームのある内容なため、2日に分割しての記事となります。

しばしお付き合いいただければ幸いですので、何卒よろしくおねがいします!

本内容ですが、現在開発中のものであり、実際の仕様とは異なる場合があります!

前提

メルクストーリア は、おかげさまでもうすぐ7周年を迎えます!???

そんな メルクストーリア では、チャットギルドバトル をはじめ、
MMOのような見た目の 急襲!降臨モンスターイベント など、
ユーザー様同士のマルチプレイができる遊びを多く提供していますが、
基本的には ポーリング で実現しています。

1207_01.png

ポーリング である以上、一定のタイムラグが発生し、
ユーザーの皆様にご不便をおかけしている部分もあると感じており、
さらなる遊びの提供やユーザー体験の向上を目標に、リアルタイム通信 を用いた開発に取り組んでいます。

ただし、現在運用中の Ruby on Rails (Ruby) の APIサーバー が存在していますが、
そちらを リアルタイム通信 に対応するのではなく、
リアルタイム通信専用 に別系統のサーバーを立てて、部分的なリアルタイム通信の導入 を目指しています。
なお、クライアントは Unity です。

サーバー フレームワーク インフラ
APIサーバー (既存) Ruby on Rails (Ruby) オンプレミス
リアルタイム通信サーバー (新規) .NET (C#) クラウド (AWS)

また、インフラ面でも課題があり、
既存のAPIサーバーは、オンプレミス での運用に対して、
現在開発中のリアルタイム通信サーバーは、クラウド (AWS) での運用が前提になっています。

そのため、運用中のユーザーデータなどへのアクセスに制限があり、
リアルタイム通信サーバー (クラウド)APIサーバー (オンプレミス) で通信することも可能ですが、
APIサーバーの負荷等も考慮すると、頻繁なアクセスはできないため、
基本的に運用中のユーザーデータへのアクセスはできない前提とします。

技術的にいろいろハイブリッドな構成となっていますし、
いろいろな事情も絡んで、リアルタイム通信サーバー で実現できることも限られていますが、
そのあたりも含めて、全体像をご紹介したいと思います。

使用している技術 (2020年12月時点)

サーバーサイド

  • .NET 5.0 (C# 9.0)
  • MagicOnion 4.0
  • MessagePack for C# 2.2
  • ZLogger 1.3
  • StackExchange.Redis 2.1
  • System.Reactive

クライアントサイド

  • Unity 2018.4 (C# 7.3)
  • MagicOnion 4.0
  • MessagePack for C# 2.2
  • UniTask 2.0

インフラ

  • Kubernetes 1.18
  • Agones 1.0.9
  • AWS
    • ElasticKubernetesService (EKS)
    • ElasticContainerRegistry (ECR)
    • ElastiCache (Redis)
    • SecretsManager
    • CodeBuild

MagicOnion

MagicOnion とは、gRPCMessagePack for C# がベースの通信フレームワークで、
クライアントもサーバーも 「C#で大統一!」 できる特徴があります。

1207_02.png

MagicOnion では、C# の interface で gRPC のメソッドを表現できるようになっていたり、
また、通信に用いるデータを MessagePack for C# でシリアライズすることで、
gRPCベース の Remote Procedure Call を利用できるようになっています。

1207_03.png

機能的には、2種類 の通式方式がサポートされていて、
単発のAPI通信に向いている「Service」 と、
双方向通信も可能でリアルタイム通信に向いている「StreamingHub」 があり、
用途によって使い分けることができます。

また、Filter という共通処理のための仕組みや、
Group というマルチプレイのための仕組みなども実装されており、
通信まわりは一通り MagicOnion におまかせできるようになっています。

今回は、リアルタイム通信での使用を目的としていますが、
単純なAPIサーバーとしても使用できるようになっています。

サーバー構成

さて、MagicOnion を用いることで リアルタイム通信 は実装できそうですが、
インフラ面で問題が出てきます。

マルチプレイでの遊びを考えた場合、
複数ユーザー間でデータを同期する必要があり、どうやってデータを管理・共有するか問題になってきます。

方法としては、おおまかに 2種類 あります。

ステートレスな構成

ロードバランサーによる分散を前提としたサーバー構成の場合、
当然、ユーザーの接続するサーバーもバラバラになるため、
マルチプレイに必要なデータは、Redis Pub/Sub など、
サーバー外部の共通ストレージで管理・共有することで解決できます。

1207_04.png

MagicOnion には、Redis Pub/Sub での繋ぎ込みをサポートする、
MagicOnion.Server.Redis というライブラリも用意されています。

ステートフルな構成

サービスディスカバリー という リアルタイム通信サーバー への誘導用サーバーを立てることで、
ロードバランサーによる分散はせず、
マルチプレイさせたいユーザー同士を直接同じサーバーに接続させ、
マルチプレイに必要なデータは、サーバー内部のメモリ上で管理・共有することで解決できます。

1207_05.png

MagicOnion には、サーバー内でユーザー同士を束ねることのできる、
Group というブロードキャストシステムが内蔵されています。

メルクストリーアでは

どちらで構成してもメリット・デメリットがあり、
一概にどっちが良いかはケースバイケースとなりますが、
今回は、ステートフルな構成 を採用しています。

1207_06.png

マルチプレイさせたいユーザー同士をひとつのサーバーに集めて、Group で束ねることで、
リアルタイム通信 を実現します。

たとえば、チャットルーム のような機能を実現したい場合、
チャットルームごとに Group を作って、ユーザー同士を束ねることができます。

Group は、チャットルームID のような任意の文字列で簡単に作ることができ、
Group に所属しているユーザーに対して、サーバーからクライアントへのブロードキャストが可能となります。

データの管理・共有は、共通のインスタンスを利用することで解決し、
どうしても外部に保存しておきたいデータのみ、外部のストレージに保存するようにします。

前編まとめ

ということで、前編は MagicOnion を用いた マルチプレイなリアルタイム通信 を実現する上で、
基本的な仕組みについて、ご紹介させていただきました!

gRPC 含めて サーバーサイドC# ということだけであれば、
.NET標準 だけでも十分開発できるようになっており、
先月リリースされた .NET 5 では、gRPC のパフォーマンスもトップレベルで良くなっているようです。

MagicOnion は、.NET標準 の実装に沿いつつ MessagePack for C# によるシリアライズもサポートし、
さらに いい感じC#gRPC できるようになっています!

機能面でも複雑に多機能な感じではなく、基本的な部分は README や サンプル が用意されているため、
それほど学習コストも高くないように感じました。
どちらかというと、Kubernetes など他の技術の学習コストの方が高めです…。

明日は、本日の内容を前提に KubernetesAgones といったインフラ寄りの話題に加えて、
具体的な処理の流れなどをご紹介したいと思います。

引き続きお楽しみいただければ幸いです!

メンバー募集

Happy Elements株式会社 カカリアスタジオでは、
いっしょに【熱狂的に愛されるコンテンツ】をつくっていただけるメンバーを大募集中です!

もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください。
Happy Elements株式会社 採用特設サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゲーム開発におけるボイスデータの効率的な管理 with CRI ADX2 (前編)

この記事の対象者

  • キャラクターのボイスがあるゲームを作っている開発者
  • 「ADX2」を使っている開発現場のプランナーさん・ゲームデザイナーさん
  • ボイスデータが大量にあるので効率的に管理したい人
  • 音量バランスや設定をプログラマー以外に任せたいと考えている方

ボイスデータの設定・管理におけるADX2の使い方

この記事では、ゲームサウンド開発のツール「ADX2」を使ってキャラクターボイスの管理・制御を行う例を紹介します。
ADX2の初期設定や組み込みについては、本投稿では触れません。
記事を読んでADX2を試してみたい、と思った方は以下の投稿も一緒にご覧ください。

すでにADX2が開発ワークフローに組み込まれている現場で、ツールを使った音声の設定をプランナーさんにも任せられるような説明書としても書かれています。
ゲームプログラマーでなくても理解できるように、極力プログラム寄りの専門用語を減らしています。
「ADX2」の概念説明にも利用できます。

なお、本記事の書き手はUnityを使ったインディーゲームを開発しているプログラマーです。
今作にはキャラクターのボイスを導入するため、自分の備忘録としてもまとめました。

ゲーム開発用サウンドツールCRI ADX2とは

「CRI ADX2」は、簡単に言えばゲームで再生する音を管理・制御するソフトウェアです。

UnityやUE4を使っている開発現場では、まずエディターに音声ファイルを登録して、C#スクリプトないしはブループリント等でそれらの再生を実行します。
その際、単純な再生開始処理だけではなく、「一時停止」「フェードインアウト」「同時再生数の抑制」「優先度設定と管理」「サウンドエフェクトのオンオフ」など、多岐にわたる処理を組む必要があります。
ADX2は、ゲームに頻出の音の演出設定を「音声データ側に埋め込む」ことで、開発省力化を図ることができる製品です。

またADX2では、音声データの圧縮に「HCA」という圧縮形式を利用します。
これは本ツールの独自形式で、圧縮率はOggとほぼ同じです。Oggと比較して再生時のCPU負荷が低く、高音域の再現に強く、高圧縮にした際に女性の声がよりきれいに再現できる特徴があります。

エディションは、法人向けの「ADX2」と、同人・インディーゲーム開発者向けの無償版「ADX2 LE」があります。

ADX2_r600.jpg

ADX2のシステムでは、音声ファイル単独に対して再生を行うのではなく、音声ファイルをラップした「キュー」という単位で音を再生します。

たとえば、「2個の音からランダムに効果音を鳴らす」という処理を考えてみます。ゲームエンジンの標準機能などでは、通常はゲームプログラム側で開発する必要があります。
しかし、ADX2にはプラグイン内に「ランダム再生」の汎用機能が用意されており、この処理を個別に作る必要がありません。
「キュー」に再生する音声データとランダム再生の設定を指定しておけば、実行時にADX2が設定に従ってランダム再生が行われます。

ADX2キューの概念図.jpg

キューに「この音は2種類の音からランダムに効果音を鳴らす」というフラグをセットしておくことで、その演出を含んだ音の再生が可能になります。このワークフローによって、プログラミングの総量を減らしつつ、サウンド再生の演出を作りこむことができます。
キューの設定は、同梱の専用ツール「Atom Craft」で行います。

ADX2ツールオフィシャル画像.png

ツールはWindows用とmac用が用意されています。見た目は作曲ツールっぽいですが、ADX2はキャラクターボイスや効果音の管理・制御に役立つ機能も多く入っています。
たとえば、キューには制御パラメーターの他に文字データを埋め込むことができます。ボイスデータの管理においては、セリフ文字そのものを埋め込むことで、このキューがなんのセリフなのか目視で確認できます。

ユーザーデータによるセリフの埋め込み.png

(上記はAtom Craftでパラメーターを一括表示する「リストエディター」ウィンドウを使っています。後ほど解説します。)
この埋め込み情報はゲームエンジンやエディター側からも確認できますので、エディター上で確認しながら組み込んだり、ゲームの中でこの文字列を使ってセリフの表示をすることも可能です。

この記事では次の活用方法について紹介します。

  • ADX2でボイス音声のデータを用意する手順
  • ボイス音声データを一覧表示しながらボリューム調整
  • BGMや環境音を同時再生しながらボイス音声データの聞こえ具合を確かめる
  • カテゴリ分け機能で、ゲームの音量を音の種類ごとに管理
  • 音声データに文字列を埋め込んで、何のセリフか判断しやすくする設定

また、次回の記事では次の活用方法を紹介します。

  • カテゴリ分けを使って同一キャラのボイスは複数同時に再生させない設定
  • BGMが鳴っているときにボイスを再生するとBGMの音量を落とす設定
  • 複数のファイルからランダムに選んで再生する仕掛けを作る設定

ADX2でボイス音声のデータを用意する手順

まずは、元となる音声データをツール「Atom Craft」に登録します。圧縮前の音声素材データは、AtomCraftの中の「マテリアル」ウィンドウで管理します。
マテリアルツリーウィンドウは、デフォルトでは左下に開いています。
閉じている場合は、下部のウィンドウ呼び出しボタンから「マテリアルツリー」を選んでクリックすると開きます。

マテリアルツリーウィンドウ.png

このマテリアルツリーウィンドウにWaveファイルをドラッグ&ドロップすれば、ツールに登録されます。
なお、エクスプローラー側でwaveファイルを配置した場合は、マテリアルツリーウィンドウのフォルダ上右クリックメニューで出る「未登録ファイルの登録」でツール側が認識します。
ボイスデータはキャラクターごとのフォルダに分けて登録しておくと見やすいです。

wavファイルをインポートして圧縮設定

ADX2のシステムでは、サウンドデータの圧縮設定は元素材データ単位で設定できます。設定は、プロジェクト全体の圧縮設定、フォルダごと、ファイルごとで細かく指定できます。

例として、ボイスデータのルートフォルダでボイス全体の圧縮設定を行います。キャラクターごとにボイスのフォルダを分けている場合は、フォルダ単位で一気に設定を適用しましょう。

圧縮設定.png

設定については、現代のゲーム機やスマホ、モバイル機器で動作させるならば「HCA」形式の「最高品質」モードで問題ないです。
データ容量の制限が厳しい場合は、重要度の低い音や容量にインパクトが大きい音のエンコード品質を下げることで対応します。

サウンド再生単位である「キュー」を作成

waveファイルをAtom Craftに登録しただけでは、まだ再生用のデータを出力できません。
次の段階として、プログラムから再生される単位である「キュー」を作成します。

ボイス用のキューは、マテリアルツリーウィンドウから一括で生成・登録ができます。
マテリアルツリー内のフォルダを「ワークユニットツリー」のワークユニットフォルダへドラッグ&ドロップすることで、マテリアルの名前と同じキューが一括生成されます。

2新規キューの作成.png

ワークユニットツリーのデフォルト設定では、「WorkUnit_0」というフォルダがあります。その下にドラッグアンドドロップすると、フォルダ単位の「キューシート」が生成され、マテリアルごとにキューが生成されます。
(ワークユニットツリーの名前はプロジェクトによって異なります)

キュー一覧.png

「キューシートは」、複数のキューをひとまとめにしたファイル単位です。Atom Craftからデータを出力する際は、キューシート単位でデータが生成されます。
プログラムからは、キューシート単位でデータの読み込みを行います。

ボイスデータ向けのキューシートの分け方ですが、アクションゲーム・RPGなどでは、バトルやフィールド場面用のサウンドデータはキャラクターごとのキューシートにまとめ、アドベンチャーゲームや会話パートなどでは、そのシーンのボイス音声データごとにまとめたりします。
キューシートの分け方についてはプロジェクトによって異なるので、実装担当と相談しておきましょう。

ボイス音声データを一覧表示しながらボリューム調整

キューには様々な制御パラメータを含むことができます。このパラメータにはボリューム情報も含まれます。
ゲームの実装担当にデータを渡す前に、ツールで各ボイス音声のボリュームバランスを調整しておきましょう。
キューが持つパラメーターは、ワークユニットツリー上でキューを選択したときのインスペクターウィンドウで個別に編集できます。

インスペクターでボリュームを確認2.png

ボイスデータのように、同じように扱われるキューが大量にあるときはパラメータの編集を一括で行うと早いです。
その場合は「リストエディター」を使います。AtomCraftの下メニューから「リストエディター」を選択して開きます。

リストエディターを開く.png

リストエディターは、ワークユニットツリー上で要素を選択したときに、その要素が含んでいるデータを一覧で表示します。キューの一覧を表示したいときは、その親となるキューシートを選択します。

はじめに、ボイスデータのボリュームパラメータを一括で0.7にします。これは他のボイス音声と比べて音量が小さかった時に、上げ幅を確保するための作業です。
(プロジェクトによっては0.4や0.5にするケースもあると思います)
リストエディターに表示されているアイコンのうち、スピーカーマークのようなアイコンをクリックすると「ボリューム」と「ボリュームランダム幅」の項目が現れます。
Ctrl + Aでキューを全選択してからどれかのボリューム項目を操作すると、選択したキュー全てに反映されます。

一括ボリューム変更.png

ADX2 LEの2.10系を使っている場合はリストエディターの見た目が少し違いますが、操作は同じです。「全て」または「ボリューム/ピッチ」ボタンで、各キューのボリューム項目を表示・操作できます。

LE2.10系.png

一括操作した後は、上から順番にプレビュー再生しながら、ボリュームが大きすぎるキューが無いか確認しながら一つずつ修正していきましょう。
キューを選択しながら再生ボタンまたはF5キーでプレビュー再生が実行されます。

BGMや環境音を再生しながらボイス音声を再生して確かめる

音楽プレイヤーなどと異なり、Atom Craftでは音声の同時再生が当たり前に行えます。
BGMのキューを再生開始させておき、そのままボイスデータのキューを再生することで、実際のゲームとほぼ同じ状況でボリュームを確認できます。
ツール上部の再生ボタンまたはF5キーで多重再生OK状態でのプレビュー再生を行います。
現在流れている音をすべて止めてからキューのプレビュー再生を行いたい場合は、スペースキーで実行できます。

ボイスが利用されるBGMや効果音と一緒に流したとき、意図した聞こえ方になっているかどうか確かめることができます、
ただしBGMの種類が多い場合は、キューシートを行ったり来たりすることが手間の場合もあります。
その際は、「セッションウィンドウ」機能を使うことで、キューシートをまたいだキューの一覧表示を行いながら再生できます。
また、より複雑な音の組み合わせを確かめることができます。「セッションウィンドウ」については、次のカテゴリ分けの後に説明します。

カテゴリ分け機能による音量コンフィグ開発の簡略化

ADX2におけるボイスデータ管理において、「カテゴリ」機能は非常に便利です。
イメージとしては音声データにタグ付けが行われ、そのタグごとに音量や再生数制御が可能になります。

カテゴリそのものは、「プロジェクトツリー」で作成します。今回はカテゴリ分けとして、「BGM」「GameSe」「MenuSe」「EffectSe」「Voice」があるとします。
(適切な分け方はゲームジャンルによって異なります)

カテゴリレイ.png

キューにカテゴリを設定してみましょう。リストエディターを開き、「◎」っぽいボタンでキュー情報を表示します。

CRI Atom Craft Ver.3.44.08 (built on Jul 17 2020)2 .png

「ADX2 LE」の2.10系では「カテゴリ」をクリックすることでカテゴリ情報を表示します。

LECategory2.png

キューシートをドラッグアンドドロップすれば、その下のキュー全てにカテゴリが設定されます。

ドラッグ.png

また、キューを選んで一括指定することもできます。ALT + Aキーでキューを複数選択して、プロジェクトツリーウィンドウのカテゴリフォルダにドラッグアンドドロップします。これで、選択した全キューのカテゴリが一気に設定されます。

カテゴリの編集 2020-11-21 00.26.28.png

これでボイスデータのキューに「VOICE」というカテゴリが付与されました。プログラムからは、このカテゴリごとにボリュームを変更することができます。
すなわち、ゲームでよくある「ボリュームコンフィグ」を簡単に作ることができます。
(ボイスの音量バランスをそろえる、といった静的な設定については、キューやトラック、キューシートのボリューム項目で調整するようにしましょう。)

実際にこのカテゴリでボリュームが変更されたときにどのような効果になるかを確かめてみましょう。ADX2の「セッションウィンドウ」という機能を使います。
メニューバーの「表示」「セッションウィンドウ」からセッションウィンドウ」を起動します。

セッションウィンドウは、ゲームから指定される様々なパラメータをエミュレートしつつ再生テストを行うウィンドウです。
今回はカテゴリによるボリューム調整を確かめるので、左上の「カテゴリ」ボタンを押してカテゴリを表示させ、下部の「player1」にはBGMのキューをドラッグアンドドロップ、「player2」にはボイスデータをドラッグアンドドロップします。
(player2はデフォルトではplaye1の裏側に別タブで隠れているので、いったんドラッグで分離してplayer1と2が並んで表示するようにしています)

セッションウィンドウの活用例.png

上部の「カテゴリ」で影響を確かめたいカテゴリの「ボリューム」欄にチェックマークを入れ、スライダーを左右に操作することでボリュームの変化を確かめることができます。

また、セッションウィンドウではキーボードの左側エリアにキュー再生のショートカットが割り当てられていますので、複数の音を同時再生してみたり、連打してみたりすることが容易になっています。

音声データに文字列を埋め込んで、何のセリフか判断しやすくする設定

ボイス音声データにセリフの文字列を埋め込む作業を行ってみましょう。
リストエディターを表示し、ワークユニットツリー上でキューシートを選んでいる状態で「◎」アイコンをクリックします。
右側にある「ユーザーデータ」がキューに埋め込むことができる文字列データの領域です。
ここにセリフを記入していきます。

ユーザーデータによるセリフの埋め込み.png

どの名前のキューがなんのセリフが入っているか、非常に視認しやすくなりました。

例として、Unity Editorで表示させたときは次のように見ることができます。
(ADX2 for Unity SDKを少し改造しています。改造方法はこちら

「ユーザーデータ」はツールから出力する音声データに埋め込まれる情報ですが、似たような項目に「コメント」があります。これは出力データに含まれず、ツール上でのメモとしての機能で使います。

(後編に続く)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでカメラの向きを参考に移動するソースコード

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    Transform mainCamera;

    float scale = 0.1f;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            var direction = Quaternion.Euler(mainCamera.eulerAngles) * Vector3.forward;
            transform.position += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.S))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.back;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.A))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.left;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.D))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.right;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでカメラの向き(ベクトル)を参考に移動するソースコード

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    Transform mainCamera;

    float scale = 0.1f;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            var direction = Quaternion.Euler(mainCamera.eulerAngles) * Vector3.forward;
            transform.position += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.S))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.back;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.A))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.left;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
        if (Input.GetKey(KeyCode.D))
        {
            var direction = Quaternion.Euler(mainCamera.transform.eulerAngles) * Vector3.right;
            transform.localPosition += new Vector3(direction.x, 0, direction.z) * scale;
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】新規ゲームのUI開発で気をつけた39のTips前編

本記事はサムザップ #1 AdventCalendar 2020 の12/1の記事です。

株式会社サムザップでUnityエンジニアをしている大庭@ohbashunsukeです。

前年に引き続き今年もサムザップ社員エンジニア全員参加のアドベントカレンダーが始まりました。僕は昨年のアドベントカレンダーでも以下のUnity関連のTips記事を執筆しましたが、今年も引き続きUnityにまつわるTipsを執筆していこうと思います。

本Tipsの前提

普段僕が関わることの多い「スケジュールが潤沢ではないUnityを使った中〜大規模スマホゲーム開発」を前提としたTipsになっております。ここ数年新規ゲーム開発の立ち上げに携わることが多く、そこで得たUI開発の知見を詰め込んでみた感じです。一つでも皆様の為になる話があれば幸いです。

おまえ誰だよ?

最初に軽く自己紹介しておきます。大庭俊介@ohbashunsukeです。
グラフィックデザイナー、Flashデベロッパーを経て、Unityでスマホゲームを作っています。Unity歴は7年くらいで、今までコマンドバトル、ピンボール、麻雀などインゲーム・アウトゲーム・基盤問わず開発に携わってきました。現在は新規開発に携わりつつサムザップエンジニア全体の組織運営に関わっています。
僕の開発思想としてスピードを何よりも重要視しています。ただスピードと言っても雑に作るという意味ではありません。

ゲーム開発に限りませんが実機で触って初めて理解できることが多く、頭の中では上手く行っていることも触ってみるとイマイチな事ってたくさんあります。

「作って評価して壊す」このサイクルをいかにくり返す事が出来るかが、ゲームのクオリティに大きく関わってくると考えています。

そして「作って評価して壊す」をくり返すためには開発スピードが必要になります。
開発序盤は雑でも良いので必要とされる要件を満たして実機で触れる状態、要は適切に評価出来る状態にいち早く到達させます。
評価した結果大半を壊すことになったとしても、早く作っている事で残り時間には余裕があり、作り直すことで更にブラッシュアップされたプロダクトになる可能性が高いです。以上の理由からスピードを意識したモノづくりは利点しか無いと考えています。

そして、今回紹介するTipsは上記の思想を大きく反映しているものとなっています。

弊社エンジニアブログで開発スピードについて「新規プロジェクトでスピード開発を実現するエンジニアの挑戦」というタイトルで寄稿した記事がありますので、気になる方はそちらを読んでみてください。

※ここからが本題です。

■UI設計思想編

新規のゲームUI開発に限りませんが、開発で最も重要なのは「設計」だと考えています。設計次第で中盤から終盤にかける開発スピード、発生するバグの数が雲泥の差で変わってきます。

また中〜大規模のゲーム開発の場合、多くの人が関わるという点もポイントです。
設計次第で投入した人数の割に成果が出ないという事はよくあります。本セクションでは具体的な技術というより設計者のマインド寄りの話に触れていきます。

Tips.1 UI設計者はアプリ全体の設計をする必要がある

初っ端から偉そうな事を書いてしまっている自覚はありつつも自分に言い聞かせるという意味でも敢えて書いています。すみません。

UI設計をするエンジニアはアプリ全体の設計をする意識が必要になります。UIと聞いてパッと思いつくボタンやダイアログ、リストなどの設計をするだけでは足りません。

UIはゲーム起動直後から画面に映し出されます。そして、シーン遷移、画面遷移、通信中、アセットのロードなど、ゲームが起動している最中UIが消えることはありませんし、適切なUXを常に提供し続けなければなりません。

その「適切なUX」は「点」の設計では実現が難しく、全体を包括した「線」、「面」でなくてはいけません。

何が言いたいかというとUXは仕様書やデザインデータからは伝える事が困難です。UI設計を担当するという事は、ゲーム全体の挙動におけるUXの責任が乗っかってくるという事です。

Tips.2 多人数開発が可能であること

本Tipsのターゲットは中〜大規模スマホゲーム開発という事なので多人数が同時開発できる必要性があります。というのも、プロジェクトが進行していくうちに仕様変更によるスケジュールの遅延、大量に発生するバグを収束させる為に人を投入するという事はよくあります。これらを見越して多くのメンバーが同時に開発できる設計は必須です。

これは初期の設計段階から意識しておかねばなりません。途中からでも可能ですが、大きく手戻りする可能性があるのでオススメしません。具体的にどうするか見ていきましょう。

例えば以下のような単位での作業分担です。

  • 各画面単位
  • UIパーツ単位
  • シーン単位
  • ダイアログ単位

画面であれば1人1画面を担当し、複数人がそれぞれの画面を同時編集できる開発が望ましいと考えます。ただし主要画面(例えばホーム、クエストなど)は頻繁な修正・改修が入り、同時に編集する必要が出てくる場合があり、その対策も心がけたい所です。

UnityではPrefab単位にパーツを切り分けておく事ができるため1画面1Prefabの中にNestedPrefab機能を使い、パーツを小分けにして作業分担できるような設計をしておくと更に開発の効率は上がります。
※補足 : 僕自身があまりAdditiveSceneで開発をしないため1画面1シーンについては割愛しています。

具体的な手法は一旦置いておいて、まずはUI設計思想として将来同時多発で開発を行うという事を視野に入れ、他人との作業がコンフリクトし辛い環境にするという視点を持つ事が大事だと思っています。

Tips.3 一度決めた設計で無理して突っ走らない

UIの基本実装が落ち着いてきたとしても、定期的な設計の見直しをする事は大事です。
開発の序盤から全てを考慮することは難しく、ゲームの仕様もどうしても変化していくため、決めた設計のコンセプトから少しずつずれていく事があります。

ずれを感じた際に、思い切って見直す、具体的には壊して作り直すくらいの思い切りも大事な場合があります。

というのも壊す事ができるのはゲームがリリースする前のタイミングでしかできません。リリース後は壊す暇はなく、売上数字とともに突っ走っていくだけなので...。

設計次第で中盤から終盤にかける開発スピード、発生するバグの数が雲泥の差で変わってきます

前述の引用ですが、設計がゲーム開発にとって最も重要なファクターなのでリリース前であれば出来る限り良い状態に持っていくべきで、可能な限り設計にはこだわり抜くのが良いと考えています。

■具体的なUI設計編

今まで設計の思想について触れてきましたが、ここからは具体的なUIの設計について紹介していきます。

Tips.4 エントリーポイントの設計

エントリーポイントとはプログラムの始まりの場所です。
本Tipsのエントリーポイントとは、Unityが隠蔽している厳密なプログラムのエントリーポイントの話ではなく、Unityエンジニアが実際に書いたC#の始まりの部分を指しています。エントリーポイントを設計する上で気をつけている事があります。

  1. どのシーンからでも起動できるようにする
  2. エントリーポイントという事を分かりやすくする

1.どのシーンからでも起動できるようにする

後述の「Tips.5 シーン遷移システムの設計」に繋がりますがアウトゲームシーン担当、インゲームシーン担当と大抵シーン毎に担当が分かれるため、各シーンから直接起動出来る設計をしておくと開発時のストレスが格段に減ります。

それを実現するためにゲーム起動時の一連の処理をまとめておくと良いでしょう。

  • マスターゲット
  • ユーザー作成
  • ログイン
  • チュートリアルスキップ
  • アセットダウンロード

以下のようなエントリーポイントを処理するSceneEntryクラスを定義して各シーンのルートにAddComponentしておきます。

public class SceneEntry : MonoBehaciour
{
    void Awake()
    {
        // Entry Point
        Initialize();
    }

    private async Task Initialize()
    {
        // ゲーム全体で最初の1度しか実行しない処理
        await InitializeIfBootScene();

        if (CurrentScene == "TitleScene")
        {
            // タイトルシーンの場合はユーザーの画面タップを待機する
            await WaitTap();
            // GUIと共に初期化処理を実行する

//~~~~~ 略 ~~~~~

        }
        else
        {
            // 画面タップを待機することなく初期化処理を一気に走らせる
            await MasterGet();
            await CreateUser();
            await Login();
            await SkipTutorial();
            await DownloadAssets();
        }
    }
}

※紹介しているソースコードはあくまでイメージです

このように通常起動フロー、そうではない開発用起動フローを分けて各シーンから起動できるようにしておきます。もちろん開発が進む中で保守するコストは生まれますが、実行時のテストのために毎度ゲームタイトル画面から起動しないといけないという不便さからは解放されます。

UnityEditor実行のような開発中何度も繰り返される事はできる限り時間短縮を意識した設計をしておくと、より多くの開発時間が確保出来るでしょう。

2. エントリーポイントという事を分かりやすくする

新しく参画したメンバーがいち早く戦力になるためには、ソースコードの理解が必須です。UI設計者が出来ることの1つとして学習コストを下げる事を挙げてみます。

プロジェクトのソースコードを理解する上で最初に読むべきポイントは、処理がどこから始まっているかを理解する事。そうエントリーポイントがどこなのかを理解する事です。

Unityは基本的にはヒエラルキーにあるコンポーネントのAwakeやStartメソッドがシーンロード直後に実行されるため、その辺りに目星を付けますがプロジェクトによってはAwakeやStartが複数存在するという事もあります。

そんな時に、ヒエラルキーを見て一発で分かるようにしておくことも、地味ですが開発スピードを上げるためには大切な事だと思っています。


上図のようにパット見でエントリーポイントが分かるようなヒエラルキーになるようにAwake、Startの使用を制限するという事も設計する上で気にしておくと良いかもしれません。この辺りの話題は、後述の「Tips.8 MonoBehaviourクラスのAwake、Startメソッドは極力使わない」で解説しているのでそちらをどうぞ。

Tips.5 シーン遷移システムの設計

  • タイトルシーン
  • アウトゲームシーン
  • バトルシーン
  • バトルリザルトシーン
  • ストーリーシーン

といった多人数開発・リソース管理を考慮してシーン分けをする事が多いと思います。

シーン遷移はUnityが提供するSceneManagerクラスを使うことになりますが、そのままベタ書きせずにラッパークラスを用意します。理由としてはシーン遷移中には下記のようなUIの表示・機能が必要になるためです。

  • Tipsの表示
  • ローディングの表示
  • シーン間のデータ送信
  • アセットのダウンロードまたはロード
  • 現在のシーン、遷移するシーンの取得
// シーン遷移前処理
ShowTips();
ShowLoading();
// シーン遷移開始
SceneManaer.LoadScene("InGameScene");

//~~~~ 略 ~~~~

// シーン遷移後にシーン遷移中UIを非表示
HideTips();
HideLoading();

上記のような各遷移処理毎に同じようなコードを書きたくないため、ラッパークラスの中に隠蔽するなどして処理をまとめた方が良いでしょう。ここではSceneManagerをラップしたSceneManagerWrapperクラスを用意したとします。

// シーン遷移開始処理
SceneManagerWrapper.onStartLoad += ()=> {
    ShowTips();
    ShowLoading();
};

// シーン遷移完了後の処理
SceneManagerWrapper.onCompleteLoad += ()=> {
    HideTips();
    HideLoading();
};

// シーン遷移開始
await SceneManagerWrapper.LoadSceneAsync("InGameScene", new ToInGameSceneData{ battleId = _battleId });

// 遷移先シーンでシーン遷移間のデータを受け取る
var data = SceneManagerWrapper.GetData() as ToInGameSceneData;

※上記はSceneManagerラッパークラスのソースコードのイメージです

前述の「Tips.4 エントリーポイントの設計」で現在のシーンを取得するAPIをこのラッパークラスで提供しています。

ここまではエンジニアリングな話でしたが後述の「Tips.7 画面遷移システムの設計」も同様ですが、遷移中のUIをどうするのかを予めUIデザイナーと連携をとっておく必要があります。

  • ローディングを表示する
  • 通信中を表示する
  • Tipsを表示する
  • Tipsを表示するならどこからそのデータを取得するか?
  • 表示のアニメーションのタイミング

などなど。


TitleSceneからOutGameSceneにシンプルに遷移してしまうと、TitleSceneを破棄したタイミングで一瞬何も無い状態が画面に表示されてしまいます。

この辺りは後述の「Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ」で説明していますが、以下のようにシーンの継ぎ目が見えないようにして、ローディングやTipsを表示させる設計をします。

この辺りはメモリ解放など技術的な話も多く、エンジニア主導で話を進めていかないと、プロジェクトが前に進まない可能性があるため、エンジニア側から解決手段を提供すると良いかもしれません。

システム面、UI/UX面全てを包括した形でシーン遷移のシステムは作っていく必要があるという事になります。

Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ

  • ダイアログ
  • ローディング
  • タップ・スワイプエフェクト

上記のようなシーンをまたいで使用する共通オブジェクトはシーン遷移で破棄したくないので、DontDestroyOnload領域に生成するようにしています。

前述の「Tips4. エントリーポイントの設計」と関連して、ゲーム全体で最初の一度しか実行しない処理の中で実行するような設計になっています。これにより各シーンから実行する際も共通オブジェクトは何も特殊な処理を挟む事なく使用できるようになっています。

注意点としてシーン遷移間で破棄されないため、メモリーリークする可能性があるため注意です。

Tips.7 画面遷移システムの設計

前述の「Tips.5 シーン遷移システムの設計」と似ていますが、こちらは1つのシーン内で発生する画面遷移の話になります。前提の通り複数人で開発出来るよう、1画面1Prefabの単位で分けて設計しています。

画面遷移処理の流れ

  1. ボタンなどのトリガーから画面遷移リクエストをマネージャに送る
  2. マネージャから画面遷移管理へ次の画面遷移をリクエスト
  3. 画面Poolerから次の画面を取得(新規生成 or キャッシュを返却)
  4. 古い画面と次の画面を入れ替える

図にすると以下のようなイメージです。

画面遷移システムを設計する上で以下のような事を考えておく必要が出てきます。

  1. どの画面からも遷移でき、どの画面へも遷移できる
  2. Androidバックキー対応
  3. 画面遷移中のユーザー入力への対応

1. どの画面からも遷移でき、どの画面へも遷移できる

プロジェクト初期は「画面Bは画面Aからしか遷移することはありません」という仕様だったとしても、開発中盤で「やっぱり画面Cからも遷移することになりました!!」みたいな仕様変更はよく発生することです。
これは本当によくあることなので、予め画面はどの画面からでも遷移できるように作って汎用性を高めておくのが良いです。

2. Androidバックキー対応

Androidバックキー対応も画面遷移システムを設計する上で重要なポイントになります。戻るの要件はプロジェクトごとに変わってきますが、基本的には遷移前の画面に遷移するという事になります。実装方法は様々ありますが、例として履歴を保持するやり方を挙げてみます。
戻るが実行されたら画面遷移履歴リストから一つ前の画面情報を取得して遷移させるというやり方です。そのために、画面遷移履歴情報と現在の画面情報を画面遷移マネージャに保持しておきます。

3. 画面遷移中のユーザー入力への対応

画面遷移中のユーザー入力に対してどこまで対応するのかは悩みどころです。画面Aに遷移中に画面Bに遷移するユーザー入力を受け取った場合、画面Aに遷移する処理を全てキャンセルする必要があります。

  • アセットのロードキャンセル
  • ロード済みアセットの破棄
  • 各処理の中断

などなど。各画面実装の複雑さが増していき、学習コストが上がります。またこれをテストするコストも高くなります。
以上の事を踏まえると画面遷移中の割り込み処理対応はコスト高めです。
手っ取り早いやり方としては、遷移中はユーザー入力を受け付けないという割り切りも視野に入れて工数を見積もった方がよいと考えています。
画面遷移マネージャが遷移状態を管理するようにして、遷移中はユーザー入力をブロックする処理を実行するような仕組みで実装します。

// クエストトップへ画面遷移リクエスト
await DisplayManager.Goto(DisplayType.QuestTop, new ToQuestTopData{
    // 表示させるクエストのIDを指定
    questId = _questId
});

※画面遷移サンプルコード

その他

また画面全体、一部で使うヘッダやフッターのような共通UIパーツの扱いも設計する上では考えておく必要があります。出来る限り各画面のユニークな実装処理を減らすように基盤システムを構築できるかが、その後の開発効率につながると考えています。

Tips.8 ダイアログシステムの設計

「Tips.7 画面遷移システムの設計」と同様、同時に複数人で開発出来るよう、1ダイアログ1Prefabの単位で分けて設計しています。

// アラートダイアログを開く
var alertDialog = await DialogManager.Open(
    new AlertDialogData{ title = _title, message = _message }
);

※ダイアログを開くサンプルコード

大抵必要になる要件として以下。

  1. Androidバックキー対応への配慮
  2. 結局ダイアログの上にダイアログはいくつも重なる事になる

1. Androidバックキー対応への配慮

Androidバックキー対応は、単純に重なっているダイアログを順番に閉じていく。または閉じてはいけないものもあるので、閉じては行けないダイアログが最前面の場合はAndroidバックキーが効かなくなるといった制御が必要になります。

2. 結局ダイアログの上にダイアログはいくつも重なる事になる

デザイナー的にダイアログの上にダイアログを載せたくない要望が出てくる事がありますが、プロジェクトが進んでいくうちに、ショップ購入確認ダイアログや例外処理のエラーダイアログをどうしても重ねざるを得ない状態になったりします。

ということで、僕は理想は追いかけつつも最初からダイアログは重なっていくことを前提に設計を予めしておきます。

Tips.9 MonoBehaviourクラスのAwake、Startメソッドは極力使わない

若干UI設計とは離れますが、大事な事なので差し込んでいます。
UnityエンジニアにとってのエントリーポイントとなるAwake、Startメソッドはとても便利な半面、多用しすぎると見通しが悪くなる、また処理順を保証できなくなってきます。

// A.cs
void Awake()
{
    // Do Something.
}

// B.cs
void Awake()
{
    // Do Something.
}

A、Bとコンポーネントヒエラルキーに存在している場合、この2つのAwakeの処理順はどちらが先に実行されるかは保証されません。1
処理順を把握できないという事は、処理順によるバグを生むリスクも伴います。

例) A => B という順にAwakeが呼ばれている場合は成功していた処理が、B => Aと呼ばれる事によって不具合が起きるといったものです。

改善の一例

下記のようにAwakeの回数を最小限に留めてそれに代わる処理(Initialize等)のメソッドを定義し、明示的に実行させる方が良いと考えています。

// A.cs (EntryPoint)
void Awake()
{
    _b.Initialize();
}

// B.cs
public void Initialize()
{
    // Do something.
}

個人で開発する時は、逆にAwakeやStartを使わない事が面倒くさいと感じることが多いかもしれませんが、中〜大規模の集団開発となるとAwake、Startの乱用がカオス化を招きます。
Awake、Startなどを使わないルールを設けることで逆に開発の効率が上がるのではないかと思っています。

Tips.10 Androidバックキーの事を忘れないで

今まで何度か出てきましたが、UIを設計する上でAndroidバックキーの存在を忘れてはいけません。

常に「Androidバックキーが今押されたらどうなるだろう?」という事を頭の片隅に置きつつ設計しておかないと、後から大きく見直す必要が出てきます。

以下のような優先順位で実装しておくと良いかなと思っています。

  1. ユーザー入力をブロックしていたら処理しない
  2. ダイアログのAndroidバックキー処理
  3. 画面のAndroidバックキー処理
// Androidバックキーが押された時の処理
void OnExecuteAndroidBackKey()
{
    // 画面遷移中などのユーザー入力をブロックしている時はAndroidバックキーは処理しない
    if (UIBlocking.IsEnabled()) return;

    // 画面全面のダイアログを優先して処理(処理を実行したらtrueを返却)
    if (DialogManager.OnExecuteAndroidBackKey()) return;

    // 画面側の戻る処理(処理を実行したらtrueを返却)
    if (DisplayManager.OnExecuteAndroidBackKey()) return;
}

Androidバックキー処理のサンプルコードですが、各マネージャクラスにAndroidバックキー処理をリクエストします。

常日頃からAndroidバックキーを実行する

画面遷移などの基盤に寄せた処理の場合はAndroidバックキー対応の抜け漏れは発生しづらいですが、各画面のユニークな演出中のAndroidバックキー対応は漏れがちです。
(例 : 強化演出中、ガチャ演出中など)

UnityEditorで開発中はエスケープキーを日常的に押して挙動的に大丈夫か習慣づけておくと良いでしょう。

Tips.11 戻るの設計

Androidバックキー対応と重複する部分がありますがUI上に表示する戻るボタンの対応についてです。
もちろんゲームの仕様によって戻るの要件は様々なので、一概に何が正しいはありませんが出来るだけシンプルに実装しておくのが良いと思っています。

例えば以下のような実装が考えられます。

  • 各画面の履歴を保持しておく
  • 戻るボタンが押されたら履歴から取り出して遷移
  • 履歴がなくなったら最初の画面に強制的に遷移
  • 画面毎の戻るの挙動を変更する場合は処理をオーバーライド

※画面遷移については前述の「Tips.7 画面遷移システムの設計」で説明しているので割愛します。

Unityで戻るを実装する時に複雑化しやすいのはシーンを跨いだ時の戻るの挙動です。
シーンを跨ぐと前のシーンの情報は基本的にメモリから破棄されます。履歴情報が破棄された状態から戻るということは、何かしらの情報を元に戻り先を作らなければなりません。

シンプルに最初の画面に戻すといった要件であれば簡単ですが、UX的にどうしてもそれでは不便という事で対応する必要も出て来る場合もあります。

このようなユニークな処理を実現するために、「画面毎の戻るの挙動を変更する場合は処理をオーバーライド」が出来るような設計にして、ある程度の仕様変更に耐えうる状態にしておくのが良いと考えています。

Tips.12 GameObjectはキャッシュして体感アップ

画面・ダイアログを表示する度にそれらを生成(Instantiate)していては体感が損なう場合があります。プロファイラーで計測すると分かりますがInstantiate処理はCPU負荷が高く低スペック端末だと顕著にフレームレートに響いてきます。

最低でもよく使う画面やダイアログはシーンロード時についでに生成してシーン内にキャッシュしておく設計にしてゲームプレイ中の生成コストを下げ、ゲームの触り心地を良くします。

キャッシュする事で、状態の管理が複雑化してしまいがちですが、そこは基板側の設計で吸収してあげるのが良いと考えています。表示の初期化や後始末忘れを回避するために開発者に提供するメソッドは絞ると良いでしょう。

public abstract class DialogBase : MonoBehaviour
{
        // ダイアログを開く前に呼ばれる初期化メソッド
        // キャッシュを再利用した際のリセット処理も兼ねる
        public virtual async Task ReInitialize(){}

        // ダイアログを開くメソッド
        public virtual async Task Open()
        {
                // 共通のダイアログ表示アニメーション
                // 特殊ケースの場合は上書きする
        }

        // ダイアログを閉じるメソッド
        public virtual async Task Close()
        {
                // 共通のダイアログを閉じるアニメーション
                // 特殊ケースの場合は上書きする
        }

        // ダイアログが閉じてアセットの破棄などを実行するメソッド
        public virtual void CloseComplete(){}
}

ソースコードはイメージですが、上記のような感じでベースクラスで各種イベントで実行されるメソッドが定義されていて、それをサブクラス側で具体的な処理を書いていきます。

Tips.13 UIアニメーションの実装方法基準

UIアニメーションをどうやって実装するかは悩み所です。

僕も毎度とても悩んでいます。ただ、何度か経験する中で一つの基準が出来たので紹介します。

結論から書きます。

  • 再利用するものはスクリプト
  • 再利用しないものは何でもいい

極端に聞こえるかもしれません。詳しくは今年11月のUnity勉強会「yokohama.unity#4」で発表した資料がありますので、ご参考にどうぞ。



LT版クリエーターとUnityエンジニアの狭間でUIアニメーションを設計する3つのTips/yokohama-unity4 - Speaker Deck

※UIアニメーションを具体的にどう作るかといったワークフローについては後編で執筆予定です

■後回しにしない方が良い事編

当たり前だと思いますが影響範囲が大きいものは早めに設計の目処をつけておきたいです。

この章では、明らかに最初に手を付けておかないといけないものから、パッと見後から実装しても大丈夫そうに見えるけど序盤にケリをつけておいた方が良さそうなUI周りの設計について紹介していきます。

Tips.14 UI解像度を決めてから本格的に動き出す

UIの解像度は開発序盤に決めておく必要があります。これが決まらないとデザイナーの元素材の解像度が決まらず、デザイナー・Unityエンジニア共に作業が出来ません。
※作業出来なくもないですが、高確率で作り直し確定です。

ちなみに僕は以下のような決め方をしています。

  • 基準となるスマホ端末を決める
  • 検証用の仮UIを用意して解像度毎に実機で確認

基準となるスマホ端末を決める

基準となるスマホ端末の決め方は、そのゲームのメインターゲットや戦略、その時その時の状況によって変わるためプロジェクト内で話あって決めます。

検証用の仮UIを用意して解像度毎に実機で確認

大事なのはスマホ実機で実際に見て確認するです。

ターゲットユーザー層に対してどのくらいのクオリティを提供しないといけないかプロデューサー・プランナー・デザイナーと相談することになります。

繰り返しになりますが大きな手戻りが発生する可能性があるため、UI解像度を決めてから本格的に動き出すことをオススメします。

Tips.15 セーフエリア対応

iPhoneX以降対応する必要のあるセーフエリアですが、これも後回しにすると地獄を見る案件です。

「セーフエリア対応処理を入れたらUIが重なって全画面調整する必要が出てきた」といった事がプロジェクト後半UIが量産された状態で発生すると深刻な手戻りコストになります。

そうならないように、開発序盤からセーフエリアに対する要件を決めて設計しておく方が良いでしょう。

Tips.16 UIの階層関係を決めておく

経験上以下のような階層構造になることが多いです。
※上から順に前面に表示されるもの

  1. タップエフェクト
  2. ローディングなどの最前面表示UI
  3. システム系ダイアログ
  4. 通常ダイアログ
  5. コンテンツUI
  6. 背景

システムダイアログ、通常ダイアログはそれぞれの階層で複数ダイアログが重なるような設計にする事になります。

タップエフェクトもこの階層を決める段階で実装しておく方が良いと考えます。パッと見後から実装しても大丈夫そうではありますが、階層構造を後から手を加えるのはリスクなので、序盤に仕組みを固めておくことを心がけています。

Tips.17 通信・アセットのダウンロード・ロード処理

「Tips.1 UI設計者はアプリ全体の設計をする必要がある」と関連した内容で、本来のUI開発の範疇なのかは微妙なラインですが、通信周りの設計はUXのクオリティを上げるために無視できません。

開発序盤のタイミングで、エンジニア側から通信やロードタイミングのすり合わせをUIデザイナーとしておくと良いでしょう。UIデザイナー側、Unityエンジニア側共に譲れない、譲りづらい部分があると思います。その辺りを開発序盤から話し合っておくと後々のトラブルが少なくなる印象です。

よく発生しがちなケースとして、UIデザイナーが想定していないタイミングで通信処理が走っていて、予想していたタイミングで表示されないといった認識のズレです。

仕様の変更やブラッシュアップで通信のタイミングが増加、変更する事は、開発中によくありますが、UXを損なっていないかという視点も大切です。

通信やアセットのロードのタイミングなどに変更が入る場合はUIデザイナーと認識を合わせておくと良いでしょう。

最後に

いかがでしたでしょうか。スケジュールが潤沢ではないUnityを使った中〜大規模スマホゲーム開発におけるUI開発前編(Tips.1〜17)を紹介してきました。

  • Tips.1 UI設計者はアプリ全体の設計をする必要がある
  • Tips.2 多人数開発が可能であること
  • Tips.3 一度決めた設計で無理して突っ走らない
  • Tips.4 エントリーポイントの設計
  • Tips.5 シーン遷移システムの設計
  • Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ
  • Tips.7 画面遷移システムの設計
  • Tips.8 ダイアログシステムの設計
  • Tips.9 MonoBehaviourクラスのAwake、Startメソッドは極力使わない
  • Tips.10 Androidバックキーの事を忘れないで
  • Tips.11 戻るの設計
  • Tips.12 GameObjectはキャッシュして体感アップ
  • Tips.13 UIアニメーションの実装方法基準
  • Tips.14 UI解像度を決めてから本格的に動き出す
  • Tips.15 セーフエリア対応
  • Tips.16 UIの階層関係を決めておく
  • Tips.17 通信・アセットのダウンロード・ロード処理

何か1つでも役に立つものがあれば幸いです。
続きのTips.18〜39は以下の内容について書いていくつもりで12/22リリース予定。

  • UIレギュレーション編
  • UIアセットワークフロー構築編
  • UI担当Unityエンジニアマインド編
  • UI開発Tipsおまけ編

2020年Qiitaアドベントカレンダーは始まったばかりです。
楽しんでいきましょう。

明日は@Gaku_Ishiiさんの「C#のネイティブ関数呼び出し(P/Invoke)時に行われていることを調べてみた」です。お楽しみに!!

サムザップのアドベントカレンダーは人数の関係上2つあります。
こちらもよろしくお願いいたします!
サムザップ #2 Advent Calendar 2020 - Qiita


  1. Script Execution Orderで指定は可能 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HoloLens2 × Azure Cognitive Services (Face API 編)

はじめに

HoloLensアドベントカレンダー1日目の記事です!
APIよくわからないと弟子から相談があったので、Cognitive Services系をまとめていきたいと思いまーす。
今日は、Cognitive ServicesのFace APIをHoloLens2でやってみました。
実機なしでもできるのでやってみてください。

開発環境

  • Unity 2019.4.1f1
  • MRTK 2.5.1
  • OpenCV for Unity
  • Windows PC

導入

1.AzureポータルからFace APIを作成し、エンドポイントとサブスクリプションキーをメモしておいてください。
image.png
image.png

2.Unityでプロジェクトを作成、MRTK2.5.1をインポートします。なんかウィンドウでたらApplyします。

3.メニューのMixed Reality Toolkit->Add to Scene and Configureしてください。
image.png

4.Build Settingsから、Universal Windows PlatformにSwitch Platformして、以下のように設定してください。あとAdd Open ScenesでScenes/SampleSceneにチェックが入っていることを確認します。

image.png

5.MixedRealityToolkitのDefaultHoloLens2ConfigureProfileをcloneし、Diagnostics->Enable Diagnostics Systemのチェックを外します。これでCPU使用率とかのデバッグ情報を非表示にできます。

image.png

6.Project SettingsのXR Settings、Publishing Settings->Capabilitiesを以下のように設定してください。
image.png

image.png

7.空のGameObjectを作成し、名前を「TapToCapture」にします。
image.png

8.Add Componentから「TapToCapture.cs」スクリプトを作成します。エアタップしたら、画像をキャプチャし、Face APIに投げるスクリプトになります。

TapToCapture.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using UnityEngine;
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Threading.Tasks;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.UnityUtils;
using OpenCVForUnity.ImgprocModule;

public class TapToCapture : MonoBehaviour
{
    public GameObject quad;

    [System.Serializable]
    public class Face
    {
        public string faceId;
        public FaceRectangle faceRectangle;
        public FaceAttribute faceAttributes;
    }

    [System.Serializable]
    public class FaceRectangle
    {
        public int top;
        public int left;
        public int width;
        public int height;
    }

    [System.Serializable]
    public class FaceAttribute
    {
        public float age;
        public string gender;
    }

    UnityEngine.Windows.WebCam.PhotoCapture photoCaptureObject = null;
    Texture2D targetTexture = null;

    private string endpoint = "https://<Your Endpoint>.cognitiveservices.azure.com/face/v1.0/detect";
    private string subscription_key = "<Insert Your API Key>";

    public void AirTap()
    {
        Resolution cameraResolution = UnityEngine.Windows.WebCam.PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
        targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);

        // PhotoCapture オブジェクトを作成します
        UnityEngine.Windows.WebCam.PhotoCapture.CreateAsync(false, delegate (UnityEngine.Windows.WebCam.PhotoCapture captureObject) {
            photoCaptureObject = captureObject;
            UnityEngine.Windows.WebCam.CameraParameters cameraParameters = new UnityEngine.Windows.WebCam.CameraParameters();
            cameraParameters.hologramOpacity = 0.0f;
            cameraParameters.cameraResolutionWidth = cameraResolution.width;
            cameraParameters.cameraResolutionHeight = cameraResolution.height;
            cameraParameters.pixelFormat = UnityEngine.Windows.WebCam.CapturePixelFormat.BGRA32;

            // カメラをアクティベートします
            photoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result) {
                // 写真を撮ります
                photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemoryAsync);
            });
        });
    }

    async void OnCapturedPhotoToMemoryAsync(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result, UnityEngine.Windows.WebCam.PhotoCaptureFrame photoCaptureFrame)
    {
        // ターゲットテクスチャに RAW 画像データをコピーします
        photoCaptureFrame.UploadImageDataToTexture(targetTexture);
        byte[] bodyData = targetTexture.EncodeToJPG();

        Response response = new Response();

        try
        {
            // string query = endpoint + "?detectionModel=detection_02&returnFaceId=true";
            // string query = endpoint + "?detectionModel=detection_01&returnFaceId=true&returnFaceLandmarks=false&returnFaceAttributes=age,gender,headPose,smile,facialHair,glasses,emotion,hair,makeup,occlusion,accessories,blur,exposure,noise";
            string query = endpoint + "?detectionModel=detection_01&returnFaceId=true&returnFaceAttributes=age,gender";
            var headers = new Dictionary<string, string>();
            headers.Add("Ocp-Apim-Subscription-Key", subscription_key);
            // headers.Add("Content-Type", "application/octet-stream");

            response = await Rest.PostAsync(query, bodyData, headers, -1, true);
        }
        catch (Exception e)
        {
            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
            return;
        }

        if (!response.Successful)
        {
            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
            return;
        }

        Debug.Log(response.ResponseCode);
        Debug.Log(response.ResponseBody);
        string newResponseBody = "{ \"results\": " + response.ResponseBody + "}";
        Face[] faces = JsonHelper.FromJson<Face>(newResponseBody);

        Mat imgMat = new Mat(targetTexture.height, targetTexture.width, CvType.CV_8UC4);

        Utils.texture2DToMat(targetTexture, imgMat);
        // Debug.Log("imgMat.ToString() " + imgMat.ToString());

        foreach (var face in faces){
            //Debug.Log(face.faceId);
            //Debug.Log(face.faceRectangle.left);
            //Debug.Log(face.faceRectangle.top);
            //Debug.Log(face.faceRectangle.width);
            //Debug.Log(face.faceRectangle.height);
            Imgproc.putText(imgMat, face.faceAttributes.age.ToString()+","+face.faceAttributes.gender, new Point(face.faceRectangle.left, face.faceRectangle.top-10), Imgproc.FONT_HERSHEY_SIMPLEX, 1.5, new Scalar(0, 0, 255, 255), 2, Imgproc.LINE_AA, false);
            Imgproc.rectangle(imgMat, new Point(face.faceRectangle.left, face.faceRectangle.top), new Point(face.faceRectangle.left + face.faceRectangle.width, face.faceRectangle.top + face.faceRectangle.height), new Scalar(0, 0, 255, 255), 2);
        }

        Texture2D texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);
        Utils.matToTexture2D(imgMat, texture);

        // テクスチャが適用されるゲームオブジェクトを作成
        // GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);   

        Renderer quadRenderer = quad.GetComponent<Renderer>() as Renderer;
        // quadRenderer.material = new Material(Shader.Find("Unlit/UnlitTexture"));

        // quad.transform.parent = this.transform;
        // quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f);

        quadRenderer.material.SetTexture("_MainTex", texture);

        // カメラを非アクティブにします
        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
    }

    void OnStoppedPhotoMode(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result)
    {
        // photo capture のリソースをシャットダウンします
        photoCaptureObject.Dispose();
        photoCaptureObject = null;
    }
}

9.PhotoCaptureのサンプルはこちらです。エアタップしたら、画像キャプチャするようにInputActionHandlerをAdd Componentし、AirTap関数を作成します。エアタップ(On Input Action Started)したらAirTap関数が発火するように設定します。

10.撮影できたら、targetTextureに画像データが入っているので、JPGにエンコードして、Face APIに投げます。FaceAPIのサンプルはこちらC#Pythonです。

11.endpointとsubscription_keyにメモしておいたものを貼り付けてください。

12.クエリパラメータは、detection_01モデルを使用、FaceId、年齢と性別を返すように設定しています。

<your endpoint>?detectionModel=detection_01&returnFaceId=true&returnFaceAttributes=age,gender"

ちなみにfaceAttributesはsmile, headPose, gender, age, facialHair, glasses, emotion, blur, exposure, noise, makeup, accessories, occlusion, hairといった情報が取れます。

13.MRTKのRestを用いてHTTPリクエストします。
ヘッダーは、"Ocp-Apim-Subscription-Key": subscription_keyを指定、"Content-Type": "application/octet-stream"はRestの中でやってくれるのでコメントアウトします。

14.クエリと画像データ、ヘッダーをPOSTします。
response = await Rest.PostAsync(query, bodyData, headers, -1, true);

15.response.ResponseBodyが下記のように返ってくればOKです。

[{"faceId":"f1b97cf1-58d0-4dc9-9169-e19cb0655e48","faceRectangle":{"top":347,"left":451,"width":285,"height":285},"faceAttributes":{"gender":"male","age":23.0}}]

16.Face APIのResponseBodyがリストのjsonになっているので、パースできるようにJsonHelper.csスクリプトを作成します。

JsonHelper.cs
using UnityEngine;
using System;

public class JsonHelper
{
    public static T[] FromJson<T>(string json)
    {
        Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(json);
        return wrapper.results;
    }

    [Serializable]
    private class Wrapper<T>
    {
        public T[] results;
    }
}

JsonHelperについて
- yuiyoichi/JsonHelper.cs
- How to load an array with JsonUtility?
- UnityのJsonUtilityでJSON配列を処理する

17.返ってきたResponseBodyを次のようにすることで、パースすることが可能になります。

{
    "results" : [ {...} ]
}

18.あとは仕様に合わせてFaceクラスとFaceRectangleクラス、FaceAttributeクラスを作成しました。

19.顔検出結果をOpenCVを使って画像に描画し、Quadのマテリアルに割り当てます。3D Object->Quadを作成しましょう。
image.png

OpenCV for Unity サンプルはこちら
- Texture2DからMatに変換
- 矩形を描画(Imgproc.rectangle)
- テキストを描画(Imgproc.putText)

20.OrbitalをAdd Componentし、Quadがカメラに追従するようにしています。
image.png

21.TapToCaptureにQuadをD&Dしてアタッチしたら完成です。

実行

HoloLens2にデプロイして、実行した結果がこちらになります。Editor上でもできるので試してみてください。
20201201_043500_HoloLens.jpg

お疲れ様でした。
明日は弟子(@Horomoto-Asahi)による「HoloLens 2のSpatialAwarenessの調査」です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DontDestroyOnLoadのGameObjectを列挙する

シーンに配置されているもの一覧

開発中のデバッグにおいて、今のシーンに配置されているGameObjectの一覧を取得したいことがあります。
UnityEditor上でならばHierarchy上にあるものを見れば済むわけですが、やっぱり実機上で何が起こっているのかを知りたいわけです。
このとき、普通のシーン上に配置されているものなら以下のコードで取得できます。

getCurrent
// 現在のHierarchyのRootにあるGameObjectの一覧
var currentSceneGameObjects = SceneManager.GetActiveScene().GetRootGameObjects();

DontDestroyOnLoadの一覧

普通のやつはそれでいいのですが、DontDestroyOnLoadに登録されたGameObjectを取得するAPIはありません。
実機でしか動作しないSDKが動的にテキトーに作成するGameObjectや、タイミングによって実機でしか起きない事象などを解析する場合など、どうしても実機上でDontDestroyOnLoadの一覧が取りたい場合があります。
というわけで、以下のような回避策で取得することができました。

workaround
// まずDontDestroyOnLoadされたGameObjectを作る
var go = new GameObject(string.Empty);
Object.DontDestroyOnLoad(go);
// DontDestroyOnLoadはSceneの一種なので,GameObject.sceneから取得することができる
var dontDestroyOnLoadGameObjects = go.scene.GetRootGameObjects();

解析用コード

これを利用してこんな解析用のコードを作ってみました。
このファイルをプロジェクトに入れておくだけで、シーン遷移のたびに、シーン名と合わせてDontDestroyOnLoadの一覧をログに出力してくれます。
CollectDontDestroyOnLoad()publicにしておいたので、好きなときにGameObjectの配列を取得することもできます。

gist

DontDestroyOnLoadCollector
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Nekomimi.Daimao
{
    public class DontDestroyOnLoadCollector
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void Register()
        {
            SceneManager.sceneLoaded += (scene, mode) =>
            {
                var ddol = CollectDontDestroyOnLoad();
                var separator = ", ";
                var s = string.Join(separator, ddol.Where(go => go != null).Select(go => go.name));
                Debug.Log($"DDOL_{scene.name} : {s}");
            };
        }

        public static GameObject[] CollectDontDestroyOnLoad()
        {
            var go = new GameObject(string.Empty);
            Object.DontDestroyOnLoad(go);
            var ddol = go.scene.GetRootGameObjects();
            Object.Destroy(go);
            return ddol.Where(o => o != null && !string.Equals(o.name, string.Empty)).ToArray();
        }
    }
}

実機での動作について

How can I get all DontDestroyOnLoad GameObjects?
ここの解答をもとに作ったのですが、Editorでしか動かないことが強調されています。

works only in the Editor

また、その根拠として示されている公式ドキュメントでも以下のような記載があります。

You do not have access to the DontDestroyOnLoad scene and it is not available at runtime.

でも私が試したところではAndroidでもiOSでも実機にブッこんだら動きました
UnityEditorのバージョンは2018と2019です。下のバージョンは覚えてない。
AndroidiOSでしか試してませんが、この分だと他のプラットフォームでも動きそうな気がします。
公式ができねえ! と言ってることをやってるので、使用範囲はあくまでもデバッグにとどめたほうがよいとは思いますが。

まとめ

先人の解答を参考にしつつも、とりあえず自分で数発殴ってみることは重要だなって思いました。
あと普通に考えて公式がそういうAPI用意してくれてもよくない……? って! 思いました!
DontDestroyOnLoadって状態の権化みたいなものなのでできるだけ使わないのがベストプラクティスな気もしますが。でも使っちゃうね。しょうがないね。

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】今日から始める Unity + Rider 生活【随時更新】

最初に

どうも、ろっさむです。

今回は JetBrains という会社から出ているつよつよIDE、Riderについてまとめていこうと思います。

また、記事内の作業環境は以下の通りとなります。

  • JetBrains Rider:2020.2
  • Windows:Windows 10
  • Mac:Cetalina 10.15.7

また、本記事で気になったことや書いてほしい機能等ありましたら以下Twitterもしくはメールアドレスまでご一報いただけますと幸いです。

Twitter:https://twitter.com/4_mio_11
mail:mio041100505@gmail.com

Riderとは

JetBrains Rider(以下 Rider)は、JetBrainsというIntelliJ IDEAやPyCharmなどのIDEを開発している会社から出ているクロスプラットフォーム対応の .NET IDEです。対応している環境は以下の通りです。

  • .NET Framework
  • .NET Core
  • Mono

フレームワークに関しても ASP.NET, ASP.NET Core, Xamarin, WPFなどをサポートしています。

特徴

  • マルチプラットフォーム
  • 高速/多機能
  • 低消費電力モード有り
  • シンタックスハイライト
  • IntelliSenseサポート
  • 豊富なコンテキストアクション
  • Git機能サポート(エディタ内で完結可能)
  • ターミナル機能サポート
  • 豊富なデバッグ機能
  • データベースとSQLの操作が可能
  • スニペットやテーマ、コーディングに役立つ2300以上のプラグインが公開されている。
  • C#7.0までの全てのC# バージョンと、C#8.0の一部機能をサポート

VSとの比較は以下の公式ページを確認してみてください。

https://www.jetbrains.com/rider/compare/unity-rider-vs-visual-studio/

価格

JetBrains Riderは非常に優秀なIDEですが、有料となります。ただし、金額としてはそこまで高くないと思いますし、費用対効果は非常に高いと感じられます。それでも無料で使用できるIDEをお探しの場合は、VSCode又はVSをオススメします。

今日からUnity + Visual Studio Codeを用いた快適な開発生活(随時更新中)

また、JetBrains Riderは最初の30日間はトライアル期間として無料で使用することが可能です。試しに使用してみてから購入を検討するのが良いかと思います。

では、気になるお値段はと言いますと、個人での使用であれば一か月あたり1600円となります。

image-20201127105345672.png

年間での支払いは16000円となりますが、翌年以降はどんどん安くなります。

image-20201127105641360.png

更に「特別オファー」として、学生は無料で使用できたり、スタートアップの企業は50%オフで使えたりなどの特典が存在します。詳しくは以下の公式ページをチェックしてみてください。

https://www.jetbrains.com/ja-jp/rider/buy/#discounts?billing=yearly

提供されているバージョン

記事執筆時点での最新バージョンは2020.2となりますがもうすぐ2020.3が出るようです。

また、アーリーアクセスバージョンが存在します。こちらはリリース前のビルドであり、新機能をいち早く試したい人向けとなります。導入しているプラグインによっては動かなくなる可能性があったり、エラーが含まれている可能性もあるため、基本的には正式リリースのバージョンを使用する方が良いでしょう。

アーリーアクセスバージョンは以下からDLできます。

https://www.jetbrains.com/ja-jp/rider/nextversion/

Riderの導入方法

まずは以下の公式サイトからDLを行い、インストールして下さい。

https://www.jetbrains.com/ja-jp/rider/

又は、ToolBoxという、UnityHubのようにツール毎のバージョンやインストール状況を管理するアプリを入れてからRiderをインストールしてください。

https://www.jetbrains.com/ja-jp/toolbox-app/

image-20201127113810218.png

インストール時にはユーザーインターフェースのテーマやエディターカラースキーム、キーマップ、を選択することができます。

インストーラーが立ち上がったら基本的にデフォルト設定で「NEXT」を押して行ってください。

以下の画面の際には、ご自身でお好きな設定を行ってから「NEXT」を押下してください。

image-20201127114539224.png

各設定項目は以下の通りです。

  • Creaste Desktop Shortcut:デスクトップにショートカットを作成するか否か
  • Update context menu:フォルダなどを右クリックした際に表示されるコンテキストメニューに、Add "Open Folder as Project"を追加するか否か
  • Update PATH variable(restart needed):PATH環境変数にRiderのランチャーディレクトリパスを追加するか否か(追加する場合は再起動が必要です)
  • Create Associations.sln.csprojなどの拡張子とRiderを関連付けるか否か

その後起動した時に、サブスクリプションのライセンスを持っていればアクティベートするか、30日間のお試しバージョン(Evaluate for free)を選択してEvaluateを押下します。

Unityへの連携方法

次にUnity側でRiderを使う設定にする必要があります。

Riderを使用したいプロジェクトを開き、上部メニューから Edit > Preferences...を押下します。

image-20201130110602101.png

次に Extemal Toolsを開いた状態で、 Extemal Script EditorRiderを選択します。

image-20201130110726163.png

これで対象のプロジェクト内でScriptを開いたりすると自動でRiderが立ち上がるようになります!

デバッグは右上のナビゲーションバーにあるボタンで操作が可能です。虫みたいなアイコンをクリックするとアタッチが開始されます。

image-20201201001921806.png

画面構成

image-20201130203620829.png

  1. Explorer Window: ファイルやアセット等を整理して表示してくれるビュー。Ctrl + Alt + Lで表示・非表示の切り替えが可能。ビューの種類切り替えによって表示を変更できる。UnityビューだとUnityエディタ内で見たときと同じプロジェクトツリーで表示されるため非常に見やすいので、おすすめ。 image-20201130211818166.png
  2. エディター:コードの確認・編集・Gitでの差分確認などのメイン作業をこのビューで行います。 他のビューをアクティブ状態にしていてもEscapeでエディターに戻ります。リファクタ等を行う際に表示されるアクションリストもこちらに表示されます。
  3. Tool Bar:エラーの確認やILの確認、Databaseの編集などがデフォルトで表示されているビューです。特に Ctrl + Alt + 2を押下することでエラー一覧をウィンドウ表示にして確認できるので便利です。 Shift + Escで閉じることができます。
  4. Tool Window:Gitやデバッグログ、TODO、Terminal等の表示を行います。デフォルトで Alt + 9でGitビュー、 Alt + 5でデバッグビュー、Alt + 8でUnit Testsビューが開かれます。こちらも Shift + Escで閉じることができます。また、それぞれのビューを開いた状態で Alt + 矢印キーによりタブの移動が可能です。
  5. ステータスバー:左側に最新のイベントメッセージ等が表示され、右側にはIDEのステータスなどを示したり、バックグラウンドタスクの進行状況等が表示されます。8:12というのが12行中の8行目を選択しているということです。エンコードの変更やファイルを読み取り専用に変更したりも可能です。緑のチェックマークアイコンにて、現在のソリューションの状態がわかります。エラーの場合は以下の画像のように表示され、エラー箇所へジャンプすることができます。
    image-20201130220843811.png

上記のアイコンなどはカーソルを合わせてしばらく待つと、説明が表示されるものがほとんどです。機能が豊富なため、最初は少し戸惑うかもしれませんが、慣れると非常に強力なIDEだと実感できるようになるので、是非基本操作を身に着けていきましょう。

https://pleiades.io/help/rider/Guided_Tour_Around_the_User_Interface.html

Unity開発で使えるRider機能紹介

Code Vision

VSで言うと CodeLensのようなものです。

ソリューション全体の解析を行って、Find Usagesや 子クラス、Gitでの情報を元に誰がそのクラスを作成・変更したか等の情報群がクラス名・変数・メソッド等の上部に表示されます。

image-20201130225239873.png

Find Usagesでは、対象のクラスをコンポーネントとして追加しているプレハブや、Unityのシーン、アセット等も即座に確認できます。更にダブルクリックするとUnityエディタ側で直接プレハブ等を開いて確認することができます。

Unityのメタデータの中身まで見てるからこその機能ですね。

https://pleiades.io/help/rider/Code_Vision.html

バージョン管理サポート機能

RiderではGitやSubversion、Perforce等を含むバージョン管理システムをサポートする機能があります。ここでは一例としてGit機能での見方を紹介します。

ファイル等の内容に変更があった場合にはTool Window表示にてGitビューを有効にすると以下のような表示となります。

image-20201130233132494.png

左側のチェックマークが Commitで、その下のUターンしてる矢印がロールバックとなります。スタッシュボタンもついています。ファイルを選択してクリックすると以下のように変更差分がエディタ上に表示されます。
赤が削除した箇所、あずき色が置き換え、黄色が追加した箇所で一目でわかるようになっています。

image-20201130232852807.png

コミットボタンを押下すると、コミット用のウィンドウが表示されます。

image-20201130234004367.png

また、プルリクのレビューや、コードに対するコメント、マージなども可能です。

https://blog.jetbrains.com/idea/2020/05/intellij-idea-2020-2-early-access-program-is-starting/?_ga=2.167265207.820440953.1606177116-227137106.1605402138#version_control

ちなみにGitの基本操作は上部のナビゲーションバーの左側にあるアイコン群からも可能となっています。

image-20201201001704257.png

左から pull, commit, push, history, rollback となっています。

Code Inspections

Riderでは、コード解析を行うことでコード上のパフォーマンス的に問題のあるコードの検出や、名前・コメントなどのスペルチェックを行ってくれます。

例えば、Updateメソッド内でGetComponentを使用していた場合は、呼び出しコストが高いため、対象箇所に波線を表示してお知らせしてくれます。または、新しい言語機能が使えたり、効率的なLINQ構文に差し替えが行える箇所でも同様にお知らせしてくれます。これらの箇所で Alt + Enterを押下するとアクションリストが表示され、クイックFixで自動修正することができます。

クイックアクション内ではソリューション内で似た問題があるかどうか、それをFixするかどうかなど、全体を解析した上での修正提案も行ってくれるので非常に便利です。

https://pleiades.io/help/rider/Code_Analysis__Code_Inspections.html

クイックFixを適用する

Code Inspectionsでも名前が出てきましたが、こちらの機能はエラーや警告が出た箇所をアクションリストから即時に修正を行える機能となります。

ハイライトされたコードの左側に以下のようなアイコンが表示されます。

image-20201130224308225.pngエラー:例)必要なファイルがimportされていない等
image-20201130224334816.png警告、提案:例)GetComponentをStart又はAwakeで行うように提案、スペルミスを発見したためリネームの提案
image-20201130224438600.png提案:例)リネーム、アクセス修飾子の変更

https://pleiades.io/help/rider/Code_Analysis__Quick-Fixes.html#applying-quick-fixes

データブレークポイント(Windows Only)

特定のプロパティのデータが変化する場合に一時停止する便利機能です。データが変化するけど、どこから変更されているのかがわからない時に有用となる機能なので是非活用していきたいところです。

https://blog.jetbrains.com/dotnet/2020/07/22/data-breakpoints-and-pin-to-frame-debugger-updates-in-rider-2020-2-eap/

Live Template

Riderではテキストの自動補完機能(つまりスニペット)があり、デフォルトで入っているスニペットを使用したり、自作したりすることができます。

image-20201130225449979.png

スニペットの作成は 上部メニューのFile > Settings > Editor > LiveTemplateから行えるようです。

以下の記事が非常に参考になります。

https://qiita.com/r-ngtm/items/ba875d32c17c5aacf80c

コード生成

Unityで開発する際に非常によく作成するメソッドを自動で生成してくれたり(しかも説明付き)、まだ宣言してないメソッド名をコード内に記述した場合に自動で生成したりなど至れりつくせりな機能があります。

ex) updateと打ち込んだ場合

image-20201201001017548.png

他にもインターフェースを継承した場合に、実装メソッドを生成してくれたり、プロパティを生成してくれたり……。

https://pleiades.io/help/rider/Code_Generation__Index.html

その他

このほかにもRiderには様々な便利機能が存在します。

  • Markdownプラグインを同梱
  • .shaderファイルのサポート
  • Burst 解析
  • デコンパイラ      etc...

公式ドキュメントが非常に充実しているため、一度通して見てみると良いかもしれません。

https://pleiades.io/help/rider/Introduction.html

おすすめプラグイン

JetBrains Riderでは自分好みにプラグインを入れることでカスタマイズしていくことができます。Ctrl + Alt + Sで設定画面を開き、Pluginsの選択するとプラグインの管理画面を表示することができます。不必要になったプラグインも Disable又はuninstallが気軽に行えるので、どんどん試していきましょう。

Japanese Language Pack EAP

image-20201201005037766.png

Riderを日本語対応させるプラグインです(私はそのまま使用しているためINSTALLはしていません)。

IdeaVim

image-20201201003107416.png

Vim操作に慣れ親しんでいる人なら使用したいプラグインです。このほかにもSublimeやAtomなどのユーザーのためのプラグインも存在しています。

CodeGlance

image-20201201003542125.png

エディターの右側にコードの全体を表す小さいビューが表示されるようになります。

コード内を移動するときとかに便利です。

Key Promoter X

image-20201201003700135.png

ショートカットキー全然覚えられん…って人向けです。マウスクリックなどで操作を行った場合に右下に「このショートカットキーで同じ操作できるよ」っていうポップアップを表示してくれたり、そのログをまとめて、Tool Barの方などに「忘れてそうなショートカットキーリスト」を出してくれたりします。

Material Theme UI

image-20201201003921871.png

エクスプローラーウィンドウに表示されるアイコンも含めて、IDEの配色テーマの種類を増やし、セットアップできるプラグインです。気に入るテーマがあるといいですね。

Translation

image-20201201004347760.png

コード内でわからない英単語が出てきた時に右クリックでコンテキストメニューを出すか、 Ctrl + Shift + Yで翻訳を行うことができます。オープンソースのコードとか読むときに便利です。

image-20201201004739948

String Manipulation

image-20201201004448791.png

文字列周りでの操作あれこれが可能になるプラグインです。

Rainbow Brackets

image-20201201004127399.png

対応する括弧同士を同じ色にしつつ、他の括弧と色を変えることで視覚的にコードがわかりやすくなるプラグインです。レインボー。

Rainbow CSV

image-20201201004257410.png

CSVファイルにて、項目ごとに色を変えてくれるプラグインです。レインボー。

Nyan Progress Bar

image-20201201004553167.png

ステータスバーにあるプログレスバーがレインボーにゃんこになります。かわいいレインボー。

image-20201130232948856.png

最後に

有償だけど超強力なIDEであるRider、使いこなせれば趣味も仕事も実際のコーディングの設計の方にパフォーマンスを発揮できるものとなっております。値段以上の価値があると思いますので、まずは30日間お試しで使用してみましょう。

それではここまで読んでいただいて有難うございました。

(後日チートシートも作成予定です)

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】今日から始める Unity + Rider 生活

最初に

どうも、ろっさむです。

今回は JetBrains という会社から出ているつよつよIDE、Riderについてまとめていこうと思います。

また、記事内の作業環境は以下の通りとなります。

  • JetBrains Rider:2020.2
  • Windows:Windows 10
  • Mac:Cetalina 10.15.7

また、本記事で気になったことや書いてほしい機能等ありましたら以下Twitterもしくはメールアドレスまでご一報いただけますと幸いです。

Twitter:https://twitter.com/4_mio_11
mail:mio041100505@gmail.com

Riderとは

JetBrains Rider(以下 Rider)は、JetBrainsというIntelliJ IDEAやPyCharmなどのIDEを開発している会社から出ているクロスプラットフォーム対応の .NET IDEです。対応している環境は以下の通りです。

  • .NET Framework
  • .NET Core
  • Mono

フレームワークに関しても ASP.NET, ASP.NET Core, Xamarin, WPFなどをサポートしています。

特徴

  • マルチプラットフォーム
  • 高速/多機能
  • 低消費電力モード有り
  • シンタックスハイライト
  • IntelliSenseサポート
  • 豊富なコンテキストアクション
  • Git機能サポート(エディタ内で完結可能)
  • ターミナル機能サポート
  • 豊富なデバッグ機能
  • データベースとSQLの操作が可能
  • スニペットやテーマ、コーディングに役立つ2300以上のプラグインが公開されている。
  • C#7.0までの全てのC# バージョンと、C#8.0の一部機能をサポート

VSとの比較は以下の公式ページを確認してみてください。

https://www.jetbrains.com/rider/compare/unity-rider-vs-visual-studio/

価格

JetBrains Riderは非常に優秀なIDEですが、有料となります。ただし、金額としてはそこまで高くないと思いますし、費用対効果は非常に高いと感じられます。それでも無料で使用できるIDEをお探しの場合は、VSCode又はVSをオススメします。

今日からUnity + Visual Studio Codeを用いた快適な開発生活(随時更新中)

また、JetBrains Riderは最初の30日間はトライアル期間として無料で使用することが可能です。試しに使用してみてから購入を検討するのが良いかと思います。

では、気になるお値段はと言いますと、個人での使用であれば一か月あたり1600円となります。

image-20201127105345672.png

年間での支払いは16000円となりますが、翌年以降はどんどん安くなります。

image-20201127105641360.png

更に「特別オファー」として、学生は無料で使用できたり、スタートアップの企業は50%オフで使えたりなどの特典が存在します。詳しくは以下の公式ページをチェックしてみてください。

https://www.jetbrains.com/ja-jp/rider/buy/#discounts?billing=yearly

提供されているバージョン

記事執筆時点での最新バージョンは2020.2となりますがもうすぐ2020.3が出るようです。

また、アーリーアクセスバージョンが存在します。こちらはリリース前のビルドであり、新機能をいち早く試したい人向けとなります。導入しているプラグインによっては動かなくなる可能性があったり、エラーが含まれている可能性もあるため、基本的には正式リリースのバージョンを使用する方が良いでしょう。

アーリーアクセスバージョンは以下からDLできます。

https://www.jetbrains.com/ja-jp/rider/nextversion/

Riderの導入方法

まずは以下の公式サイトからDLを行い、インストールして下さい。

https://www.jetbrains.com/ja-jp/rider/

又は、ToolBoxという、UnityHubのようにツール毎のバージョンやインストール状況を管理するアプリを入れてからRiderをインストールしてください。

https://www.jetbrains.com/ja-jp/toolbox-app/

image-20201127113810218.png

インストール時にはユーザーインターフェースのテーマやエディターカラースキーム、キーマップ、を選択することができます。

インストーラーが立ち上がったら基本的にデフォルト設定で「NEXT」を押して行ってください。

以下の画面の際には、ご自身でお好きな設定を行ってから「NEXT」を押下してください。

image-20201127114539224.png

各設定項目は以下の通りです。

  • Creaste Desktop Shortcut:デスクトップにショートカットを作成するか否か
  • Update context menu:フォルダなどを右クリックした際に表示されるコンテキストメニューに、Add "Open Folder as Project"を追加するか否か
  • Update PATH variable(restart needed):PATH環境変数にRiderのランチャーディレクトリパスを追加するか否か(追加する場合は再起動が必要です)
  • Create Associations.sln.csprojなどの拡張子とRiderを関連付けるか否か

その後起動した時に、サブスクリプションのライセンスを持っていればアクティベートするか、30日間のお試しバージョン(Evaluate for free)を選択してEvaluateを押下します。

Unityへの連携方法

次にUnity側でRiderを使う設定にする必要があります。

Riderを使用したいプロジェクトを開き、上部メニューから Edit > Preferences...を押下します。

image-20201130110602101.png

次に Extemal Toolsを開いた状態で、 Extemal Script EditorRiderを選択します。

image-20201130110726163.png

これで対象のプロジェクト内でScriptを開いたりすると自動でRiderが立ち上がるようになります!

デバッグは右上のナビゲーションバーにあるボタンで操作が可能です。虫みたいなアイコンをクリックするとアタッチが開始されます。

image-20201201001921806.png

画面構成

image-20201130203620829.png

  1. Explorer Window: ファイルやアセット等を整理して表示してくれるビュー。Ctrl + Alt + Lで表示・非表示の切り替えが可能。ビューの種類切り替えによって表示を変更できる。UnityビューだとUnityエディタ内で見たときと同じプロジェクトツリーで表示されるため非常に見やすいので、おすすめ。 image-20201130211818166.png
  2. エディター:コードの確認・編集・Gitでの差分確認などのメイン作業をこのビューで行います。 他のビューをアクティブ状態にしていてもEscapeでエディターに戻ります。リファクタ等を行う際に表示されるアクションリストもこちらに表示されます。
  3. Tool Bar:エラーの確認やILの確認、Databaseの編集などがデフォルトで表示されているビューです。特に Ctrl + Alt + 2を押下することでエラー一覧をウィンドウ表示にして確認できるので便利です。 Shift + Escで閉じることができます。
  4. Tool Window:Gitやデバッグログ、TODO、Terminal等の表示を行います。デフォルトで Alt + 9でGitビュー、 Alt + 5でデバッグビュー、Alt + 8でUnit Testsビューが開かれます。こちらも Shift + Escで閉じることができます。また、それぞれのビューを開いた状態で Alt + 矢印キーによりタブの移動が可能です。
  5. ステータスバー:左側に最新のイベントメッセージ等が表示され、右側にはIDEのステータスなどを示したり、バックグラウンドタスクの進行状況等が表示されます。8:12というのが12行中の8行目を選択しているということです。エンコードの変更やファイルを読み取り専用に変更したりも可能です。緑のチェックマークアイコンにて、現在のソリューションの状態がわかります。エラーの場合は以下の画像のように表示され、エラー箇所へジャンプすることができます。
    image-20201130220843811.png

上記のアイコンなどはカーソルを合わせてしばらく待つと、説明が表示されるものがほとんどです。機能が豊富なため、最初は少し戸惑うかもしれませんが、慣れると非常に強力なIDEだと実感できるようになるので、是非基本操作を身に着けていきましょう。

https://pleiades.io/help/rider/Guided_Tour_Around_the_User_Interface.html

Unity開発で使えるRider機能紹介

Code Vision

VSで言うと CodeLensのようなものです。

ソリューション全体の解析を行って、Find Usagesや 子クラス、Gitでの情報を元に誰がそのクラスを作成・変更したか等の情報群がクラス名・変数・メソッド等の上部に表示されます。

image-20201130225239873.png

Find Usagesでは、対象のクラスをコンポーネントとして追加しているプレハブや、Unityのシーン、アセット等も即座に確認できます。更にダブルクリックするとUnityエディタ側で直接プレハブ等を開いて確認することができます。

Unityのメタデータの中身まで見てるからこその機能ですね。

https://pleiades.io/help/rider/Code_Vision.html

バージョン管理サポート機能

RiderではGitやSubversion、Perforce等を含むバージョン管理システムをサポートする機能があります。ここでは一例としてGit機能での見方を紹介します。

ファイル等の内容に変更があった場合にはTool Window表示にてGitビューを有効にすると以下のような表示となります。

image-20201130233132494.png

左側のチェックマークが Commitで、その下のUターンしてる矢印がロールバックとなります。スタッシュボタンもついています。ファイルを選択してクリックすると以下のように変更差分がエディタ上に表示されます。
赤が削除した箇所、あずき色が置き換え、黄色が追加した箇所で一目でわかるようになっています。

image-20201130232852807.png

コミットボタンを押下すると、コミット用のウィンドウが表示されます。

image-20201130234004367.png

また、プルリクのレビューや、コードに対するコメント、マージなども可能です。

https://blog.jetbrains.com/idea/2020/05/intellij-idea-2020-2-early-access-program-is-starting/?_ga=2.167265207.820440953.1606177116-227137106.1605402138#version_control

ちなみにGitの基本操作は上部のナビゲーションバーの左側にあるアイコン群からも可能となっています。

image-20201201001704257.png

左から pull, commit, push, history, rollback となっています。

Code Inspections

Riderでは、コード解析を行うことでコード上のパフォーマンス的に問題のあるコードの検出や、名前・コメントなどのスペルチェックを行ってくれます。

例えば、Updateメソッド内でGetComponentを使用していた場合は、呼び出しコストが高いため、対象箇所に波線を表示してお知らせしてくれます。または、新しい言語機能が使えたり、効率的なLINQ構文に差し替えが行える箇所でも同様にお知らせしてくれます。これらの箇所で Alt + Enterを押下するとアクションリストが表示され、クイックFixで自動修正することができます。

クイックアクション内ではソリューション内で似た問題があるかどうか、それをFixするかどうかなど、全体を解析した上での修正提案も行ってくれるので非常に便利です。

https://pleiades.io/help/rider/Code_Analysis__Code_Inspections.html

クイックFixを適用する

Code Inspectionsでも名前が出てきましたが、こちらの機能はエラーや警告が出た箇所をアクションリストから即時に修正を行える機能となります。

ハイライトされたコードの左側に以下のようなアイコンが表示されます。

image-20201130224308225.pngエラー:例)必要なファイルがimportされていない等
image-20201130224334816.png警告、提案:例)GetComponentをStart又はAwakeで行うように提案、スペルミスを発見したためリネームの提案
image-20201130224438600.png提案:例)リネーム、アクセス修飾子の変更

https://pleiades.io/help/rider/Code_Analysis__Quick-Fixes.html#applying-quick-fixes

データブレークポイント(Windows Only)

特定のプロパティのデータが変化する場合に一時停止する便利機能です。データが変化するけど、どこから変更されているのかがわからない時に有用となる機能なので是非活用していきたいところです。

https://blog.jetbrains.com/dotnet/2020/07/22/data-breakpoints-and-pin-to-frame-debugger-updates-in-rider-2020-2-eap/

Live Template

Riderではテキストの自動補完機能(つまりスニペット)があり、デフォルトで入っているスニペットを使用したり、自作したりすることができます。

image-20201130225449979.png

スニペットの作成は 上部メニューのFile > Settings > Editor > LiveTemplateから行えるようです。

以下の記事が非常に参考になります。

https://qiita.com/r-ngtm/items/ba875d32c17c5aacf80c

コード生成

Unityで開発する際に非常によく作成するメソッドを自動で生成してくれたり(しかも説明付き)、まだ宣言してないメソッド名をコード内に記述した場合に自動で生成したりなど至れりつくせりな機能があります。

ex) updateと打ち込んだ場合

image-20201201001017548.png

他にもインターフェースを継承した場合に、実装メソッドを生成してくれたり、プロパティを生成してくれたり……。

https://pleiades.io/help/rider/Code_Generation__Index.html

その他

このほかにもRiderには様々な便利機能が存在します。

  • Markdownプラグインを同梱
  • .shaderファイルのサポート
  • Burst 解析
  • デコンパイラ      etc...

公式ドキュメントが非常に充実しているため、一度通して見てみると良いかもしれません。

https://pleiades.io/help/rider/Introduction.html

おすすめプラグイン

JetBrains Riderでは自分好みにプラグインを入れることでカスタマイズしていくことができます。Ctrl + Alt + Sで設定画面を開き、Pluginsの選択するとプラグインの管理画面を表示することができます。不必要になったプラグインも Disable又はuninstallが気軽に行えるので、どんどん試していきましょう。

Japanese Language Pack EAP

image-20201201005037766.png

Riderを日本語対応させるプラグインです(私はそのまま使用しているためINSTALLはしていません)。

IdeaVim

image-20201201003107416.png

Vim操作に慣れ親しんでいる人なら使用したいプラグインです。このほかにもSublimeやAtomなどのユーザーのためのプラグインも存在しています。

CodeGlance

image-20201201003542125.png

エディターの右側にコードの全体を表す小さいビューが表示されるようになります。

コード内を移動するときとかに便利です。

Key Promoter X

image-20201201003700135.png

ショートカットキー全然覚えられん…って人向けです。マウスクリックなどで操作を行った場合に右下に「このショートカットキーで同じ操作できるよ」っていうポップアップを表示してくれたり、そのログをまとめて、Tool Barの方などに「忘れてそうなショートカットキーリスト」を出してくれたりします。

Material Theme UI

image-20201201003921871.png

エクスプローラーウィンドウに表示されるアイコンも含めて、IDEの配色テーマの種類を増やし、セットアップできるプラグインです。気に入るテーマがあるといいですね。

Translation

image-20201201004347760.png

コード内でわからない英単語が出てきた時に右クリックでコンテキストメニューを出すか、 Ctrl + Shift + Yで翻訳を行うことができます。オープンソースのコードとか読むときに便利です。

image-20201201004739948

String Manipulation

image-20201201004448791.png

文字列周りでの操作あれこれが可能になるプラグインです。

Rainbow Brackets

image-20201201004127399.png

対応する括弧同士を同じ色にしつつ、他の括弧と色を変えることで視覚的にコードがわかりやすくなるプラグインです。レインボー。

Rainbow CSV

image-20201201004257410.png

CSVファイルにて、項目ごとに色を変えてくれるプラグインです。レインボー。

Nyan Progress Bar

image-20201201004553167.png

ステータスバーにあるプログレスバーがレインボーにゃんこになります。かわいいレインボー。

image-20201130232948856.png

最後に

有償だけど超強力なIDEであるRider、使いこなせれば趣味も仕事も実際のコーディングの設計の方にパフォーマンスを発揮できるものとなっております。値段以上の価値があると思いますので、まずは30日間お試しで使用してみましょう。

それではここまで読んでいただいて有難うございました。

(後日チートシートも作成予定です)

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity Cloud Buildのビルド番号などをゲーム中で取得するライブラリ、CloudBuildManifestを作った!

この物語はフィクションです。

非プログラマー 「なんかバグってる!直して!」

プログラマー  「どれどれ?バージョンは?」

非プログラマー 「えーっと、ちょっと前のやつ!」

プログラマー  (ちょっと前っていつのだよ?何回ビルドしたと思ってんだよ!?)

はじめに

先ほどの物語はフィクションではありますが、モバイルゲームアプリの開発現場において、このような不幸な経験がある方も多いのではないでしょうか?

チーム内にゲームを配布していると、チームメンバーから「バグ報告」や「要望」をもらうことがあります。この時、問題になるのがチームメンバーが「どのバージョンの・どのビルド設定のバイナリを使っているのか?」です。頻繁に更新・修正・配布しているプロジェクトでは、古いバージョンを使い続けて、直っているバグを報告してしまうこともありえます。また、ビルド設定が複数あるプロジェクトでは、想定と違ったビルド設定をチームメンバーが使っていて、バグの原因特定に苦戦することもありえます。

もしこのような課題を抱えていて、かつ、あなたがUnity Cloud Buildを使っているのであれば、ぜひ私が作った、CloudBuildManifestを用いて、ビルドターゲットとビルド番号をアプリ内に表記してみませんか?

この投稿では、私の作ったCloudBuildManifestを紹介します。

ソースコードはこちら : https://github.com/RyotaMurohoshi/CloudBuildManifest

CloudBuildManifestの使い方

Unity Package Managerが使えるUnity、そしてGitがインストールされている環境で、導入したいUnityプロジェクトのPackages/manifest.jsonファイルのdependenciesセクションに次の一行を追加して下さい。TAG_YOU_WANT_TO_USEには、使いたいCloudBuildManifestのバージョンを指定してください。

"com.ryotamurohoshi.cloudbuildmanifest": "https://github.com/RyotaMurohoshi/CloudBuildManifest.git#TAG_YOU_WANT_TO_USE",

次のようなコードを使うことで、ビルド情報を取得することができます。私のおすすめは、次のようにCloudBuildTargetNameBuildNumberを併記してゲームアプリ内に表示する方法です。

string buildInfoText;
if (BuildManifest.HasBuildManifest())
{
    var buildManifest = BuildManifest.Load();
    buildInfoText = $"{buildManifest.CloudBuildTargetName} #{buildManifest.BuildNumber}";
}
else
{
    buildInfoText = "Not Cloud Build";
}

ちなみにUnity Cloud Buildでビルドしていない場合、たとえばローカルのUnity Editorで実行した場合、BuildManifest.Load()はnullを返します。

BuildManifestクラスが持っているメンバーは次の通りです。

  • ScmCommitId: Unity Cloud Build によってビルドされた commit、または changelist
  • ScmBranch: ビルドされたブランチ名
  • BuildNumber: このビルドに関連する Unity Cloud Build 番号
  • BuildStartTime: ビルドプロセスが始まったときの UTC timestamp
  • ProjectId: Unity Cloud Build プロジェクト識別子
  • BundleId: (iOS/Androidのみ) bundleIdentifier は Unity Cloud Build 内で設定されます
  • UnityVersion: Unity Cloud Build がビルド作成に使用した Unity のバージョン
  • XcodeVersion: (iOS のみ) ビルドに使用される XCode のバージョン
  • CloudBuildTargetName: ビルドされたプロジェクトビルドターゲットの名前。

CloudBuildManifestの内部のこと

内部がどのようになっているかを説明します。Unity Cloud Buildを行うと、Assets/UnityCloud/Resources以下にUnityCloudBuildManifest.jsonというJSON形式のテキストファイルが自動で置かれます。この生成されるJSON(CloudBuildManifest)の要素には、次のようなビルドしたコミットのリビジョン、ビルドに使ったUnityやXcodeのバージョン、ビルド設定であるビルドターゲット名が入ります。

{
    "scmCommitId":"abcdefghijklmnopqrstuvwxwz01234567890123",
    "scmBranch":"master",
    "buildNumber":"3",
    "buildStartTime":"1/1/2017 11:11:11 PM",
    "projectId":"cloud_build_example",
    "bundleId":"com.teammurosta.cloud_build_example",
    "unityVersion":"5.5.0f3",
    "xcodeVersion":"6.3.1",
    "cloudBuildTargetName":"default-ios",
}

そのAssets/UnityCloud/Resources以下に生成された、ファイルをResourcesクラスを使い読み

まとめ

CloudManifestを使えば、ビルドしたバージョンや設定などのビルド情報をアプリ内で表示することができます。ぜひ、Unity Cloud Buildを使ってみてください!

そして気に入ったら、GitHubのCloudManifestリポジトリにStarをください!テンション・モチベーションが上がります!

補足

この記事は、以下の自分の記事を再構成・合成し、編集・加筆したものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む