20191223のUnityに関する記事は29件です。

UniRx & UniTask とは何なのか

はじめに

何番煎じなのかわかりませんが、過去にいろいろ解説した記事へのリンクをまとめる意味も込めて解説します。

UniRxとは

おすすめ資料

概要

UniRxとは、Reactive ExtensionsをUnity向けの実装したC#ライブラリです。
かなり昔のバージョンのUnityでも扱うことができます。
導入することで、Unityにおいて次のような処理の実装が簡単になります。

  • 非同期処理
    • 何らかの処理の完了を待ち受けて次の処理を行うような処理の管理
    • エラーハンドリングやリトライ処理の簡略化
    • 実行結果のハンドリングやキャッシュ
    • 実行スレッドの柔軟な切り替え
// data.txtをスレッドプール上で読み込み、メインスレッド上で表示する
Observable
    .Start(() => File.ReadAllText("data.txt"))
    .ObserveOnMainThread()
    .Subscribe(x => Debug.Log(x));
  • イベント処理
    • 処理のトリガーと実際にハンドリングする場所を分離して記述できる
    • イベントメッセージの加工や合成
    • 実行スレッドの柔軟な切り替え
void Start()
{
    // Jumpボタンが押されたらログに出す
    // 1度発動したら1秒間は何もしない
    this.UpdateAsObservable()
        .Where(_ => Input.GetButtonDown("Jump"))
        .ThrottleFirst(TimeSpan.FromSeconds(1000))
        .Subscribe(_ => Debug.Log("Jump!!"));
}

UniRxは「非同期処理」と「イベント処理」の2つを扱うことができるライブラリです。
特に「時間」の扱いに長けており、Unityにおける「フレームをまたいだ処理」などの実装が簡単になります。

なお、UniRxが提供するこれら便利な機能は正式には「Observable」と呼ばれています。

Observable

ObservableとはUniRxが提供する、「メッセージを通知のための機構およびそのオブジェクト」の指します。
型としてはIObservable<T>で表現されます。

// 実行結果は IObservable<T> でハンドリングする
private IObservable<string> ReadFileObservable(string path)
{
    return Observable
        .Start(() => File.ReadAllText(path))
        .ObserveOnMainThread();
}

Observableの仕組み、およびSchedulerOperatorの概念について詳しく知りたい方は次の資料を御覧ください。

Operator一覧

UniRxでもっとも便利な機能がこのOperatorです。
OperatorObservableに対するさまざまな処理を提供してくれます。
そのためこのOperatorを組み合わせるだけで、だいたいの処理の実装が完了してしまいます。

UniTaskとは

おすすめ資料

概要

UniTaskとは、C#の標準TaskおよびTaskSchedulerをUnity向けに最適化して実装したC#ライブラリです。
UniTaskは標準Taskと比べてパフォーマンスが出るため、非同期処理を扱う場合は是非導入してほしいライブラリです。ただし導入時はUnity 2018.3以降を推奨。

UniTaskを導入することで、次のようなメリットがあります。

  • Unity向けに最適化されたTaskおよびTaskSchedulerが利用できる
    • ValueTaskのUnity向け実装
    • UnityのPlayerLoopを用いたSchedule管理
    • SynchronizationContextに依存しない
async void Start()
{
    var path = "data.txt";

    // UniTaskのawaitは基本的にメインスレッドに戻る
    var result = await LoadFileAsync(path);

    Debug.Log(result);
}

private UniTask<string> LoadFileAsync(string path)
{
    return UniTask.Run(() => File.ReadAllText(path));
}
  • さまざまなオブジェクトへのAwaiter実装の提供
    • コルーチンをasync/awaitに置き換える可能に
    • yield returnの代わりにawaitで非同期処理の待機ができるようになる
private void Start()
{
    JumpAsync(this.GetCancellationTokenOnDestroy()).Forget();
}

/// <summary>
/// ボタンが押されたらジャンプする
/// </summary>
private async UniTaskVoid JumpAsync(CancellationToken token)
{
    var rigidBody = GetComponent<Rigidbody>();

    while (!token.IsCancellationRequested)
    {
        // ボタンが押されるのをUpdateタイミングで待つ
        await UniTask.WaitUntil(() => Input.GetButtonDown("Jump"),
            PlayerLoopTiming.Update,
            cancellationToken: token);

        // 押されたらFixedUpdateのタイミングでジャンプする
        await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
        rigidBody.AddForce(Vector3.up, ForceMode.Impulse);

        // 1秒待って繰り返す
        await UniTask.Delay(1000, cancellationToken: token);
    }
}

UniTaskは「非同期処理」に特化したライブラリです。
導入することでほぼすべてのコルーチンをasync/awaitに置換することができます。

標準Taskとの比較

UniTaskC# 7.0で追加された、ValueTaskライクに作られています。
そのためasync/await時、処理が同期で完了する場合においてはヒープアロケーションが発生しないという特徴があります。また、UniTaskは実行コンテキストの管理にSynchronizationContextではなくUnity Player Loopを用いるように作られています。

そのため、標準のTask/ValueTaskと比較してUniTaskの方がよりUnityではパフォーマンスを出すことができるようになっています。

Task UniTask
機能 Unityでは不要な機能が多い Unityで活用できる機能のみ実装
オブジェクトサイズ 大きい 小さい
実行コンテキストの管理 TaskScheduler & SynchronizationContext UnityのPlayerLoop
必要なC# version C# 5.0以上 C# 7.0以上
TaskTracker UnityEditor上で確認可能
メモリアロケーション 常にヒープを確保する 同期処理で完了する場合はヒープを確保しない

UniRxとUniTask、それぞれの使い分け

UniTaskUniRxの非同期処理の使い分けの基準は何か」という疑問を持つ人もいるでしょう。
それぞれの使い分けについて説明します。

UniTaskを使うべき場合

非同期処理の結果通知が「1回」で済む場合

結果通知が1回である、つまりは単発で完了する普通の非同期処理の場合です。
こちらはUniTask(とasync/await)を使うべきです。

理由として次が挙げられます。

  • 同期処理と見た目がほぼ変わらないコードを書ける
  • 単発で済む非同期処理に対して、Observable自体がオーバースペック気味

async/awaitを使うと非同期処理をほぼ同期処理と変わらない見た目で記述することができます。
そのため同期処理で書いていた部分をあとから非同期化する、またはその逆といった対応も比較的楽にすみます。

Observableの場合、処理が1回で済む非同期処理に対してはかなりオーバースペックになってしまいます。まず「メッセージの発行元は何か」「メッセージは何回発行されるのか」を常に意識しなくてはいけません。
さらにはHotやColdといった性質も考えなくてはならず、UniTaskほど気軽に扱えません。

また、Observableを用いた非同期処理は同期処理とまったく異なる記述法になってしまいます。
そのため、あとから処理内容を同期処理へ直そうと思ったときに、Observableを使っている場所ほぼすべてを新しく書き直す必要が出てきてしまいます。

処理を手続き的に記述したい場合

Observableでは非同期処理をOperatorを用いて処理を宣言的に記述することができました。
これは処理の内容がOperatorの表現範囲で済む限りにおいては便利ではあります。
ですが実際の開発においてはそう簡単に行きません。Operatorではどうしても表現しきれない処理が出てきた場合、これを手続き的に書き下す必要があります。

また、Observableは「処理の条件分岐ができない」という欠点があります。
メッセージ内容に応じてその場で実行する処理をごっそり切り替えるといったことができません。
(処理内容ごとに新しいObservableを定義しなくてはいけないため無駄が多い)

UniTask(とasync/await)であれば、処理をほぼ同期処理と変わらずに手続き的に記述することができます。
そのため、要求どおりの仕様を満たした処理をObservableOperatorで記述するよりも、よりわかりやすい簡単に実装することができます。

UniRx(Observable)を使うべき場合

非同期処理の結果が複数個になる場合

ここでいう複数個の結果とは、UniTask<IEnumerable<T>>で済むような場合の話ではありません。非同期処理そのものが継続的に実行され、結果が断続的に発行されるようなシチュエーションを想定しています。
(たとえばディレクトリの中身をまとめて読み込んで、読み込みが先に終わったファイルから処理を次々に行うなど)

このような非同期処理はUniTaskでは表現が不可能なため、IObservable<T>を使うことになります。

補足 IAsyncEnumerable<T>IObservable<T>の違い

C# 8.0IAsyncEnumerable<T>というものが追加されました。名前のとおり「非同期ストリーム」です。IObservable<T>と役割が被ってそうですが、そこは明確に異なります。

  • IObservable<T> : 非同期処理が常に動いておりその結果がPUSHで通知される
  • IAsyncEnumerable<T> : 結果をPULLしたときに1つ非同期処理が走る

非同期処理を裏で動かしてその結果がPUSH通知されるのを待つならIObservable<T>を使う。
await foreachと組み合わせて、逐次処理をしていくならIAsyncEnumerable<T>を使う。

といった使い分けが今後は必要になってくるでしょう。
(まだUnityはC# 8.0に対応していないので、もう少しあとの話ですが)

イベント処理を行う場合

C#event構文や、UnityEventの代替としてUniRxは利用することができます。
またUpdate()FixedUpdate()Observableに変換したり、uGUIと組み合わせて使うと表現力が広がりかなり便利に使うことができます。

(そもそも「イベント処理」=「値が複数回発行される非同期処理」とほぼ同義なので、同じことを2回説明しているだけですが)

Unityで動くC#バージョンが低い場合

古のUnityでは対応しているC#バージョンが低く、async/awaitすら使えない場合があります。
特に長期運用しているプロジェクトですと未だにUnity 5系を使っているなんてこともあるでしょう。

この場合はUniTaskを用いることはできないので、UniRxが非同期処理における唯一の選択肢となります。
コルーチンとObservableを併用するなどするとよいでしょう。

相互変換

なお、ObservableUniTaskはそれぞれ相互変換が可能です。
そのためどちらか片方にこだわる必要はなく、場面に応じて変換して使い分けるとよいでしょう。

UniTask -> UniRx

キャンセルを考えない場合

UniTask.ToObservable()で変換できます。
この場合、Schedulerは自動的にMainThreadScheduler指定となります。

using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;

public class UniTaskToUniRx : MonoBehaviour
{
    void Start()
    {
        // UniTask<string>
        var uniTask = LoadTextAsync("https://github.com/");

        // UniTask<string> -> IObservable<string>
        // UniTaskのキャンセルを考えないならこれだけでOK
        uniTask.ToObservable().Subscribe(Debug.Log);
    }

    /// <summary>
    /// Textを指定のパスから読み込む
    /// </summary>
    private async UniTask<string> LoadTextAsync(string path)
    {
        var uwr = UnityWebRequest.Get(path);
        await uwr.SendWebRequest();
        return uwr.downloadHandler.text;
    }
}

キャンセルを考える場合

一発で変換するメソッドはありません。
そのため次のような方法でがんばるしかないです。

UniRx -> UniTask

IObservable -> UniTask

ToUniTask()を使うことでUniTaskに変換できます。
ただしデフォルトではOnCompletedメッセージが発行されるまで待ち受けてしまいます。
次に発行される1メッセージだけを待ち受けたい場合は、useFirstValueオプションをつけましょう。

private async UniTaskVoid CheckEnemyAsync(CancellationToken cancellationToken)
{
    // 新たに生成されたEnemyが通知されるObservableがあったとして
    IObservable<Enemy> enemyObservable = _enemySpawner.OnEnemySpawned;

    while (!cancellationToken.IsCancellationRequested)
    {
        // ToUniTask(useFirstValue: true) でUniTask化してawaitできる
        var enemy = await enemyObservable
            .ToUniTask(cancellationToken, useFirstValue: true);

        Debug.Log(enemy.Name);
    }
}

IReadonlyReactivePropertyのawait

正確にいえばUniTask変換ではないですが、どうせasync/awaitとセットで使うので解説します。
IReadonlyReactiveProperty<T>awaitすることで、次のメッセージ発行を待ち受けることができます。

using System.Threading;
using UniRx;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class ReactivePropertyAwait : MonoBehaviour
{
    // なにかのステート
    private enum GameState
    {
        Ready,
        Battle,
        Result
    }

    // ステート管理するReactiveProperty
    private ReactiveProperty<GameState> _currentGameState
        = new ReactiveProperty<GameState>();

    private void Start()
    {
        StateChangedAsync(this.GetCancellationTokenOnDestroy()).Forget();
    }

    /// <summary>
    /// ステート遷移するたびに処理を走らせる
    /// </summary>
    private async UniTaskVoid StateChangedAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            // ステート遷移を待つ
            var next = await _currentGameState;

            // 遷移先に合わせて処理をする
            switch (next)
            {
                case GameState.Ready:
                    // Do something...
                    break;
                case GameState.Battle:
                    // Do something...
                    break;
                case GameState.Result:
                    // Do something...
                    break;
            }
        }
    }
}

その他、変わった変換の例

その他の変わった変換パターンについては別記事にまとめました。

まとめ

UniRxUniTaskはUnityでC#を触るならぜひとも導入してほしいライブラリです。
入れて絶対に損はしない、むしろ無いと困るくらいには便利なものなので使い方を覚えて活用できるようになるとよいでしょう。

また、現在UniRxとUniTaskの書籍の執筆を行っています。全体の執筆が概ね終わり、現在はレビューフェーズに入っています。発売されたらぜひよろしくおねがいします。

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

【Unity】LateUpdate()やOnGUI()よりもタイミングが遅い、OnEndOfFrame()を作ってみた

問題

フレーム内のなるべく遅いタイミングで、何か処理をしたい。普通なら、 LateUpdate() で十分だが、諸々の事情により OnGUI() よりも遅いタイミングが必要になった。

そこで実行順に関する公式ドキュメント(Order of Execution for Event Functions)を確認したところ良さげなタイミングを見つけたので、 WaitForEndOfFrame() を利用して強引に作ってみた。

endofframetest.cs
public class endofframetest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(StartEndOfFrame());
        Debug.Log($"Start():{Time.frameCount}");
    }
    IEnumerator StartEndOfFrame()
    {
        while (true)
        {
            // Wait until all rendering + UI is done.
            yield return new WaitForEndOfFrame();
            OnEndOfFrame();
        }
    }

    private void FixedUpdate()
    {
        Debug.Log($"FixedUpdate():{Time.frameCount}");
    }
    void Update()
    {
        Debug.Log($"Update():{Time.frameCount}");
    }
    private void LateUpdate()
    {
        Debug.Log($"LateUpdate():{Time.frameCount}");
    }
    private void OnGUI()
    {
        Debug.Log($"OnGUI():{Time.frameCount}");
    }
    private void OnEndOfFrame()
    {
        Debug.Log($"OnEndOfFrame():{Time.frameCount}");
    }
}
Console.
Start():1

FixedUpdate():1

Update():1

LateUpdate():1

FixedUpdate():2

Update():2

LateUpdate():2

OnGUI():2

OnGUI():2

OnEndOfFrame():2

結果

Start()のタイミングを1フレーム目としたとして2フレーム目からしか呼ばれないので、その点は注意する必要があるが概ね問題なく利用できた。

ちなみに、、、OnGUI()も2フレーム目からしか呼ばれないようだ(確認したのはUnity2020.1α)。

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

Unityコルーチンについて学ぶ

概要

コルーチンとは

使い所

参考

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

2.5Dキャラクターアニメーション - Mirror Animation Playable

これはUnity Advent Calender 2019の24日目の記事です。

3Dキャラクターをミラー処理するPlayableを作りたい

2D格闘ゲームや横スクロールアクションゲームでキャラクターや背景を3Dで表現した、いわゆる2.5Dゲームにおいて、キャラクターをミラーリング(左右反転)処理したいケースがあります。

なぜミラーリングが必要なのか?それはキャラクターは常に画面側を向いててほしいからです。
普通の3Dキャラクターでは右向きで画面に向くよう調整しても、左向きだと背中向けることになってしまい、こちら側を見てくれません。
image.png
ミラーリング処理をすることで、左向きでも右向きでも常にこちら向いてくれるようになるわけです。

UnityのMecanimには既にHumanoidに限りMirror機能がありますが、それを使わずにPlayables APIを使って自前で実装します。

結果

mirroringplayable.gif

左右反転した動きをする3Dキャラクターを表示できました。
が、、よく見ると顔パーツの反転ができてません。
顔の中身をテクスチャで描いているためです。こちらの問題に対応するにXスケールを反転する(ポリゴンが反転するため、シェーダー側の対応も必要)するか、左右反転したテクスチャを用意しておいて、それに切り替えるかで対応する必要があります。
目をボーンで制御している昨今の3Dキャラクターであれば得に問題ないでしょう。

そもそも完全な左右対称の結果を得るにはシンメトリー(左右対称)なキャラクターに限定している話でもあります。
アシンメトリー(左右非対称)なキャラクターの場合、ある程度なら反転結果の差異を許容してもよいですが、場合によっては難しいケース(腕の長さが違う等)もあります。
今記事ではそこまでは触れないでおきます。

今回作成したサンプルプロジェクトをGithubで公開してます。合わせてどうぞ。
https://github.com/you-ri/MirrorAnimationPlayable

なぜMecanim(AnimatorController)のMirrorを使わず自前で実装するのか?

Mecanimには既にMirror機能があります。なのになぜ自前で用意するのでしょうか?

自身のプロジェクトは当初普通にAnimatorControllerを使っていましたが、2018.2あたりでAnimatorControllerのPlayable版、AnimatorControllerPlayableに移行しました。
ところがUnity2019.2にバージョンアップした際、キャラクターのアニメーションが動かなくなるバグを踏んでしまいました。
AnimatorControllerOverrideを使ってランタイム中に書き換えるアプローチで大量のAnimationClipを処理していたのですが、これが機能しなくなったのです。

当然バグ報告しましたが、音沙汰なし。。。これは、、、仕様ということか?
しびれを切らして、自前で大量のAnimationClipを捌き、ミラーを再現する機能をPlayables APIを使って開発する必要がでてきたというわけです。

HumanoidのミラーをAnimationScriptPlayabeで再現する

まずググったところ以下のスレッドがヒットしました。

Playables API - Mirroring Clips and DirectorUpdateMode.Manual

残念なのことにUnityの中の人も途中で投げだしてしまって解決には至っていません。
Muscleの情報から顔や体のロール(RollLeftRight)とヨー(LeftRight)を反転、さらに左右の手、足、指、目などは交換するとで、ミラーした結果が得られるのは理解できるのですが。なぜうまく行かないのか。。。

少し悩んだ末、ルートの回転ミラー計算を見直したところ、左右反転後の姿勢を作り出せることに成功しました。
ついでにIKのミラーにも対応しておきました。

        public static void MirrorPose (this AnimationHumanStream humanStream)
        {
            humanStream.bodyLocalPosition = Mirrored (humanStream.bodyLocalPosition);
            humanStream.bodyLocalRotation = Mirrored (humanStream.bodyLocalRotation);


            // mirror body
            for (int i = 0; i < (int)BodyDof.LastBodyDof; i++) {
                humanStream.MultMuscle (new MuscleHandle ((BodyDof)i), BodyDoFMirror[i]);
            }

            // mirror head
            for (int i = 0; i < (int)HeadDof.LastHeadDof; i++) {
                humanStream.MultMuscle (new MuscleHandle ((HeadDof)i), HeadDoFMirror[i]);
            }

            // swap arms
            for (int i = 0; i < (int)ArmDof.LastArmDof; i++) {
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftArm, (ArmDof)i),
                    new MuscleHandle (HumanPartDof.RightArm, (ArmDof)i));
            }

            // swap legs
            for (int i = 0; i < (int)LegDof.LastLegDof; i++) {
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftLeg, (LegDof)i),
                    new MuscleHandle (HumanPartDof.RightLeg, (LegDof)i));
            }

            // swap fingers
            for (int i = 0; i < (int)FingerDof.LastFingerDof; i++) {
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftThumb, (FingerDof)i),
                    new MuscleHandle (HumanPartDof.RightThumb, (FingerDof)i));
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftIndex, (FingerDof)i),
                    new MuscleHandle (HumanPartDof.RightIndex, (FingerDof)i));
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftMiddle, (FingerDof)i),
                    new MuscleHandle (HumanPartDof.RightMiddle, (FingerDof)i));
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftRing, (FingerDof)i),
                    new MuscleHandle (HumanPartDof.RightRing, (FingerDof)i));
                humanStream.SwapMuscles (
                    new MuscleHandle (HumanPartDof.LeftLittle, (FingerDof)i),
                    new MuscleHandle (HumanPartDof.RightLittle, (FingerDof)i));
            }


            // swap ik
            Vector3[] goalPositions = new Vector3[4];
            Quaternion[] goalRotations = new Quaternion[4];
            float[] goalWeightPositons = new float[4];
            float[] goalWeightRotations = new float[4];
            Vector3[] hintPositions = new Vector3[4];
            float[] hintWeightPositions = new float[4];
            for (int i = 0; i < 4; i++) {
                goalPositions[i] = humanStream.GetGoalLocalPosition (AvatarIKGoal.LeftFoot + i);
                goalRotations[i] = humanStream.GetGoalLocalRotation (AvatarIKGoal.LeftFoot + i);
                goalWeightPositons[i] = humanStream.GetGoalWeightPosition (AvatarIKGoal.LeftFoot + i);
                goalWeightRotations[i] = humanStream.GetGoalWeightRotation (AvatarIKGoal.LeftFoot + i);
                hintPositions[i] = humanStream.GetHintPosition (AvatarIKHint.LeftKnee + i);
                hintWeightPositions[i] = humanStream.GetHintWeightPosition (AvatarIKHint.LeftKnee + i);
            }
            for (int i = 0; i < 4; i++) {
                int j = (i + 1) % 2 + (i / 2) * 2;                  // make [1, 0, 3, 2]
                humanStream.SetGoalLocalPosition (AvatarIKGoal.LeftFoot + i, Mirrored(goalPositions[j]));
                humanStream.SetGoalLocalRotation (AvatarIKGoal.LeftFoot + i, Mirrored(goalRotations[j]));
                humanStream.SetGoalWeightPosition (AvatarIKGoal.LeftFoot + i, goalWeightPositons[j]);
                humanStream.SetGoalWeightRotation (AvatarIKGoal.LeftFoot + i, goalWeightRotations[j]);
                humanStream.SetHintPosition (AvatarIKHint.LeftKnee + i, hintPositions[j]);
                humanStream.SetHintWeightPosition (AvatarIKHint.LeftKnee + i, hintWeightPositions[j]);
            }
        }

        public static Vector3 Mirrored (Vector3 value)
        {
            return new Vector3 (-value.x, value.y, value.z);
        }

        public static Quaternion Mirrored (Quaternion value)
        {
            return Quaternion.Euler (value.eulerAngles.x, -value.eulerAngles.y, -value.eulerAngles.z);
        }

しかし、これだけでは不完全です。武器もミラーリングしないと実際のゲームでは使えません。

武器のミラーに対応する

Humanoidのミラー対応できました。次は武器、追加ボーンのミラー対応をします。

武器のミラーリングは複製した武器をもう片方の手にもたせて、表示、非表示で表現する方法が最も簡単ですが、武器オブジェクトが複製されているので、更新も2重に適応したりする必要があったり扱いが煩雑になりそうです。

今回はミラーリング時には片方のボーンに固定することで擬似的に親子関係が変わったように表現します。
武器の親ボーンをミラーリングした後に、武器をそのボーン下のトランスフォームに固定します。

ヒエラルキーのセットアップ

開発中のゲームのモデルを例に説明したいと思います。
まずミラーリング対応するためのヒエラルキーをセットアップします。

image.png

アニメーション制作時に向く方向は統一しておきます。今回weapon_Rにアニメーション情報が入っており、常にAnimationClipによって更新されます。
それの反転姿勢を保持するためのweapon_Lを作り、その子にミラー後の表示位置を確認するための武器としてjanis_weapon_mirrorをアタッチし、に左右対称になるように調整しておきます。
janis_weapon_mirrorはミラー後の武器のアタリとして用意するだけで実際は表示しません。無効化しておきます。

追加トランスフォーム分をミラー処理する

武器の親トランスフォームをミラーリングしないといけません。
image.png
weapon_Rのミラーリング後した姿勢をweapon_Lへ反映します。

Humanoidのミラーリング処理する前に武器ボーンをミラーリングします。
ミラーリングしたいボーンをキャラクターの座標系(ルート座標系)に変換した後、XY平面でミラー化してその結果を片方のボーンに設定します。

    public struct MirroringPlayableJob : IAnimationJob, IDisposable
    {

        public struct MirroringConstrant
        {
            public TransformStreamHandle driven;
            public TransformStreamHandle source;
        }

        public struct MirroringPosture
        {
            public TransformStreamHandle source;
            public TransformStreamHandle driven;
        }

        public bool debug;
        public bool isMirror;

        public TransformStreamHandle root;

        public NativeArray<MirroringPosture> mirroringTransforms;
        public NativeArray<MirroringConstrant> mirroringConstrants;

        public void ProcessRootMotion (AnimationStream stream) { }

        public void ProcessAnimation (AnimationStream stream)
        {
            Vector3 rootPosition;
            Quaternion rootRotation;
            root.GetGlobalTR (stream, out rootPosition, out rootRotation);
            var rootTx = new AffineTransform (rootPosition, rootRotation);

            var mirroredTransforms = new NativeArray<AffineTransform> (mirroringTransforms.Length, Allocator.Temp);

            // 追加トランスフォームのミラーリング計算
            if (isMirror) {
                for (int i = 0; i < mirroringTransforms.Length; i++) {

                    if (!mirroringTransforms[i].source.IsValid (stream)) continue;
                    if (!mirroringTransforms[i].driven.IsValid (stream)) continue;

                    Vector3 position;
                    Quaternion rotation;
                    mirroringTransforms[i].source.GetGlobalTR (stream, out position, out rotation);

                    var drivenTx = new AffineTransform (position, rotation);
                    drivenTx = rootTx.Inverse() * drivenTx;
                    drivenTx = AnimationStreamMirrorExtensions.Mirrored (drivenTx);
                    drivenTx = rootTx * drivenTx;
                    mirroredTransforms[i] = drivenTx;
                }
            }


            // Humanoid ミラーリング
            ~~ 省略 ~~

            // 追加トランスフォームのミラーリング適用
            if (isMirror) {
                for (int i = 0; i < mirroringTransforms.Length; i++) {

                    if (!mirroringTransforms[i].source.IsValid (stream)) continue;
                    if (!mirroringTransforms[i].driven.IsValid (stream)) continue;

                    mirroringTransforms[i].driven.SetGlobalTR (stream, mirroredTransforms[i].position, mirroredTransforms[i].rotation, false);
                }
            }

            // 追加トランスフォームのミラーリング拘束
            ~~ 省略 ~~
        }
    }

ミラー時にペアレントを切り替える

ミラー後のトランスフォームに武器を拘束して親子関係を切り替えたように見せます。
image.png
やり方は単純です。
この段階では janis_weapon_mirror の親トランスフォームのweapon_Lにはミラー後の姿勢になっているのでjanis_weapon_mirrorの姿勢をjanis_weaponにコピーするだけで、あたかも親トランスフォームがweapon_Lに切り替わったかのように見せることができます。

        public void ProcessAnimation (AnimationStream stream)
        {
            // Humanoid のミラーリング
            ~~ 省略 ~~

            // 追加トランスフォームのミラーリングの適用
            ~~ 省略 ~~

            // ミラーリングしたトランスフォームへのコンストレント
            if (isMirror) {
                for (int i = 0; i < mirroringConstrants.Length; i++) {

                    if (!mirroringConstrants[i].source.IsValid (stream)) continue;
                    if (!mirroringConstrants[i].driven.IsValid (stream)) continue;

                    Vector3 position;
                    Quaternion rotation;
                    mirroringConstrants[i].source.GetGlobalTR (stream, out position, out rotation);
                    mirroringConstrants[i].driven.SetGlobalTR (stream, position, rotation, false);
                }
            }
        }

あとはこれらのコードをPlayableGraphに登録するためのコンポーネントを作成すればおkです。
image.png
それの詳細についてはここでは言及しません。サンプルプロジェクトの
CustomAnimation.csを参照ください。

まとめ

これで3Dキャラクターを使って2Dゲーム的な表現ができるようになった!といいたいところですが、画角があるため立ち位置によって見え方が変化してしまう問題が残っています。
image.png
これを解決するにはキャラクターやそれに付随するエフェクトなどは画角を抑えて描画する必要があります。
それはまた別の機会があれば記事にしたいと思います。

3Dキャラクターを使うことで開発中もデザインを後から修正することが楽になりますし、ランタイムで入れ替えることも可能になります。
3Dくささを打ち消すのが大変だったりしますが、魅力的なキャラクターが活躍する2.5Dゲームが増えればいいな~と思っています。

コツコツとゲーム作ってます。
対戦格闘アクション http://extrival.com

参考

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

Unity内で取得したデータをcsvに書き出す方法

1. はじめに

Unityのゲーム内で取得したデータをcsvに書き出す方法についての記事を書きます。

具体的には、2つのGameObjectの操作記録をcsvに時系列順で書き出すスクリプトを作成します。

2. 準備

2.1. 用意するGameObject

用意するGameObjectは3つです。

  • F … 赤色のGameObject
  • J … 水色のGameObject
  • SaveCsv … EmptyGameObject

aaaaa.PNG

2.2. 用意するスクリプト

用意するはスクリプトも3つです。

  • SampleFScript … F用のスクリプト
  • SampleJScript … J用のスクリプト
  • SampleSaveCsvScript … SaveCsv用のスクリプト

j.PNG

3. スクリプトに記述

3.1. SampleSaveCsvScripのコードの解説

まずは、csvに保存するためのコードをSampleSaveCsvScripに記述します。

3.1.1. void Start()

引用元サイト①で詳しく解説されています。
ざっくりとした説明をすると、新しくcsvファイルを作成して、{}の中の要素分csvに追記をするコードです。

3.1.2. public void SaveData(string txt1, string txt2, string txt3)

「s1」で記述したヘッダーの数分「string txt」を用意してください。
「public」をつけることで他のスクリプトでも「SaveData(~)」が使用できるようになります。
その他については、上記と同じです。

3.1.2. void Update()

Enterキーが押されたらcsvへの書き込みを終了するコードを記述しています。

SampleSaveCsvScrip
using System.IO;
using System.Text;

public class SampleSaveCsvScript : MonoBehaviour
{
    private StreamWriter sw;

    void Start()
    {
        sw = new StreamWriter(@"SaveData.csv", true, Encoding.GetEncoding("Shift_JIS"));
        string[] s1 = { "F", "J", "time" };
        string s2 = string.Join(",", s1);
        sw.WriteLine(s2);
    }

    public void SaveData(string txt1, string txt2, string txt3)
    {
        string[] s1 = { txt1, txt2, txt3 };
        string s2 = string.Join(",", s1);
        sw.WriteLine(s2);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            sw.Close();
        }

    }
}

3.2. SampleFScriptのコードの解説

SampleFScriptには、Fキーを検出するコードを記述します。

3.2.1. void Start()

引用元サイト②で詳しく解説されています。
ざっくりとした説明をすると、他のスクリプトを参照するコードを記述しています。

3.2.2. void Update()

Fキーが押されたら、csvに「F」と「いつ押されたか」という情報が書き加えられます。

SampleFScrip
using UnityEngine;
using System.IO;

public class SampleFScript : MonoBehaviour
{
    private float time;
    private StreamWriter sw;

    GameObject SaveCsv;
    SampleSaveCsvScript SampleSaveCsvScript;

    void Start()
    {
        SaveCsv = GameObject.Find("SaveCsv");
        SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>();
    }

    void Update()
    {
        time += Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.F))
        {
            SampleSaveCsvScript.SaveData("F", " ", time.ToString());
        }
    }
}


3.3. SampleJScriptのコードの解説

SampleJScriptには、Jキーを検出するコードを記述します。

3.3.1. void Start()

上記と同じです。

3.2.2. void Update()

Jキーが押されたら、csvに「J」と「いつ押されたか」という情報が書き加えられます。

SampleJScrip
using UnityEngine;
using System.IO;

public class SampleFScript : MonoBehaviour
{
    private float time;
    private StreamWriter sw;

    GameObject SaveCsv;
    SampleSaveCsvScript SampleSaveCsvScript;

    void Start()
    {
        SaveCsv = GameObject.Find("SaveCsv");
        SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>();
    }

    void Update()
    {
        time += Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.J))
        {
            SampleSaveCsvScript.SaveData(" ", "J", time.ToString());
        }
    }
}


4. 確認

実際にゲームを動かして確認をしてみます(ここは省略)。

すると、下記のようなcsvが出てきます。
asd.PNG
「F」や「J」そして「いつ押されたか」という情報がきちんと書き出されていることが確認できると思います。

5. 最後に

Unityのゲーム内で取得したデータをcsvに書き出す方法について書いてみましたが、いかかだったでしょうか。
分かりづらい点や間違っている点があれば、ご指摘いただけると幸いです。

参考URL

① Unity 2D] データを保存する(外部ファイルCSV)
https://high-programmer.com/2017/12/10/unity-savedata-otherfile/

② [Unity]他のオブジェクトについているスクリプトの変数を参照したり関数を実行したりする。
https://qiita.com/tsukasa_wear_parker/items/09d4bcc5af3556b9bb3a

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

GitHub ActionsでUnityのAndroid, iOSビルドをやってみる

この記事は Akatsuki Advent Calendar 2019 23日目の記事です。
前日は @ShaderError さんによる Unityでシェーダー描いてみたい でした。
アカツキ人事がハートドリブンに書く Advent Calendar 2019 もあるのでそちらもぜひ。

はじめに

以前に仕事でCircleCIを使ってUnityのAndroidとiOSビルドを行なっていたのでその事について書こうかなと思っていたのですが、最近GitHub Actionsという新機能がGitHubで公開されました。
これがいい感じにCircleCIでUnityビルドをしていた際に起きていた問題を解決していたので、試験的に試してみたまとめが本記事になります。

環境

GitHub Actions 12/23時点のワークフロー構文
Unity 2019.3.0f1
MacBook Pro (self-hostedで利用するマシン)

GitHub Actionsとは

GitHubによるCI/CDツールです。
詳しいことは GitHub Actions Documentation に書いてありますが、JenkinsCircleCIなどに替わる新しいツールとなるのか(個人的に)気になるサービスです。

GitHub Actions を導入する

前提:UnityProjectのリポジトリがGitHubに存在する状態であること
利用想定:self-hostedを使ってローカルにあるマシンをGitHub Actionsで利用する
前提の状態にするまでの解説は行いませんので、各自作業を行なってください。

GitHubのリポジトリの Setting -> Actions を選択して
Actions permissionsEnable local and third party Actions for this repositoryに変更してください
その後にSelf-hosted runnersAdd runner を選択
スクリーンショット 2019-12-22 22.28.51.png
そうするとrunnerを追加するためのCLIのコマンドが表示されます。
各コマンドを順に実行してください。
実行した後にGitHubのページを再読み込みをするとSelf-hosted runnersにCLIコマンドを実行したPCが表示されていると思います。
これでGitHub Actionsself-hostedが使えるようになりました。
スクリーンショット 2019-12-19 0.19.05.png
その後にGitHubのActionsタブにあるSimple workflowを使って動作テストをしてみます。
runs-onself-hostedにするのを忘れないでください。
変更が完了しコミットをすると、self-hostedbuild.ymlが実行されます。
なお、こちらに記載がありますが、ワークフローファイルは、リポジトリの.github/workflowsディレクトリに保存する必要があるのでご注意ください。
スクリーンショット 2019-12-18 23.31.15.png
実行結果はActionタブから確認できます。
おそらく以下の画像のような結果になっていると思います。
これでローカルにあるself-hostedに追加されたマシンから実行できるようになりました。
ひとまずこれで基本設定は完了になります。
スクリーンショット 2019-12-19 0.38.33.png

Unityのビルドスクリプトを作成する

ここからは本格的にビルド処理を実装していきます。

まずはUnityでビルドスクリプトを書きます。

using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
using System.Collections.Generic;
using UnityEditor.iOS.Xcode;
using UnityEditor.Callbacks;

public class MobileBuild
{
    static string[] GetEnabledScenes()
    {
        return (
                   from scene in EditorBuildSettings.scenes
                   where scene.enabled
                   where !string.IsNullOrEmpty(scene.path)
                   select scene.path
               ).ToArray();
    }

    private static void BuildAndroid()
    {
        // Setting for Android
        EditorPrefs.SetBool("NdkUseEmbedded", true);
        EditorPrefs.SetBool("SdkUseEmbedded", true);
        EditorPrefs.SetBool("JdkUseEmbedded", true);
        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);

        // Build
        bool result = Build(BuildTarget.Android);

        // Exit Editor
        EditorApplication.Exit(result ? 0 : 1);
    }

    private static void BuildIOS()
    {
        // Setting for iOS
        PlayerSettings.SetScriptingBackend(BuildTargetGroup.iOS, ScriptingImplementation.IL2CPP);
        EditorUserBuildSettings.iOSBuildConfigType = iOSBuildType.Debug;

        // Build
        bool result = Build(BuildTarget.iOS);

        // Exit Editor
        EditorApplication.Exit(result ? 0 : 1);
    }

    private static bool Build(BuildTarget buildTarget)
    {
        // Get Env
        string   outputPath   = GetEnvVar("OUTPUT_PATH");               // Output path
        string   bundleId     = GetEnvVar("BUNDLE_ID");                 // Bundle Identifier
        string   productName  = GetEnvVar("PRODUCT_NAME");              // Product Name
        string   companyName  = GetEnvVar("COMPANY_NAME");              // Company Name

        outputPath = AddExpand(buildTarget, outputPath);

        Debug.Log("[MobileBuild] Build OUTPUT_PATH :" + outputPath);
        Debug.Log("[MobileBuild] Build BUILD_SCENES :" + String.Join("", GetEnabledScenes()));

        // Player Settings
        BuildOptions buildOptions;
        buildOptions = BuildOptions.Development | BuildOptions.CompressWithLz4;

        if (!string.IsNullOrEmpty(companyName)) { PlayerSettings.companyName = companyName; }

        if (!string.IsNullOrEmpty(productName)) { PlayerSettings.productName = productName; }

        if (!string.IsNullOrEmpty(bundleId)) { PlayerSettings.applicationIdentifier = bundleId; }

        // Build
        var report = BuildPipeline.BuildPlayer(GetEnabledScenes(), outputPath, buildTarget, buildOptions);
        var summary = report.summary;

        // Build Report
        for (int i = 0; i < report.steps.Length; ++i)
        {
            var step = report.steps[i];
            Debug.Log($"{step.name} Depth:{step.depth} Duration:{step.duration}");

            for (int d = 0; d < step.messages.Length; ++d)
            {
                Debug.Log($"{step.messages[d].content}");
            }
        }

        if (summary.result == BuildResult.Succeeded)
        {
            Debug.Log("<color=white>[MobileBuild] Build Success : " + outputPath + "</color>");
            return true;
        }
        else
        {
            Debug.Assert(false, "[MobileBuild] Build Error : " + report.name);
            return false;
        }
    }

    private static string GetEnvVar(string pKey)
    {
        return Environment.GetEnvironmentVariable(pKey);
    }

    private static string AddExpand(BuildTarget buildTarget, string outputPath)
    {
        switch (buildTarget)
        {
            case BuildTarget.Android :
                outputPath += ".apk";
                break;
        }

        return outputPath;
    }
}

大まかに書くとこのような感じになるかと思います。
ビルド後に実行する処理等は書いていませんので、必要に応じて追加してください。

ローカルで上記のスクリプトが動作するか以下のコマンドをCLIで確認してください。

# 適宜書き換えてください
export COMPANY_NAME=""
export PRODUCT_NAME=""
export BUNDLE_ID=""
export OUTPUT_PATH =""

/Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildIOS -buildTarget iOS

/Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget Android

CLIが実行できたのであればいよいよGitHub Actionsを使った話へ進みます

GitHub Actions で Unity のビルドを行う

Android Build

まずはAndroidのビルドを行います。
Unity2019からAndroidのNDKやJDKがインストール時に追加できるようになりました!
なのでNDKやJDKの設定は各自でお願いします(いい時代になりましたね)

早速build.ymlファイルを書き換えましょう

name: ApplicationBuild

on: [push, pull_request]

env:
  OUTPUT_PATH: ""
  BUNDLE_ID: ""
  PRODUCT_NAME: ""
  COMPANY_NAME: ""
  UNITY_VERSION: 2019.3.0f1
jobs:
  build:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v2
    - name: Android Build
      run: |
        /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
          -projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget Android

Androidビルドをするだけのシンプルな処理です。
環境変数は各自で適切に入力してください。

いくつかを説明を軽くしますと、

name

name:はワークフローの名前になります。
この名前がリポジトリのアクションページにワークフローに表示されます。
スクリーンショット 2019-12-23 14.04.47.png

on

on:はワークフローをトリガーするGitHubイベントの名前です。
イベントの種類は多くありこちらにドキュメントとしてまとめてあります。
今回はpushpull_requestをトリガーにしています。

env

  1. envはワークフローの全てのジョブから利用できる環境変数を定義します。
  2. jobs.<job_id>.env<job_id>ジョブから利用できる環境変数を定義します。
  3. jobs.<job_id>.steps.envはステップから利用できる環境変数を定義します。

先ほどのbuild.ymlは 2. のenvを使っています。
この後のiOSビルドでも使いますからね。

jobs.job_id.steps.uses

ジョブでステップの一部として実行されるアクションを選択します。
actions/checkoutはワークフローで使用できる標準アクションで、v2を指定することでチェックアウトアクションのv2を利用するという設定になります。(標準のアクションはこちらにまとまっています)

jobs.job_id.steps.run

オペレーティングシステムのシェルを使用してコマンドラインプログラムを実行します。
さらにshellキーワードを使用すると、環境のOSのデフォルトシェルを上書きできます。

iOS Build

続いてiOSビルドです。
今回は時間がなくなったのと複雑になるので、ipaファイルをビルドするところまでは行わず、xcodeprojをビルドするところまで行います。
では、build.ymlを書き換えましょう。

name: ApplicationBuild

on: [push, pull_request]

env:
  OUTPUT_PATH: ""
  BUNDLE_ID: ""
  PRODUCT_NAME: ""
  COMPANY_NAME: ""
  UNITY_VERSION: "2019.3.0f1"
jobs:
  android-build:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v2
      with:
        path: android
    - name: Android Build
      run: |
        /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
          -projectPath ./android -executeMethod MobileBuild.BuildAndroid -buildTarget Android
  ios-build:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v2
      with:
        path: ios
    - name: iOS Build
      run: |
        /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
          -projectPath ./ios -executeMethod MobileBuild.BuildIOS -buildTarget iOS

書き方が変わった箇所がありますね。
actions/checkoutwith:キーワードを使ってpath:の設定を渡しています。
こうしている理由は、AndroidとiOSで利用するフォルダを分けてインポート時間やビルド時間の高速化するためです。

この状態でpushしてみましょう。
self-hostedに登録したサーバーが動作してビルドが実行されるはずです。
AndroidとiOSのビルドが成功すると以下のような画面になります。
スクリーンショット 2019-12-23 16.58.55.png

暗号化されたシークレットを利用する

実際に運用しようとなると、ymlに書きたくない情報を渡したいケースがあると思います。
その場合は GitHub のリポジトリの Setting -> Secret ページへいき Add a new secret をクリック。
シークレットとは、暗号化された環境変数のことです。
NameValueをそれぞれ入力します。
スクリーンショット 2019-12-23 17.10.23.png
ここで設定した値をワークフローで利用するには

env:
  SOME_PARAM: ${{ secrets.SOME_PARAM }}

のように記述することで可能です。
これでpasswordTOKENの情報を渡すことができますね!
なお、シークレットの制限として
1. ワークフローで最大100のシークレットを持てる
2. シークレットの容量は最大64KB
の2点があります。
参考: 暗号化されたシークレットの作成と利用

終わりに

個人的にとてもいいツールだと感じました。
具体的には以下の点に可能性があると思います
1. マシンスペックをこちらで自由にカスタマイズできる(self-hostedの場合)
2. CI/CDがGitHubで完結する
3. Jenkinsから解放される(アセットバンドルをどうするのかという問題はありますが・・・)

他にもCircleCI(MacOS)と違ってUnityのインストールを毎回行う必要がなかったりと比較すると優れている点が多いなという印象です。
簡単な導入まででしたが、以上になります。
参考になれば幸いです。

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

UniRx オペレータ一覧

はじめに

以前、UniRx オペレータ逆引きという記事は書いたのですが、正引きがなかったので改めてまとめました。

フィルタ系

名前 効果・用途 備考
Where<T> 1:OnNextメッセージの内容を条件式でフィルタリングする 後ろがSelectの場合は最適化が自動的に走る
2:OnNextメッセージの内容および発行回数でフィルタリングする
OfType<T> 指定した型にキャスト可能なメッセージのみ通す Castとの違いは、変換失敗時に”何もしない”こと
IgnoreElements<T> すべてのOnNextメッセージをフィルタリングする 何も通さない
Distinct<T> 過去に入力された同値のメッセージをフィルタリングする Func<TSource, TKey>で比較値の取り出し方法の指定、およびIEqualityComparerの指定が可能
DistinctUntilChanged<T> 直前に入力されたメッセージと同値の場合にフィルタリングする Func<TSource, TKey>で比較値の取り出し方法の指定、およびIEqualityComparerの指定が可能
First<T> 条件を満たした最初の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合はOnError(InvalidOperationException)が発行される。
FirstOrDefault<T> 条件を満たした最初の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合はOnNext(default<T>)が発行される。
Last<T> 条件を満たした最後の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合はOnError(InvalidOperationException)が発行される。
LastOrDefault<T> 条件を満たした最後の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合はOnNext(default<T>)が発行される。
Single<T> 条件を満たすOnNextメッセージは必ず1回入力されるという制約をつける 入力が0回だった、または2回以上条件を満たすOnNextメッセージが入力された場合はOnError(InvalidOperationException)が発行される。
SingleOrDefault<T> 条件を満たすOnNextメッセージは必ず1回入力されるという制約をつける 入力が0回だった、または2回以上条件を満たすOnNextメッセージが入力された場合はOnNext(default<T>)が発行される。
Skip<T> 1:先頭から指定した個数のOnNextメッセージを無視する 引数はint(無視する個数)
2:購読開始から指定した期間メッセージを無視する 引数はTimeSpanおよび使用するScheduler
SkipWhile<T> 条件を満たす間はメッセージを無視する。一度でも条件を満たさなくなるとそれ以降は常に通過させる。
Take<T> 1:先頭から指定した個数のOnNextメッセージのみ通過させ、その直後にOnCompletedメッセージを発行する 引数はint(通過させる個数)
2:購読開始から指定した期間のみメッセージを通過させる。その後はOnCompletedメッセージを発行する 引数はTimeSpanおよび使用するScheduler
TakeWhile<T> 条件を満たす間のみメッセージを通過させる。条件を満たさなくなるとOnCompletedメッセージを発行する
TakeUntil<T> 他のObservableからのOnNextメッセージ入力を受けた瞬間に、OnCompletedメッセージを発行する Observableを外から止めたいときに使える
TakeUntilDestroy<T> 指定したGameObjectおよびComponentが破棄されたタイミングでOnCompletedメッセージを発行する GameObjectなどに連動してObservableを止めたいときに使える
TakeUntilDisable<T> 指定したGameObjectおよびComponentOnDisable()されたタイミングでOnCompletedメッセージを発行する
TakeLast<T> 1:OnCompletedメッセージが入力されたタイミングで、直前のOnNextメッセージを指定の個数分取り出す OnCompletedメッセージが入力されるまで動作しない
2:OnCompletedメッセージが入力されたタイミング、直前のOnNextメッセージを指定の期間分取り出す OnCompletedメッセージが入力されるまで動作しない
Throttle<T> 短期間に大量にメッセージが入力された場合はそれを無視し、落ち着いたタイミングで最後の1つだけを取り出して流す 引数はTimespan(落ち着いたと判断するまでの猶予時間)
ThrottleFrame<T> 短期間に大量にメッセージが入力された場合はそれを無視し、落ち着いたタイミングで最後の1つだけを取り出して流す 引数はフレーム数
ThrottleFirst<T> 1度OnNextメッセージが入力されたらそれを流し、そのあとは一定時間メッセージを無視する 連打の防止などに使える。引数はTimespan
ThrottleFirstFrame<T> 1度OnNextメッセージが入力されたらそれを流し、そのあとは一定時間メッセージを無視する 連打の防止などに使える。引数はフレーム数

メッセージの変換系

名前 効果・用途 備考
Select<T> 1:OnNextメッセージの内容を別の値に変換する 後ろがWhereの場合は最適化が自動的に走る
2:OnNextメッセージおよび発行回数を用いて別の値に変換する
Cast<T> OnNextメッセージを指定した型にキャストする キャスト失敗時にはOnErrorメッセージが発行される
AsUnitObservable OnNextメッセージの型をUnit型に変換する Select(_ => Unit.Default)の省略記法
AsSingleUnitObservable Observableが終了するタイミングでUnit型のOnNextメッセージを1回だけ発行する LastOrDefault().AsUnitObservable()

メッセージやストリームの合成系

名前 効果・用途 備考
Merge<T> 1.複数のストリームの内容を並列に結合する 引数はIObservable<T>[]
2.IObservable<IObservable<T>>IObservable<T>にまとめる IObservable<IObservable<T>>に対する拡張メソッド
3.複数のストリームの内容を並列に結合する IEnumerable<IObservable<T>>に対する拡張メソッド
Concat<T> 1.複数のストリームの内容を直列に結合する 引数はIObservable<T>[]
2.IObservable<IObservable<T>>を先頭から順番に購読する IObservable<IObservable<T>>に対する拡張メソッド
3.複数のIObservable<T>を先頭から順番に購読する IEnumerable<IObservable<TSource>>に対する拡張メソッド
SelectMany<T> 1.OnNextメッセージを使って新しいObservableを生成し、それを並列に合成する flatMapに相当する
2.OnNextメッセージを使って新しいObservableを生成し、そのメッセージを入力値と合成してから、並列に合成する 引数は「Func<T, IObservable<TR>> collectionSelector」「Func<T, TC, TR> resultSelector」の2つ
ContinueWith<T> SelectMany<T>の軽量版。1回しか実行されない代わりに軽量。 1回しかメッセージが発行されない非同期処理に向く
Switch<T> 購読する対象のObservableを次々に切り替える IObservable<IObservable<T>>に対してのみ使える
Aggregate<T> OnNextメッセージに対して加工を行い、その結果を記録してさらに次のメッセージに引き継ぐ Scan<T>との違いは、OnCompletedメッセージが発行されたタイミングで最終結果を出力する
Scan<T> OnNextメッセージに対して加工を行い、その結果を記録してさらに次のメッセージに引き継ぐ Aggregate<T>との違いは、こちらは1回処理するたびにメッセージを発行する
Buffer<T> 1.複数のOnNextメッセージを指定した個数分まとめて、1つのOnNextメッセージに加工する count == skip
2.複数のOnNextメッセージを指定した個数分まとめて、1つのOnNextメッセージに加工したあと、指定個数スキップする countskipを別々に設定できる。Buffer(count:2, skip:1)にすると直前のメッセージとセットにしてメッセージを発行できる。
3.指定の時間間隔でメッセージをまとめる
4.指定の時間間隔でメッセージをまとめたあと、指定した時間計測を一時中断する
5.個数指定と時間指定を両方組み合わせてメッセージをまとめる 時間か個数どちらかの条件を満たしたタイミングで出力される
6.メッセージをまとめ、外部からメッセージ入力されたタイミングで出力する 引数はIObservable<TWindowBoundary>TWindowBoundaryの型はなんでもよい
BatchFrame<T> 1.指定したフレーム数の間に発行されたOnNextメッセージを1つにまとめる フレーム数と、FrameCountTypeを指定できる
2.メッセージのタイミングを指定のFrameCountTypeのタイミング変換する IObservable<Unit>の場合
PairWise<T> 1. 1個前のメッセージと最新のメッセージをセットにして出力する Buffer(count:2, skip:1)とだいたいおなじ挙動
2. 1個前のメッセージと最新のメッセージをセットにして、それをさらに加工して出力する Buffer(count:2, skip:1).Select()に似ている
Zip<TLeft,TRight,TResult> 1. 型が異なる2つのストリームを購読し、値がそれぞれセットで揃ったタイミングで加工して出力する IObservable<TLeft>に対してIObservable<TRight>を合成し、IObservable<TResult>になる
Zip<T> 2. 型が同じストリームを購読し、値がそれぞれセットで揃ったタイミングで加工して出力する。最大7ストリームまで合成できる IObservable<T>に対する拡張メソッド
Zip<T> 3. 型が同じ複数のObservableをまとめ、IList<T>として出力する IEnumerable<IObservable<T>>に定義された拡張メソッド」または「Observable.Zip」から利用できる。合成できるストリーム数に上限はない
ZipLatest<TLeft,TRight,TResult> 1. 型が異なる2つのストリームを購読し、値がそれぞれセットで揃ったタイミングで最新値のみを取り出して加工して出力する Zipとの違いは、こちらは余剰に入力されたメッセージは破棄される
ZipLatest<T> 2. 型が同じストリームを購読し、値がそれぞれセットで揃ったタイミングで最新値のみを取り出して加工して出力する。最大7ストリームまで合成できる Zipとの違いは、こちらは余剰に入力されたメッセージは破棄される
CombineLatest<TLeft,TRight,TResult> 過去に入力されたメッセージ値を記憶し、それを流用して合成を強制的に行う Zipとの違いは、値がセットにならなくてもメッセージを無理やり合成する
WithLatestFrom<TLeft,TRight,TResult> 1つのストリームを主軸とし、そこに別のストリームの最新値を合成する 別のストリームのメッセージを、もう片方のストリームのタイミングで読み取る、といったことができる。例:Update()で発行されたメッセージをFixedUpdate()で読み取る
Observable.WhenAll<T> 複数のObservableがすべて終了状態になるのを待ち、完了時にその結果をまとめて通知する staticメソッド
Amb<T> 複数のストリームのうち、もっとも早くメッセージが到達したストリームをひとつだけ採択する

ストリームそのものを加工する

名前 効果・用途 備考
Delay<T> 指定した時間分、OnNextとOnCompletedメッセージを遅延させる OnErrorは素通しする
DelayFrame<T> 指定したフレーム分、OnNextとOnCompletedメッセージを遅延させる OnErrorは素通しする
DefaultIfEmpty<T> 1回もOnNextメッセージが入力されずにOnCompletedメッセージが入力された場合に、デフォルト値を発行する
StartWith<T> 1. 購読された瞬間に指定の値のOnNextメッセージを一番最初に発行する
StartWith<T> 2. 購読された瞬間に指定の関数を実行しその結果をOnNextメッセージとして一番最初に発行する 登録した関数は購読された瞬間に実行される
StartWith<T> 3. 購読された瞬間に指定された複数の値を一番最初に発行する 指定された値はそれぞれ個別のOnNextメッセージとして発行される
GroupBy<TSource, TKey> 1. OnNextメッセージを指定した条件でグループ分けを行い、それぞれに分割したストリームとして出力する 戻り値がIObservable<IGroupedObservable<TKey, TSource>>型になる
GroupBy<TSource, TKey, TElement> 2. OnNextメッセージを指定した条件でグループ分けを行い、それぞれに分割したストリームにした上でそれを変換してから出力する 戻り値がIObservable<IGroupedObservable<TKey, TElement>>型になる
Repeat<T> OnCompletedメッセージが入力されると、自分より上流のストリームに対して再購読を実行する ストリームが完了したときにSubscribe()を再実行してくれる。無限ループに注意
RepeatSafe<T> Repeat<T>とほぼ同じ。ただし、OnCompletedメッセージが連続して入力されると処理を中断する Repeat<T>を安全にしたもの
RepeatUntilDestroy<T> 指定したGameObjectComponentが破棄されるまでの間、Repeat<T>として機能する Repeat<T>を安全にしたもの
RepeatUntilDisable<T> 指定したGameObjectComponentがDisableされるまでの間、Repeat<T>として機能する Repeat<T>を安全にしたもの
Sample<T> 1. OnNextメッセージを指定の時間間隔でサンプリングする 一定時間ごとに、そのときの最新値を出力する
Sample<T> 2. OnNextメッセージを別のストリームの入力タイミングでサンプリングする 別ストリームから入力があるたびに、そのときの最新値を出力する
SampleFrame<T> OnNextメッセージを指定のフレーム間隔でサンプリングする
Timeout<T> 1. OnNextメッセージの発行間隔が一定時間あいたらOnErrorメッセージを発行する
Timeout<T> 2. 指定の時刻までにストリームが完了しなかったらOnErrorメッセージを発行する
TimeoutFrame<T> OnNextメッセージの発行間隔が一定フレーム数あいたらOnErrorメッセージを発行する
Timestamp<T> OnNextメッセージにそのメッセージが発行された時刻を付与する 戻り値はIObservable<Timestamped<T>>
TimeInterval<T> OnNextメッセージが発行された時間間隔を計測し、それをメッセージに付与する 戻り値はIObservable<TimeInterval<T>>
FrameTimeInterval<T> OnNextメッセージが発行された時間間隔を計測し、それをメッセージに付与する。ただし時間の計測にUnityのTime.timeまたはTime.unscaledTimeを用いる 時間の計測方法が違う以外はTimeInterval<T>と同じ
FrameInterval<T> 直前のOnNextメッセージとの経過フレーム数をOnNextメッセージに付け加える
Synchronize<T> メッセージ処理に排他ロックを行う lockに用いるオブジェクトを複数ストリームで共有することもできる

エラーハンドリング

名前 効果・用途 備考
Catch<T, TException> 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、任意のストリームに差し替える。型が一致しないOnErrorメッセージは無視する 引数の型はFunc<TException, IObservable<T>> errorHandlerTExceptionの型は必ず明示しないといけない
Observable.Catch<T> 指定された複数のストリームを先頭から順番に購読し、OnNextメッセージをそのまま伝える。途中でOnErrorメッセージが発生すると次のストリームにスイッチする。OnCompletedメッセージが発行されたタイミングで終了する 失敗したら次へ、失敗したら次へ、成功するまで繰り返す
CatchIgnore<T, TException> 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、Observable.Empty<T>に差し替える。型が一致しないOnErrorメッセージは無視する エラーが起きたときにもみ消して終了する
Retry<T> OnErrorメッセージが発行されたときに、上流のストリームを再購読する 計何回まで試行するか指定できる
OnErrorRetry<T> 1. OnErrorメッセージが発行されたときに、上流のストリームを再購読する 引数を何も指定しない場合。Retry<T>と同じ挙動をする
OnErrorRetry<T, TException> 2. 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、上流のストリームを再購読する Catch + Retryの複合
OnErrorRetry<T, TException> 3. 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、一定時間待ってから上流のストリームを再購読する Catch + Retryの複合だが、リトライするまでのディレイをつけられる
Finally<T> ストリームが解体されるタイミングで登録した関数を実行する ストリームの解体とは次に挙げるシチュエーションのこと。「OnCompletedメッセージが発行される」「OnErrorメッセージが発行される」「Disposeにより購読が中断される」「Subscribeで登録した関数の処理途中で例外が発生する」

Hot変換用

名前 効果・用途 備考
Multicast<T> 指定のISubjectを用いてHot変換する
Publish<T> 1. Subject<T>を用いてHot変換する Multicast(new Subject<T>())に相当
Publish<T> 2. BehaviorSubject<T>を用いてHot変換する 初期値を設定した場合はこちらの挙動になる。Multicast(new BehaviorSubject<T>())に相当
PublishLast<T> AsyncSubject<T>を用いてHot変換する Multicast(new AsyncSubject<T>())に相当。OnErrorメッセージもキャッシュしてしまうのでエラーハンドリングに注意
Replay<T> ReplaySubject<T>を用いてHot変換する Multicast(new ReplaySubject<T>())に相当
RefCount<T> Hot変換時、自分を購読するObserverがいる場合にストリームを稼働させる。Observerがいなくなると自動で停止する IConnectableObservable<T>に対してのみ利用可能
Share<T> Publish<T>().RefCount()の省略記法

Schedulerの切り替え

名前 効果・用途 備考
ObserveOn<T> メッセージの実行コンテキストを指定のSchedulerに切り替える
ObserveOnMainThread<T> メッセージの実行コンテキストをUnityメインスレッドに切り替える MainThreadDispatchTypeでメインスレッド上のどのタイミングにするかを指定できる
SubscribeOn<T> ストリームの初期構築を指定のSchedulerで行う あまり使うことはない
SubscribeOnMainThread<T> ストリームの初期構築をUnityメインスレッド上で行う
DelaySubscription<T> 1. 指定された時間だけ待ってからSubscribe()を実行する 購読開始のタイミングをずらせる
DelaySubscription<T> 2. 指定された時刻になったらSubscribe()を実行する 購読開始のタイミングを指定できる
DelayFrameSubscription<T> 指定されたフレーム数だけ待ってからSubscribe()を実行する UpdateやFixedUpdateのカウント数を指定できる

その他

名前 効果・用途 備考
Do<T> ストリームのメッセージを用いて副作用を起こす。メッセージそのものは加工しない ストリームの途中で、ストリーム外部の状態を変化させるときに使う
DoOnError<T> OnErrorメッセージを用いて副作用を起こす。メッセージそのものは加工しない OnErrorメッセージのみに反応
DoOnCompleted<T> OnCompletedメッセージを用いて副作用を起こす。メッセージそのものは加工しない OnCompletedメッセージのみに反応
DoOnTerminate<T> OnErrorまたはOnCompletedメッセージが発行されたとき副作用を起こす。メッセージそのものは加工しない OnErrorメッセージまたはOnCompletedメッセージに反応
DoOnSubscribe<T> ストリームがSubscribe()されたときに副作用を起こす 購読された瞬間をログに出したりするとデバッグに便利
DoOnCancel<T> ストリームがDispose()によってキャンセルされたときに副作用を起こす OnErrorメッセージやOnCompletedメッセージには反応しない
ForEachAsync<T> 非同期処理の結果を伝搬しつつ、そのメッセージ消費を行う Do().Last().AsUnitObservable()に挙動は似ている。AsyncReactiveCommandと組み合わせると非常に便利
Materialize<T> すべてのメッセージをOnNext(Notification<T>)メッセージへと変換する OnErrorメッセージやOnCompletedメッセージをOnNextメッセージに格納してしまう。どんなメッセージが発行されたのかを列挙できるので、テストするときに便利
Dematerialize<T> Materialize<T>によって変換されたメッセージをもとに戻す
AsObservable<T> 指定のオブジェクトのインタフェースをIObservable<T>に制限する オブジェクトのダウンキャストやクロスキャストを防止できる
ToArray<T> 発行されたOnNextメッセージをすべてキャッシュし、OnCompletedメッセージが入力されたタイミングで1つのOnNext(T[])メッセージに変換して出力する
ToList<T> 発行されたOnNextメッセージをすべてキャッシュし、OnCompletedメッセージが入力されたタイミングで1つのOnNext(IList<T>)メッセージに変換して出力する
Wait<T> ストリームが完了するのをスレッドをブロックして待機する メインスレッドで使うとフリーズするので常用禁止
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【用Unity开发Oculus Quest用的APP】 第②回:抓住物体吧

◆写在开头

这次是第②回、
手的表示,与可抓住物体的制作。

第①回的内容请点这里
第①回:导入APP、进行运行状态确认

(还有日文版哦)
PS:因为我的制作环境是日语环境,所以下面的软件截图,都是日文版的截图。
表示内容不同,但菜单的位置,作用等都是一样的,
根据说明和参考截图应该能找到相对应的功能的所在位置。

◆ 开发环境

macOS Mojave 版本 10.14.6
Unity 2018.4.12f1
Android SDK

◆ 制作顺序

  • 1. 在画面中表示手
    • 1_1. 导入LocalAvatarWithGrab
    • 1_2. Ovr Avatar 的设定
    • 1_3. 发行与设置OculusAPP用ID
  • 2. 追加一个可以被抓住的物体
  • 3. 运行确认

1.在画面中表示手

1_1. 导入LocalAvatarWithGrab

新建一个Scene
具体的制作方法请参考上回的内容第①回:导入APP、进行运行状态确认

想要抓住物体,首先要有抓住物体用的手。
这次依旧使用上一回导入的Oculus Integration中的功能,
在里面找到LocalAvatarwithGrab,它是搭载了抓住物体功能的Prefab
01.png
「LocalAvatarWithGrab」在
「Assets」→「Oculus」→「SampleFramework」→「Core」→「AvatarGrab」→「Prefabs」里面可以找到。
文件夹的层次比较深、推荐使用Project上方的搜索栏,直接搜索「LocalAvatarWithGrab」。
02.png
找到后,将它拖入Hierarchy后自动追加完毕。
确认追加完成后、点开「Inspector」→「Transform」完成一些初始设定。
这里的设定基本是自由的,这次只把「Scale」调整成1.7便可。
03.png
设置调整完成后、将上一回制作的「OVRCameraRig」删除掉。
因为「LocalAvatarWithGrab」里面已经存在「OVRCameraRig」了,所以之前的就不需要了。

1_2. Ovr Avatar 的设定

接着,点击刚才追加的「LocalAvatarWithGrab」。
点击「Inspector」→「Ovr Avatar」→「Shaders 」调整设定。
04.png
点击「Controller Shader」右边的小圆圈,
会自动弹出「Shader」的设置画面。
05.png
直接在搜索栏搜索「AvatarPBRV2Simple」
找到文件名对得上的「Shader」后点击,自动设置完成。
06.png
设置完成后、「Shader」会变成如图所示的状态。

1_3. 发行与设置OculusAPP用ID

为了能够表示追加完成的手、需要OculusAPP用ID「Create android manifest」。

AppID需要访问Oculus主页,申请后获得。
这里就不一一详细说明了。

07.png
08.png
ID发行后、回到Unity。
选择菜单的「Oculus] →「Avatars」→「Edit Setting」
弹出「OvrAvatarSettings」窗口。

将刚才发行的ID设置好。
09.png
最后,为了手能够正确显示,需要设置「Create android manifest」。
它的设置方法非常简单。
点击「Oculus」→「Tools」→「Create store-compatible AndroidManifest.xml」即可完成。

2. 追加一个可以被抓住的物体

10.png
11.png
在「Hierarchy」中右键,选择「3D」→「Object」→「Cube」后,画面中会自动追加一个方块。
方块尺寸可自由调整。
12.png
选择刚刚追加的「Cube」
在「Inspector」中选择「Add Component」找到「Rigidbody」和「OVR Grabbable 」追加。
「Box Collider」在生成方块的时候应该自动生成了、但如果没有的话,需要用上面同样的方式追加进去。
这样方块的设置就完成了。

3. 运行确认

スクリーンショット 2019-12-20 16.40.25.png
导入APP之前、推荐先在Unity上运行一次、确认一下方块是否会自然落下。
com.oculus.UnitySample1-20191220-165057.jpg
如果没有问题,那么就把APP导入Oculus,实际确认以下是否运行正常吧。

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

【UnityでOculus Quest向けのアプリを作る】 第②回:物を掴む為には

◆はじめに

第②回ですね、
手の表示と物を掴む方法をやります。

第①回は↓に参照してください。
第①回:APPをビルドして、動作確認する

(日本語と中国版あります)

◆ 開発環境

macOS Mojave バージョン 10.14.6
Unity 2018.4.12f1
Android SDK

◆ 手順

  • 1. 手を表示する
    • 1_1. LocalAvatarWithGrab導入
    • 1_2. Ovr Avatar の設定
    • 1_3. OculusのアプリのID発行と設置
  • 2. 掴む物体の追加と設定
  • 3. 動作確認

1.手を表示する

1_1. LocalAvatarWithGrab導入

新規シーンを作ります。
具体的な制作方法は前回の第①回:APPをビルドして、動作確認するを参照してください。

物を掴む為には、まず手を表示する必要が有ります。
前回導入した、Oculus Integrationの中には、物を掴む機能を搭載しているLocalAvatarwithGrab というプレハブが存在しています。
今回は、それを使います。
01.png
「Assets」→「Oculus」→「SampleFramework」→「Core」→「AvatarGrab」→「Prefabs」
の中に「LocalAvatarWithGrab」あります。
階層は深い為、Projectの検索欄で「LocalAvatarWithGrab」と検索を掛けたら探しやすいです。
02.png
これをHierarchyにドラックし、追加します。
追加したら、「Inspector」→「Transform」でポジションなどを設定します。
今回は、 「Scale」は1.7、他は全部0にします。
03.png
設置完了後、前回作った「OVRCameraRig」を削除します。
「LocalAvatarWithGrab」の中にも「OVRCameraRig」が存在している為、不要です。

1_2. Ovr Avatar の設定

続いて、追加した「LocalAvatarWithGrab」クリックし、
「Inspector」→「Ovr Avatar」→「Shaders 」の設定を調整します。
04.png
「Controller Shader」右側の丸をクリック。
「Shader」を設定する画面に遷移するはずです。
05.png
検索欄で「AvatarPBRV2Simple」検索し、
ファイル名が合っている「Shader」をダブルクリックし、設定する事ができます。
06.png
設定したら、「Shader」はこのような状態になります。

1_3. OculusのアプリのID発行と設置

追加した手を表示するためには、OculusのアプリのIDと「Create android manifest 」が必要です。

アプリのIDは、
Oculus ホームページのダッシュボードにアクセスし、
「新しいアプリを作成」からアプリを登録して、発行出来ます。
Oculus Developer Dashboard
詳しいID発行方法は、ここでは割愛します。

07.png
08.png
ID発行後、Unityに戻ります。
メニューから「Oculus] →「Avatars」→「Edit Setting」をクリックすると、
「OvrAvatarSettings」が表示されます。

先程発行したアプリIDを設定して、これでID設定は完了になります。
09.png
最後、手を正しく表示するために「Create android manifest」を設置する必要があります。
設置方法は非常に簡単、
「Oculus」→「Tools」→「Create store-compatible AndroidManifest.xml」をクリックするだけです。

2. 掴む物体の追加と設定

10.png
11.png
掴む用のオブジェクトを追加します。
「Hierarchy」で右クリック「3D」→「Object」→「Cube」を追加します。
サイズは自由です。
12.png
追加した「Cube」を選択します。
「Inspector」→「Add Component」で「Rigidbody」と「OVR Grabbable 」を検索し、追加します。
「Box Collider」は最初から存在するはずですが、なければ「Rigidbody」と同じ方法で追加します。
これで、設置完了です。

3. 動作確認

スクリーンショット 2019-12-20 16.40.25.png
ビルドする前に、まずUnity上で一回実行して、「Cube」落下するかどうか確認します。
com.oculus.UnitySample1-20191220-165057.jpg
問題無ければ、ビルドして、実機で動作確認しましょう。

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

UniRxとUniTask 相互変換の変わったパターン紹介

はじめに

UniRxのObservableと、UniTaskはそれぞれ相互変換することができます。
今回はその中でも少し変わった変換パターンを紹介したいと思います。

System.Linq UniRx UniRx.Async のusingを忘れずに

IEnumerable<UniTask<T>> -> IObservable<T>

複数のUniTask<T>をまとめて、1つのIObservable<T>にする方法です。
やり方が何パターンかあります。

// 対象
IEnumerable<UniTask<string>> tasks = CreateSample();

// ---

// 並列にまとめる(要素の順序を無視して、終わったものから結果を返す)
IObservable<string> parallel = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Merge(); // IO<T>

// 直列にまとめる(要素の先頭から順番に結果を返す)
IObservable<string> sequential = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Concat(); // IO<T>


// 全部終わってからまとめて結果をとるなら(IO<IE<T>>)
IObservable<IList<string>> whenAll = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Zip(); // IO<IList<T>>

// でも、全部まとめてとるならUniTask.WhenAllでいいのでは?
UniTask<string[]> whenAll2 = UniTask.WhenAll(tasks);

UniTask<IEnumerable<T>> -> IObservable<T>

さっきとネストの仕方が逆のパターン。
UniTask<IE<T>>を分解して1つのIObservable<T>にする方法です。
2パターンあるけど結果は同じです。

// 対象
UniTask<IEnumerable<string>> task = CreateSample();

// パターン1
IObservable<string> p1 = task
    .ToObservable() // IO<IE<T>>
    .Select(x => x.ToObservable()) // IO<IO<T>>
    .Merge(); // IO<T>

// パターン2
IObservable<string> p2 = task
    .ToObservable() // IO<IE<T>>
    .SelectMany(x => x.ToObservable()); // IO<T>

IObservable<T> -> UniTask<IEnumerable<T>>

IObservable<T>が発行するすべてのメッセージを「まとめて」待ち受けるUniTask<T>を作りたい場合。

// 対象
IObservable<string> observable = CreateSample();

// ToArray()してからToUniTask()でOK
UniTask<string[]> task = observable.ToArray().ToUniTask();

IObservable<UniTask<T>> -> IObservable<T>

ObservableUniTaskを扱う場合に、それを1つのObservableにまとめる。

// 対象
IObservable<UniTask<string>> observable = CreateSample();

// 並列(終わった順に結果を出すなら)
IObservable<string> parallel = observable.SelectMany(x => x.ToObservable());

// 直列(もとのIO<T>から発行された順序を維持するなら)
IObservable<string> sequential = observable.Select(x => x.ToObservable()).Concat();

IObservable<UniTask<T>> -> UniTask<IEnumerable<T>>

ObservableUniTaskを扱う場合に、それをUniTask側にまとめる。

IObservable<UniTask<string>> observable = CreateSample();

// 結果は先に終わったUniTaskの順番になる
UniTask<string[]> task = observable
    .SelectMany(x => x.ToObservable()) // IO<IO<T>>
    .Merge() // IO<T>
    .ToArray() // IO<T[]>
    .ToUniTask(); // UniTask<T[]>

UniTask<IObservable<T>> -> IObservable<T>

UniTaskの中にObservableが入り込んじゃった場合。

// 対象
UniTask<IObservable<string>> task = CreateSample();

// taskをIO<IO<T>>に変換してからMerge()
IObservable<string> observable = task.ToObservable().Merge();

UniTask<IObservable<T>> -> UniTask<IEnumerable<T>>

UniTaskの中にObservableが入り込んじゃったものを、今度はUniTask側にまとめる場合。

// async/await使っちゃうのが楽
private async UniTask<IEnumerable<string>> Unwrap(UniTask<IObservable<string>> task)
{
    var observable = await task;
    return await observable.ToArray();
}

IObservable<IObservable<T>> -> UniTask<IEnumerable<T>>

こんなシチュエーションあるのかよくわからないけど。

IObservable<IObservable<string>> observable = CreateSample();

UniTask<string[]> task = observable
    .SelectMany(x => x) // IO<T>
    .ToArray() // IO<T[]>
    .ToUniTask(); // UniTask<T[]>

まとめ

UniRxUniTaskはだいたいどんなパターンでもそれぞれに変換することができます。
両者を組み合わせて使い、必要に応じて変換をかけるとよいでしょう。

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

ゲーム製作の感想

通っている専門学校でチームでのunityを使ったゲーム制作があり、自分の至らぬ点も見つかったので一人反省会です
私の担当はゲームシーンが切り替わるときのエフェクト作成が主でした

フェードインとフェードアウトのプログラムはTAMA-LABさんのスクリプトを参考にしました
Unityでフェードイン/フェードアウトを実現する方法(TAMA-LABさん)

Fade_controler.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class Fade_controler : MonoBehaviour
{
    float fadeSpeed = 0.01f;        //透明度が変わるスピードを管理
    float red, green, blue, alfa;   //パネルの色、不透明度を管理

    private bool isFadeOut = false;  //フェードアウト処理の開始、完了を管理するフラグ
    private bool isFadeIn = false;   //フェードイン処理の開始、完了を管理するフラグ

    Image fadeImage;                //透明度を変更するパネルのイメージ

    void Start()
    {
        isFadeIn = true;

        fadeImage = GetComponent<Image>();
        red = fadeImage.color.r;
        green = fadeImage.color.g;
        blue = fadeImage.color.b;
        alfa = fadeImage.color.a;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0)&&isFadeIn==false)
        {
            isFadeOut = true;
        }

       if (isFadeIn==true)
       {
          StartFadeIn();
       }

       if (isFadeOut==true)
       {
          StartFadeOut();
       }


    }

    void StartFadeIn()
    {
        Debug.Log("1");
        alfa -= fadeSpeed;                //a)不透明度を徐々に下げる
        SetAlpha();                      //b)変更した不透明度パネルに反映する
        if (alfa <= 0)
        {                    //c)完全に透明になったら処理を抜ける
            isFadeIn = false;
            fadeImage.enabled = false;    //d)パネルの表示をオフにする
        }
    }

    void StartFadeOut()
    {
        Debug.Log("2");
        fadeImage.enabled = true;  // a)パネルの表示をオンにする
        alfa += fadeSpeed;         // b)不透明度を徐々にあげる
        SetAlpha();               // c)変更した透明度をパネルに反映する
        if (alfa >= 1)
        {             // d)完全に不透明になったら処理を抜ける
            isFadeOut = false;
            SceneManager.LoadScene("Traning");
        }
    }



    void SetAlpha()
    {
        fadeImage.color = new Color(red, green, blue, alfa);
    }
}

今回の反省点は、この親切でわかりやすいプログラムを自分で理解して扱うまでに一週間かけてしまったことです
理由を考えてみるとフラグを使い慣れておらず使い方を思い出せなかったことや、何がわからないかを整頓できなかったことから、日ごろからプログラムを学ぼうとする意欲が足りなかったことが原因だと考えつきました
これをもとに、どんなゲームを作るか考えることだけでなく、今まで考えたゲームを片っ端から作ることでもっとプログラムに親しもうと思いました
あとゆにてぃがおもったとりにうごいてくれたときはすごくたのしかったしうれしかったです。

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

unityのParticle Systemを使って魔法っぽいエフェクトを作る

制作理由

学校でグループ制作中に戦闘時に攻撃する際にエフェクトがあったほうが戦闘時に単調にならず、またプレイヤーを楽しませる要素の一つになると考えたので制作しました。

Particle Systemを作成する

製作途中のゲームのものをそのまま使用しているため実際には必要のないものが混じっています

まずはHierarchy内で右クリックをしてEffects > Particle Systemで作成します。
image.png

今回は攻撃魔法用の爆発エフェクトを例に作成していきます。

パーティクルのカスタマイズ

作成した段階では白い球のようなものが上の方向に拡散しながら上がっていくだけなので、まずは爆発っぽくなるように中心から外側に広がっていくように設定します。
InspectorからShapeを選択し、ConeからSphereに変更します。
image.png

これだけだとまだ爆発っぽくないのでもっと爆発に近いエフェクトになるようにしていきましょう。
まずはEmissionを下のように設定します。
image.png
これで一度に出現するParticleの数が増加しました。
image.png
続けてLimit Velocity over LifetimeとSize over Lifetimeを次のように設定してください。
image.png
これで爆発のようなエフェクトにはなりますが少しParticleが残る時間が長いのでもう少し短くしましょう。
DurationとStart Lifetimeの値を次のように設定してください。
image.png
最後にRendererのMaterialからお好みのMaterialを選択すれば完成です。
image.png
↑こんな感じになります

今回はStandard AssetsのParticleFireballを使用しましたが、ほかに自分が気に入ったものがあればそちらをお使いください。

最後に

今回が初めての投稿になります。こうしたほうがもっと見やすくなる、こっちのほうがもっと爆発っぽいエフェクトになるなどのアドバイスがありましたら是非教えていただきたいです。よろしくお願いいたします。

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

3Dマークアップ言語でWebサービス的なやつを作ってみる

目的

前回までの、3Dマークアップ言語を動的に出力するWebシステムを作る。

道具立て

比較的慣れてるので、PythonでFlaskを使ってサクッと作ってみる。

時計を作る

データベースとかアクセスしたりAPI使うのも大がかりなので、PoCとして最低限として、アクセス時刻の表示をする時計を作ってみる。
以下、時分秒に対応する球を表示するプログラム。時刻の経過に合わせて動くわけではない。

app.py
from flask import Flask
import datetime
import math

app = Flask(__name__)

@app.route('/')
def clock():
    dt_now = datetime.datetime.now()

    hour = dt_now.hour % 12
    minute = dt_now.minute
    sec = dt_now.second

    yh = 0.2 * math.cos(hour * 2 * 3.141592 / 12)
    xh = 0.2 * math.sin(hour * 2 * 3.141592 / 12)
    ym = 0.4 * math.cos(minute * 2 * 3.141592 / 60)
    xm = 0.4 * math.sin(minute * 2 * 3.141592 / 60)
    ys = 0.35 * math.cos(sec * 2 * 3.141592 / 60)
    xs = 0.35 * math.sin(sec * 2 * 3.141592 / 60)

    homl = '''<homl><head><title>CLOCK</title></head>
<body><a-scene wx=0.2 wy=0.2 wz=0.2>
<a-sphere r=0.05 x=0 y=0 z=0 color=white />
<a-sphere r=0.05 x={xh} y={yh} z=0 color=red />
<a-sphere r=0.03 x={xm} y={ym} z=0 color=green />
<a-sphere r=0.01 x={xs} y={ys} z=0 color=blue />
</a-scene></body></homl>
'''.format(xh=xh,yh=yh,xm=xm,ym=ym,xs=xs,ys=ys)

    return homl

if __name__ == '__main__':
    app.run()

結果

20191223_homl_clock.PNG

それっぽくなった。
これで、インターネットの世界とXRの世界が簡単につながるようになった。

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

LuaエンジンをUnityに組み込んでみる

目的

HTMLに対応するJavaScriptのように、前回の3Dマークアップ言語にスクリプティングシステムとしてLuaを組み込んでみたい。
そのためのとっかかりとして、UnityにLuaエンジンを組み込み、GameObjectをLuaから操作してみる。

道具立て

組み込むLuaエンジンとして、NLuaを使ってみた。
nugetなどで導入する。

Unityプロジェクトとプログラム

Unityのヒエラルキーはこんな感じ。

20191223_nlua.PNG

プログラムは、まず、以下のコードを関係する全オブジェクト(Root,Child1,Cube)にアタッチしておく。これを介してLuaからGameObjectを操作する。

NLuaTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace NLuaTestNS
{
    public class NLuaTest : MonoBehaviour
    {
        public NLuaTest getChild(int i)
        {
            return gameObject.transform.GetChild(i).gameObject.GetComponent<NLuaTest>();
        }

        public void setColor(int r, int g, int b, int a)
        {
            gameObject.GetComponent<Renderer>().material.color = new Color((float)r/255f, (float)g/255f, (float)b/255f, (float)a/255f);
        }

        public void setPosition(float x, float y, float z)
        {
            gameObject.transform.localPosition = new Vector3(x/10f,y/10f,z/10f);
        }

        public void setRotation(float x, float y, float z)
        {
            gameObject.transform.localRotation = Quaternion.Euler(x,y,z);
        }
    }
}

そして、以下のコードをRootにアタッチして実行。

NLuaDo.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NLuaTestNS;
using NLua;

public class NLuaDo : MonoBehaviour
{
    private Lua state;
    private int color;

    // Start is called before the first frame update
    void Start()
    {
        state = new Lua();
        state["root"] = GetComponent<NLuaTest>();
        state.DoString("root:getChild(0):getChild(0):setColor(0,0,0,255)");
    }

    void Update()
    {
        color += 1;
        color %= 256;
        state.DoString("root:getChild(0):getChild(0):setColor("+color+","+color+","+color+",255)");
        state.DoString("root:getChild(0):getChild(0):setPosition(" + color + "," + color + "," + color + ")");
        state.DoString("root:getChild(0):getChild(0):setRotation(" + color + "," + color + "," + color + ")");
    }
}

色を変えるのと、適当なアニメーションをつけてみた。

結果

素直に動いてくれた。

20191223_nluaexec.PNG

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

unityちゃんを透明にする方法

製作理由

学校のグループ製作中に、敵であるunityちゃんの姿を消したいなと思い、このプログラムを書きました。

unityちゃんのメッシュ情報を取得する

製作中のゲームからそのまま持ってきたので余計な物も入ってます

まずunitychanの中にあるmesh_rootを取得します
画像参照
説明用.png

このmesh_rootをつけたり消したりすると以下のようになります
mjLeSGiIi1SMt0QZ2fXM1577078165-1577078176.gif

これらを用いて簡単に付いたり消えたりするものを作りました

sample.cs
GameObject mesh;

private void start
{
    float elapsedTime = 0;
    mesh = GameObject.Find("mesh_root");
}
private void update
{
    elapsedTime+=Time.deltaTime;
    if(elapsedTime > 2f)
        mesh.SetActive(true);
    else
        mesh.SetActive(false);
    if(elapsedTime > 4f)
        elapsedTime = 0f;
}

これでunityちゃんが見えたり消えたりするようにできました。

最後に

もっと簡単にできるよ!等のアドバイス等ありましたら
是非教えていただきたいです!よろしくお願いします!

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

HTML Agility Packを使ってUnityで使える3Dマークアップ言語を実装してみる

目的

3Dオブジェクトを記述する3Dマークアップ言語をUnityで実装してみる。

マークアップ言語?

ざっくりHTMLみたいな、A-Frameに近いものを作ってみます。

道具立て

HTMLライクなマークアップ言語のパースに、HTML Agility Packを使います。
NuGetで入れることもできますし、サイトからnupkgをダウンロードして7zipなどで展開して、DLLだけ使うということもできます。
.Net 4.5用だったり、.Net Standard 2.0用だったり、いくつか種類があるので、自分のプロジェクトに合ったものを使います。
UnityのDLLのImportセッティングのところで、適切なプラットフォームに対して、適切なDLLが使われるように設定しましょう。

実際のプログラム

以下のプログラムがパーサーのエントリー部分になります。

HomlParser.cs
using System.Collections;
using System.Collections.Generic;
using System.Runtime;
using UnityEngine;
using HtmlAgilityPack;
using Homl.DOM;
using System.Reflection;

namespace Homl.Parser
{
    public class HomlParser
    {
        public static Dictionary<string, ParseNode> nodeTemplates;

        public HomlParser()
        {
            if(nodeTemplates == null)
            {
                nodeTemplates = new Dictionary<string, ParseNode>();

                nodeTemplates.Add("a-scene", new Body());
                nodeTemplates.Add("a-box", new Cube());
                nodeTemplates.Add("a-cylinder", new Cylinder());
                nodeTemplates.Add("a-sphere", new Sphere());
                nodeTemplates.Add("a-link", new ATag());
                nodeTemplates.Add("a-text", new Text());
                nodeTemplates.Add("homl", new Homl());
            }
        }

        public Document Parse(string homl)
        {
            var htmlDoc = new HtmlAgilityPack.HtmlDocument();
            htmlDoc.LoadHtml(homl);

            GameObject documentGO = new GameObject("Document");
            Document document = documentGO.AddComponent<Document>();
            BoxCollider bc = documentGO.AddComponent<BoxCollider>();

            walkinChild(htmlDoc.DocumentNode.ChildNodes, documentGO);

            bc.size = new Vector3(0.3f, 0.3f, 0.3f);
            return document;
        }

        private void walkinChild(HtmlAgilityPack.HtmlNodeCollection nodes, GameObject parent)
        {
            foreach (HtmlAgilityPack.HtmlNode node in nodes)
            {
                GameObject nodeObject = null;
                Debug.Log(node.Name);

                if (nodeTemplates.ContainsKey(node.Name))
                {
                    nodeObject = nodeTemplates[node.Name].parse(node, parent);
                } else
                {
                    nodeObject = parent;
                }

                if (node.HasChildNodes)
                {
                    walkinChild(node.ChildNodes, nodeObject);
                }

            }
        }
    }
}

再帰的にマークアップの構造をたどり、タグ名の辞書から該当タグをパースするオブジェクトを探しながらUnityのGameObjectを作成していきます。

各タグのパーサーは、例えばCubeに対するパーサーが以下のようになっています。

Cube.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HtmlAgilityPack;

namespace Homl.Parser
{
    public class Cube : ParseNode
    {
        public override GameObject parse(HtmlNode homlNode, GameObject parent)
        {
            PrimitiveProducer pp = GameObject.Find("PrimitiveProducer").GetComponent<PrimitiveProducer>();
            GameObject cube = GameObject.Instantiate(pp.getCube());
            float x = float.Parse(homlNode.GetAttributeValue("x", "0.0f"));
            float y = float.Parse(homlNode.GetAttributeValue("y", "0.0f"));
            float z = float.Parse(homlNode.GetAttributeValue("z", "0.0f"));
            float s = float.Parse(homlNode.GetAttributeValue("size", "0.1f"));
            string color = homlNode.GetAttributeValue("color", "white");

            cube.transform.parent = parent.transform;
            cube.transform.Translate(new Vector3(x, y, z));
            cube.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);

            Material mat = cube.GetComponent<Renderer>().material;
            mat.color = ColorManager.Instance.getColor(color);
            mat.SetColor("_BaseColor", ColorManager.Instance.getColor(color));

            Vector3 scale = new Vector3(s, s, s);
            Vector3[] verts = cube.GetComponent<MeshFilter>().mesh.vertices;
            Vector3[] newverts = new Vector3[verts.Length];
            int i = 0;
            foreach (Vector3 v in verts)
            {
                v.Scale(scale);
                newverts[i] = v;
                i++;
            }
            cube.GetComponent<MeshFilter>().mesh.vertices = newverts;
            BoxCollider bc = cube.GetComponent<BoxCollider>();
            bc.size = scale;

            DOM.Cube cb = cube.AddComponent<DOM.Cube>();
            return cube;
        }
    }
}

PrimitiveProducerは、プリミティブの3DオブジェクトPrefabの参照を管理しているオブジェクトです。
Attributeの指定の仕方などは、本家のA-Frameと異なるところなので、コンポーネント志向の構造と合わせて抜本的な見直しは必要ですが、
これに加えて、DOMのクラス群を合わせて動かすことで、3DオブジェクトをマークアップからUnity内に生成して、リンクなどの仕組みを作ることもできるようになりました。

表示結果

20191223_homl.PNG

もとになったマークアップ

天気予報の雪だるまのつもり

<homl>
<head>
<title>Weather</title>
</head>
<body>
<a-scene wx=0.2 wy=0.001 wz=0.2>
<a-cylinder height=0.02 r=0.02 x=0 y=0.18 z=0 color=red />
<a-sphere r=0.1 x=0 y=0.05 z=0 color=white />
<a-sphere r=0.08 x=0 y=0.14 z=0 color=white />
</a-scene>
</body>
</homl>

機能テスト用

<homl>
<head>
<title>TestHoloML</title>
</head>
<body>
<a-scene wx=0.2 wy=0.2 wz=0.2>
<a-box size=0.1 x=0 y=0 z=0 color=blue>
 <a-cylinder height=0.05 r=0.02 x=0.1 y=0.1 z=0.1 color=red />
 <a-sphere r=0.05 x=-0.1 y=-0.1 z=-0.1 color=green />
 <a-text size=0.1 x=0 y=0 z=0 color=blue>ABCDEF</text>
</a-box>
<a-link href=”test.homl”>
 <a-box size=0.1 x=0.1 y=-0.1 z=0.1 color=white />
</a-link>
</a-scene>
</body>
</homl>

3Dグラフ

<homl>
<head>
<title>3D Graph</title>
</head>
<body>
<a-scene wx=0.5 wy=0.001 wz=0.2>
<a-cylinder height=0.2 r=0.05 x=-0.2 y=0.2 z=0 color=red />
<a-cylinder height=0.12 r=0.05 x=-0.1 y=0.12 z=0 color=red />
<a-cylinder height=0.14 r=0.05 x=0 y=0.14 z=0 color=red />
<a-cylinder height=0.08 r=0.05 x=0.1 y=0.08 z=0 color=red />
<a-cylinder height=0.05 r=0.05 x=0.2 y=0.05 z=0 color=red />
</a-scene>
</body>
</homl>

現状ではプリミティブオブジェクトの組み合わせだけなのですが、このように表示が可能になりました。

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

30分で作る初めてのUnity

はじめに

普段はWeb周りの技術を扱ってばかりなので、
今回は以前から興味があったUnityを触ってみました。

この記事は、

なんでもいいから一度Unityを使ってプロジェクトを作ってみたい。

という方向けに、プロジェクトの作成からBuildまでの流れを掴んでもらう為に書いたものです。
ですので、数あるUnity機能の中からほんの数個ピックアップして追加したのみにすぎません。
ご了承ください。

目次

  1. イントール
  2. プロジェクトの作成
  3. レイアウトの変更
  4. オブジェクトの作成
  5. オブジェクトに装飾
  6. オブジェクトに動きを与える
  7. プロジェクトをBuild

1. イントール

まずはUnityをダウンロードしましょう。
個人開発であれば無料です。
→ ダウンロード画面へ
(*途中のアカウント作成等は割愛させていただきます。)

スクリーンショット 2019-12-22 16.02.23.png

2. プロジェクトの作成

インストールしたApplicationを開くと以下の画面が現れるので、Newを選択します。

スクリーンショット 2019-12-22 16.06.02.png

Template3D にしてプロジェクトを作成します。
スクリーンショット 2019-12-22 16.18.34.png

3. レイアウトの変更

こちらがプロジェクトのデフォルト画面です。

スクリーンショット 2019-12-22 16.22.30.png

自分の作業しやすいレイアウトに変更してください。
私は2by3にしています。

スクリーンショット 2019-12-22 16.27.32.png

4. オブジェクトを作成

ゲームオブジェクト(以下オブジェクト)を作成します。
HierarchyCreate3DオブジェクトCube の順で選択してください。

すると画面左のSceneCubeが現れます。

スクリーンショット 2019-12-22 16.31.55.png

オブジェクトの形を変えるために画面右上のInspectorを下記のように編集します。

スクリーンショット 2019-12-22 16.41.28.png

オブジェクトの形が変わりました。
スクリーンショット 2019-12-22 16.42.28.png

この調子で Squareも追加してみます。

スクリーンショット 2019-12-22 16.44.05.png

5. オブジェクトに装飾

CubeSquareに色を与えましょう

ProjectMaterialsを選択

スクリーンショット 2019-12-22 16.46.38.png

画面を見やすくするために 右上設定(三本の棒線)からOne Column Layoutを選択します。

スクリーンショット 2019-12-22 16.47.11.png

Material内のスポイトのようなアイコンから色を選択します。

スクリーンショット 2019-12-22 16.49.34.png

この調子でもう1色作成します。

そして、今作成したMaterialは対象のオブジェクトにドラッグ&ドロップできます。
(Scene, Hierarchyどちらでも可)

スクリーンショット 2019-12-22 16.51.43.png

6. オブジェクトに動きを加える

次はSquareに動きをつけられるようにします。

まず、SquareInspectorからRigidBody(オブジェクトに重力を付与)を追加します。

スクリーンショット 2019-12-22 17.09.41.png

次に、Add Componentを選択します。
最下部にあるNew Scripを選択、名前を決めてScriptを作成します。

スクリーンショット 2019-12-22 16.55.56.png

スクリーンショット 2019-12-22 16.56.21.png

作成されたScriptをダブルクリックするとEditorが開くはずです。

スクリーンショット 2019-12-22 16.58.56.png

以下、カーソルを押した方向にSquareを動かすようにするScriptです。

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

public class moveScriot : MonoBehaviour
{
    Rigidbody rigidbody;

    // Start is called before the first frame update
    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {
        float moveH = Input.GetAxis("Horizontal");
        float moveV = Input.GetAxis("Vertical");
        Vector3 move = new Vector3(moveH, 0, moveV);
        rigidbody.AddForce(move);
    }
}

これでSquareが動くようになったと思います。

ezgif.com-video-to-gif.gif

画面左下に見えているGameが実際にユーザーがゲームを遊ぶときの視点です。
わかりやすくするために、Main CameraTransFormを調整してください。

スクリーンショット 2019-12-22 18.07.33.png

7. プロジェクトをBuildする

最後にこのプロジェクトをBuildしてみましょう。

FileBuild Settingsを選択

スクリーンショット 2019-12-22 18.11.57.png

こちらでプラットフォームを選択することができます。
今回はMacOSのまま進むのでPlatformはそのままで問題ありません。
右上のAdd Open Souceから現在のScenesを追加して、
最後にBuildを選択してください。
スクリーンショット 2019-12-22 20.54.56.png

デスクトップに作成したゲームが出現します。

スクリーンショット 2019-12-22 18.14.06.png

これでいつでも先程のGameを開くことができるようになりました。

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

OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する

最近のOculusQuestさんちょっと攻めすぎですよね。
OculusLinkのβが出たのもつい最近な気がしているのに、さらにハンドトラッキングまで!!!

そして、2019/12/20にはUnityのAssetStoreにハンドトラッキング対応のSDKが配布されました。 高速感!

導入方法なんかについては他の方の記事にお任せして・・・。

肝はOVRSkelton

OVRHand には GetFingerIsPinching()GetFingerPinchStrength() があり、それぞれ、ピンチ(つまんでいる)状態かどうかと
つまんでいる力(距離?)が取得できるメソッドが入っています。
参考:OculusQuestのハンドトラッキングについて色々調べてみた

しかし、ぱっと見たところそれ以外の情報取得がそんなにありません。

そこで上記記事でも言及していますが、もっと詳しい情報を知りたい場合は OVRSkelton.Bones にボーン情報が入っている のでそれを使うことにします。

というのも

僕がやりたかったのは以前作った 「空中に図形を描いて物体を生成するアプリを作ってみた」 のハンドトラッキング版だったので、空中に絵を描くイメージです。
みなさんが空中に絵を描く(例えば、誰かに「**って漢字どう書くんだったっけ?」と聞かれた)時に指をどんな形にするかっていうと

十中八九:point_up:←これですよね

この「人差し指だけを伸ばして、他曲げている」をGetFingerIsPinching()GetFingerPinchStrength()でやるのはちょっと無理があります。(できなくはないと思いますが)

そこで、 OVRSkelton.Bones から情報を抜き出して簡単に分析することにしました。

Bone情報取得

OVRSkelton.Bones は IList(Readonly)で、何番目にどの情報が入っているかは以下のようにenumで宣言されています。

    public enum BoneId
    {
        Invalid                 = OVRPlugin.BoneId.Invalid,

        Hand_Start              = OVRPlugin.BoneId.Hand_Start,
        Hand_WristRoot          = OVRPlugin.BoneId.Hand_WristRoot,          // root frame of the hand, where the wrist is located
        Hand_ForearmStub        = OVRPlugin.BoneId.Hand_ForearmStub,        // frame for user's forearm
        Hand_Thumb0             = OVRPlugin.BoneId.Hand_Thumb0,             // thumb trapezium bone
        Hand_Thumb1             = OVRPlugin.BoneId.Hand_Thumb1,             // thumb metacarpal bone
        Hand_Thumb2             = OVRPlugin.BoneId.Hand_Thumb2,             // thumb proximal phalange bone
        Hand_Thumb3             = OVRPlugin.BoneId.Hand_Thumb3,             // thumb distal phalange bone
        Hand_Index1             = OVRPlugin.BoneId.Hand_Index1,             // index proximal phalange bone
        Hand_Index2             = OVRPlugin.BoneId.Hand_Index2,             // index intermediate phalange bone
        Hand_Index3             = OVRPlugin.BoneId.Hand_Index3,             // index distal phalange bone
        Hand_Middle1            = OVRPlugin.BoneId.Hand_Middle1,            // middle proximal phalange bone
        Hand_Middle2            = OVRPlugin.BoneId.Hand_Middle2,            // middle intermediate phalange bone
        Hand_Middle3            = OVRPlugin.BoneId.Hand_Middle3,            // middle distal phalange bone
        Hand_Ring1              = OVRPlugin.BoneId.Hand_Ring1,              // ring proximal phalange bone
        Hand_Ring2              = OVRPlugin.BoneId.Hand_Ring2,              // ring intermediate phalange bone
        Hand_Ring3              = OVRPlugin.BoneId.Hand_Ring3,              // ring distal phalange bone
        Hand_Pinky0             = OVRPlugin.BoneId.Hand_Pinky0,             // pinky metacarpal bone
        Hand_Pinky1             = OVRPlugin.BoneId.Hand_Pinky1,             // pinky proximal phalange bone
        Hand_Pinky2             = OVRPlugin.BoneId.Hand_Pinky2,             // pinky intermediate phalange bone
        Hand_Pinky3             = OVRPlugin.BoneId.Hand_Pinky3,             // pinky distal phalange bone
        Hand_MaxSkinnable       = OVRPlugin.BoneId.Hand_MaxSkinnable,
        // Bone tips are position only. They are not used for skinning but are useful for hit-testing.
        // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
        Hand_ThumbTip           = OVRPlugin.BoneId.Hand_ThumbTip,           // tip of the thumb
        Hand_IndexTip           = OVRPlugin.BoneId.Hand_IndexTip,           // tip of the index finger
        Hand_MiddleTip          = OVRPlugin.BoneId.Hand_MiddleTip,          // tip of the middle finger
        Hand_RingTip            = OVRPlugin.BoneId.Hand_RingTip,            // tip of the ring finger
        Hand_PinkyTip           = OVRPlugin.BoneId.Hand_PinkyTip,           // tip of the pinky
        Hand_End                = OVRPlugin.BoneId.Hand_End,

        // add new bones here

        Max                     = OVRPlugin.BoneId.Max
    }

親指:Thumb
人差し指:Index
中指:Middle
薬指:Ring
小指:Pinky

で、1,2,3 は関節を表しており、数字が大きくなるほど指先に近づいていく ようです。
僕は英語をロクに読まず 「第一関節」= 1 だと思い込んで処理を書いていたらさっぱり正しい値が返ってこなくてハテ? となりました。ご注意を・・・。
そして、 Tip とついているのは指先です。

分かりやすくマッピングしてみると、どうもこんな感じ・・・?
image.png

なお、Pinky と Thumb だけ 0番があります。

そして、この Bone には Transform が入っており、位置や回転が取れそうです。
例えば「人差し指の先端の位置」を取るには

var indexTipPos = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

こうなります(注:enumなのでintキャストが必要)

直線判定

以上を踏まえ、:point_up:この「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」を判定します。(親指は除きました。)

この「人差し指がまっすぐになっている」というのは人差し指の「第三関節→第二関節」の方向(ベクトル)と、「第二関節→第一関節」の方向(ベクトル)と「第一関節→指先」の方向(ベクトル)が大体同じ方向を向いている ということです。

この「二つのベクトルが大体同じ方向を向いている」=「どれぐらい似通っているか」を表すのは。そう内積(Vector3.Dot)です。

ベクトルの内積は直角(一番似通っていない)な場合は0
全く同じ場合は+1
全く逆方向の場合は-1です

これを人差し指だけではなく他の指の分もベタ書きするとそこそこ長くなってしまうので、以下のようなメソッドを用意すると便利だと思います。

        [SerializeField]
        private OVRSkeleton _skeleton; //右手、もしくは左手の Bone情報

        /// <summary>
        /// 指定した全てのBoneIDが直線状にあるかどうか調べる
        /// </summary>
        /// <param name="threshold">閾値 1に近いほど厳しい</param>
        /// <param name="boneids"></param>
        /// <returns></returns>
        private bool IsStraight(float threshold, params OVRSkeleton.BoneId[] boneids)
        {
            if (boneids.Length < 3) return false;   //調べようがない
            Vector3? oldVec = null;
            var dot = 1.0f;
            for (var index = 0; index < boneids.Length-1; index++)
            {
                var v = (_skeleton.Bones[(int)boneids[index+1]].Transform.position - _skeleton.Bones[(int)boneids[index]].Transform.position).normalized;
                if (oldVec.HasValue)
                {
                    dot *= Vector3.Dot(v, oldVec.Value); //内積の値を総乗していく
                }
                oldVec = v;//ひとつ前の指ベクトル
            }
            return dot >= threshold; //指定したBoneIDの内積の総乗が閾値を超えていたら直線とみなす
        }

可変長配列でBoneIDを複数(3個以上)受けとり、一つ前のBoneIDが示す関節から関節のベクトルと、今のBoneIDが示す関節と次の関節のベクトルの内積を計算して、 dot にどんどん乗算していっています。(別に平均でも良い気はしますが)

これを使うと、人差し指がまっすぐかどうかを判定してLogに表示する場合、このようになります。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");

おなじく、他の指も調べていけば、:point_up:「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」は判定できそうです。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
var isMiddleStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Middle1, OVRSkeleton.BoneId.Hand_Middle2, OVRSkeleton.BoneId.Hand_Middle3, OVRSkeleton.BoneId.Hand_MiddleTip);
var isRingStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Ring1, OVRSkeleton.BoneId.Hand_Ring2, OVRSkeleton.BoneId.Hand_Ring3, OVRSkeleton.BoneId.Hand_RingTip);
var isPinkyStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Pinky0, OVRSkeleton.BoneId.Hand_Pinky1, OVRSkeleton.BoneId.Hand_Pinky2, OVRSkeleton.BoneId.Hand_Pinky3, OVRSkeleton.BoneId.Hand_PinkyTip);

Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"中指は{isMiddleStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"薬指は{isRingStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"小指は{isPinkyStraight?"まっすぐ":"曲がってる"}");

if(isIndexStraight && !isMiddleStraight  && !isRingStraight  && !isPinkyStraight ){ //人差し指だけまっすぐで、その他が曲がっている
    Debug.Log($"お前がナンバーワンだ!");
}

そして、人差し指の先端の位置 (IndexTip) で線を描くとこうなりました

うーん。 ノイズなのかなんなのか。 まったく直線が描けてないですね。

とりあえずの目標(:point_up:で線を描く)はできているので、よしとします。
この問題はまた後日・・・。

まとめ

OVRHandからとれるピンチ情報に加え、この「各指が曲がっているか(false)伸びているか(true)」が加わるだけでもいろんな事が出来るんじゃないかなと思います。
<例>
- 全部falseならグー、人差し指と中指だけtrueならチョキ、全部trueならパー、でじゃんけん
- 中指と小指がfalse、そのほかがtrueで「グワシ!」
- 中指だけtrue そのほかfalse で 「F******!」で、プログラム強制終了。
などなど。

しかし・・。ちゃんとリファレンス見てないので、こんなことしなくても情報は取れるよ!もっといい方法あるよ! などあったらコメント教えてください。

ではでは、よきOculusQuestライフを。

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

Unity AR Plane Occlusionの実装について

ARで現実世界にオブジェクトを生成する時、開発者が指示しなければ、カメラは生成したバーチャルオブジェクトと現実世界のオブジェクト(机とか)との位置関係を正しく認識することができず、結果的に不自然な描画になってしまいます。

このオブジェクトの重なりをいかに描画するかと言う問題は何もARに限った話ではなく、Computer Graphicsにおける基本的な問題の1つです。
もちろんすでに先人達が解決してくれているので、その知恵を基にこの問題に対処したいと思います。

今回、AR Plane Occlusionを実装するに当たって、レンダリングパイプラインやdepth bufferについての理解が必要不可欠だったので、そこら辺も含めて記事にしたいと思います。

今回実現したいこと

ARでより現実味のある自然な描画を実現したい!

現状

  • ARで出力された描画が不自然
  • レンダリングパイプラインのどの部分をいじれば、自然な描画に近づけるのかわからない。

準備

そもそもレンダリングパイプラインとは、パイプライン処理によって入力されたデータ(3次元モデルデータなど)を最終的に2次元の画像として出力するまでの過程全体のことです。

Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう
と言う動画で非常にわかりやすくUnityにおける描画プロセスが解説されています。

この動画の一部を転載すると
スクリーンショット 2019-12-23 0.18.26.png

各ステップについては先ほどの動画を見てもらうとして、今回注目したいのはステップ6と8です。

ステップ6

ステップ6はZTestと言われるものです。
描画するオブジェクトが重なっている場合、depth bufferの値を基に、奥にあるのか手前にあるのかを判定します。

Depth Bufferとはざっくり言えば、カメラから対象とするオブジェクトまでの奥行き(z値)を保持しているバッファになります。なので今回のようなオブジェクト同士の前後関係(手前か奥か)を判定するとは、各オブジェクトのz値を比較すると言っても過言ではないでしょう。

ステップ8

ステップ8ではZWriteがOnならばZ値の更新を行います。基本的には不透明なオブジェクトの描画の際はOnで、部分的な透過などを実装したい際はOffにします。

後述しますが今回は不透明な場合なのでOnにします。

AR Plane Occlusionの実装

ZTestで手前にあるオブジェクトが描画されるようにすれば良いまでは分かりました。
しかし現実にある机や椅子はそのままではオブジェクトとして扱えません。そこで、机やら椅子やらに対してマッピングするように透明なplaneを生成します。

ARFoundationではAR Plane Managerなるものがあり自動的に平面を検知して、指定したplaneオブジェクトを生成してくれます。

まずAR Session OriginにAR Plane Managerコンポーネントをアタッチする。
スクリーンショット 2019-12-23 2.06.29.png

AR Default Planeを作成し、MeshRendererには自作のMaterialをセットします。
Line Rendererも必要ないのでremoveしても良いです。

自作Materialには以下のシェーダーを指定するだけで良いです。

PlaneOcclusion
Shader "Custom/PlaneOcclusion"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" }
        ZWrite On
        ZTest LEqual
        ColorMask 0

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(0, 0, 0, 0);
            }
            ENDCG
        }
    }
}

大したことはしていませんが、注目ポイントはPassの前の設定の部分です。

Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" }
ZWrite On
ZTest LEqual
ColorMask 0

まずQueueタグに関して

"Queue" = "Geometry-1"

Queueタグはレンダリングの順番を指定するためのタグです。Stack, Queueとかでよく出てくるQueueと同じで、値が小さいほど先に描画されます。半透明なオブジェクトを描くためには、不透明オブジェクトとの描画順を正しく指定してあげる必要があったりしますので、Queueはそういう時のために力を発揮します。Unityでは

Name Value
Background 1000
Geometry 2000
AlphaTest 2450
Transparent 3000
Overlay 4000

の順番に従って描画されます。今回はGeometry-1とすることでこのshaderをアタッチしたオブジェクトが通常のGeometryタグが指定されているオブジェクトよりも先に描画されると言うことがわかります。

ZWrite On
ZTest LEqual

ZWrite Onとはdepth bufferの更新を行うと言うことなので
また、Z値の比較判定はLess than equalの時(つまりよりカメラに近くある時)に成功とします。

こうすることで、例えば現実世界にある机が生成したバーチャルオブジェクトよりもカメラ側により近くにあるならば、机が描かれて、バーチャルオブジェクトは描かれなくなります。

ColorMask 0

これを指定することで色の出力を無効にすることができます。現実のオブジェクトに対して色付けなどしないので、より自然な描画となります。

結果

2.gif

plane detectionの精度が高くなく、段ボールの面よりも大きくplaneをマッピングしているために、不自然な描画になってしまいました。
しかし、今回目標としていたplane occlusionはできていることが分かります。

Next Step

  • AR People Occlusionを実現したい!

参考文献

最後に

間違いがあれば指摘していただけると嬉しく思います!

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

Unity for Mac 不具合と対処法メモ

・「Save Scene as」がない
→ サンプルシーンを開き、現在のシーンを保存するか聞かれるので、そこで保存

・上書き保存ができない
→ シーンをasset内に保存すると、保存できるようになる。

・Unityからスクリプトを開くと白紙になる。
→ ビジュアルスタジオから開いて、スクリプトを一旦デスクトップに出してから、上書き保存。

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

macOS CatalinaでUnityでマイク入力が取れない場合の対処方法

Catalinaにしたら何故かマイク入力から情報が取れない。
特にエラーが出ているわけではなく見かけ上は正常に動いているがデータが空っぽ。

全然理由が分からなくて諦めかけたが他のソフトでも似たような症状が出ていることを発見して対処方法を見つけた。

ターミナルからopenでUnityを開くとマイクアクセスのダイアログが出て許可することでマイク入力が取れるようになる。

open /Applications/Unity/Hub/Editor/2019.2.15f1/Unity.app

参考:Audacity under macOS 10.15 Catalina
https://forum.audacityteam.org/viewtopic.php?f=47&t=107162&p=378694

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

Unityでシューティングゲームを作る(3)

ここまでの進捗

  • 背景がループするようにした。
  • 普通の敵の動作を作成し、その敵が3秒ごとに生成される。
  • 瞬間移動する敵の動作を作成し、5秒ごとに生成する。
  • プレイヤーが画面の範囲外に行かないようにした。
  • 敵とプレイヤーが衝突したらプレイヤーが消滅する。
  • 分散攻撃の敵を実装する。

今後やること

  • タイトルシーンとエンディングシーンを追加する。
  • ボスキャラの動作を実装する。
  • エフェクトとBGMを追加する。
  • 様々な敵の出現方法を考える。

この記事で書くのは赤文字の部分
前回の最後に「次はボスかぁー」って書いたけどシーン遷移とかやりたかったから先にタイトルとエンディングをやる

タイトルシーンの追加

最終的にどんな感じになったかというと

コメント 2019-12-22 211531.png

シーン作成

File→New Sceneから新しいシーンを作成して名前を「Title Scene」とした
コメント 2019-12-22 232318.png

以下のようにオブジェクトを追加する

コメント 2019-12-22 232248.png

背景

background1,2は背景のオブジェクトで、画面をスクロールするために2つ作った。
コメント 2019-12-22 233630_LI.jpg
オブジェクト1と2で真ん中の赤い部分が中心。
背景オブジェクト1と背景オブジェクト2をY軸に対してマイナスの方向に移動させ、もし中心がある座標までいったらオブジェクト2の位置まで移動するようにした。
具体的なスクリプトとしては以下のようになる。

transform.Translate(0, -0.05f, 0);
if(transform.position.y <= -20.44f) {
    transform.position = new Vector3(0, 18.44f, 0);
        }

これはゲーム内の背景にも適用した。

タイトル名のTextとButton

タイトル画面にタイトル名のTextとゲームを開始するためのボタンを設置した。
これらをそのまま画面に出すのは面白くないので、アニメーションをつけた。
ButtonはScaleを0.3秒毎に変更し続けるようにして、Textのアニメーションは以下のサイトを参考にした。

参考:uGUIのTextで1文字単位のアニメーションを実装できる「Text Juice」紹介
参考:badawe/Text-Juicer

今回はこの中のY Modifierというアニメーションを使用した。

最後のNormalEnemyGeneratorは背景として敵を生成している。
完成したのが下になる。
ezgif.com-video-to-gif (1).gif

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

SpresenseをROSでUnityにつなぐ

今日の目的

SpresenseのL1Sでの測位結果をUnityで取得する。

SpresenseでL1S測位

Spresenseは、Sonyが出しているマイコンボードです。Arduinoや、Sony独自組み込みOS環境でプログラムできます。
特徴的な機能として、GPSがマイコンボードに組み込まれていて、さらに準天頂衛星みちびきのL1Sという補正信号を受信して、より精度の高い測位をすることができます。
(他にも特徴的な機能はあり、全体として非常に面白いマイコンボードですが、ここではGNSSについてだけ書きます。)
まずは、Arduino開発環境で、L1Sを取得してシリアルに送出するプログラムを作成します。

といっても、よくできたサンプルコードを公式が公開しているので、今回はこれを少しだけ変更して使います。

変更点は、以下の使う衛星の設定箇所です。

/* Set this parameter depending on your current region. */
static enum ParamSat satType =  eSatGpsQz1cQz1S;

これで、GPSとQZSSのL1C/AとL1Sを受信して測位するようになります。

Raspberry Pi Zero WH でシリアル受信して、ROSでメッセージ送出

上記のプログラムをArduino IDEからコンパイルして書き込むと、USBシリアルに測位関連のメッセージが流れてきます。

pi@raspberrypi:~/ros_catkin_ws $ cat /dev/ttyUSB0
SpGnss : begin in
SpGnss : begin out
SpGnss : start in
  mode = COLD_START
SpGnss : start out
Gnss setup OK
1980/01/06 00:00:01.000498, numSat: 0, No-Fix, No Position
1980/01/06 00:00:02.000519, numSat: 1, No-Fix, No Position
1980/01/06 00:00:03.000508, numSat: 1, No-Fix, No Position
1980/01/06 00:00:04.000498, numSat: 1, No-Fix, No Position

こんな感じです。
実際に使うときは、NMEAあたりのフォーマットで送るのがいいかなと思いますが、今回はこのままROS経由でUnityに持って行ってみます。

先回作成したROSのPublisherのプログラムを以下のように変えました。

ros_gnss_test_node.cpp
#include <stdio.h>
#include <stdlib.h>
#include "ros/ros.h"
#include "ros_gnss_test/MsgGNSS.h"

int main(int argc, char **argv)
{
  FILE *fp;
  char buffer[1024];

  fp = fopen("/dev/ttyUSB0", "r");
  if(fp == NULL) {
    printf("ERROR fopen");
    exit(0);
  }

  ros::init(argc, argv, "ros_gnss_test");
  ros::NodeHandle nh;

  ros::Publisher ros_gnss_test_pub = nh.advertise<ros_gnss_test::MsgGNSS>("ros_gnss_msg", 100);
  ros::Rate loop_rate(10);

  ros_gnss_test::MsgGNSS msg;

  while(ros::ok())
  {
    for(int i = 0; i< 1024; i++) buffer[i] = 0;
    if(fgets(buffer, 1024, fp) != NULL){
      msg.data = buffer;
      msg.stamp = ros::Time::now();

      ROS_INFO("send msg = %s", msg.data);

      ros_gnss_test_pub.publish(msg);

      loop_rate.sleep();
    }
  }

  fclose(fp);

  return 0;
}

UnityでROSメッセージを受け取る

前回のプロジェクトをそのまま使って、メッセージを受け取ってみました。

20191223_rosunity_spr.PNG

雑ですが、GPSの情報をUnityに持ってくることができました。
ちゃんと作ればいろいろ便利そうです。

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

AAAクオリティのエフェクトアセット「Magic Effects Pack 1」の使い方

この記事について

この記事はUnityアセット冬のアドベントカレンダー 2019 Winter!23日目の記事です。

Magic Effects Pack 1」とは?

アセットストアにてkripto289氏より販売されている、実写(AAA)クオリティ魔法のエフェクトがたくさん詰まったアセットです。
は他にもMesh EffectRealistic Effects Pack 34等の魔法系エフェクト以外にも、様々な美麗エフェクト等のアセットを販売しているので、気になった方は是非チェックしてみてください。

Magic Effects Pack 1

エフェクトのサンプル

C1C4DC5E-F27B-4235-822A-DFB7ED3F54A6.jpeg
火花を撒き散らす雷を飛ばしたり…
D267A8D3-4967-4E3E-BAC3-6AE49B76B2BC.jpeg
水の中に相手を閉じ込めたり…
F3645A55-E88D-4CD9-98DB-99F0BB36CEA5.jpeg
炎の盾で敵の弾を防いだり…

これらを含めた実に計33個ものエフェクトが詰まった大容量パックとなっております。

Magic Effects Pack 1」の特徴

本アセットの大きな特徴として、飛び道具等のエフェクトにはヒットエフェクトもついていることです。
それらも含めると実際のエフェクトの数は50程はあると思います!
かつ用意されているPrefabにはヒット時にそれらのエフェクトを発動させるスクリプトが搭載されているので、Prefabを置くことですぐに使えるのも特徴の一つです。
ほかのエフェクトや動画で見たい方はこちらからご覧下さい。

エフェクトの使い方

プロジェクトを作成し、本アセットをインポートした前提で進めます。

本アセットをインポートしたら、ProjectタブのAssetsフォルダ内に「KriptoFX」という名前のフォルダがあるはずなのでそれを開き、さらに「Realistic Effects Pack 1」というフォルダの中に、本アセットの内容物が入っています。
86F9889E-C1EA-4760-987A-BC89B7D26AE5.jpeg

エフェクトのPrefab名がエフェクト名ではなく、番号が振られているだけなので少々分かりにくいかもしれませんので、そんな時は「PC_DEMO」という名前のサンプルシーンを開けば、全てのエフェクトを番号ごとに見ることができます。
D7DE23CA-CAD8-4699-B668-A4129BBC8F8A.jpeg
使いたいエフェクトを見つけたら、「Prefab」フォルダ内の、「Character」フォルダを開けばキャラクターとセットのエフェクトが、
4BF4E9B0-7C54-4B20-BBF3-B8F06F76B5C0.jpeg
同じく「Prefab」フォルダ内の、「Effects」フォルダを開き「PC」フォルダを開けば単体エフェクトを取り出すことができます。
3B9A4120-C50D-4CEE-90C9-51291157D687.jpeg
また、エフェクトによっては親オブジェクトに「RFX1_Target」というスクリプトがついている場合があり、そのスクリプトの「Target」という変数にオブジェクトを入れると、そのエフェクトが「Target」に入れたオブジェクトに向かって飛ぶようになります!
5D50BF94-E8AE-4BF7-809C-48667193A6B1.jpeg
これらを上手く組み合わせて、貴方の作品に溶け込ませましょう!

最後に

kripto289氏が作成しているアセットはどれもが高品質でリアリティのあるものばかりなので、貴方が作成している(する)ゲームによっては合わないこともあるかもしれませんが、もしリアル味のあるゲームを作成している(する)のであればのアセットはどれもとても重宝することになると思います!
最後まで読んで頂き、ありがとうございました!

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

ひとつのスクリプトで多数のオブジェクト管理する

オブジェクトごとにスクリプトを生成する方法ではオブジェクト間(スクリプト間)での値のやり取りがとても面倒くさい。
そのため、ひとつのスクリプトで多数のオブジェクトを管理する方法を記載する。
まずいつも通り
- Crate Emptyで空のオブジェクト生成
- Input Field生成
- Button生成
を行う。Create EmptyしたGame Objectは名前をAdminにしておく(何でもよいが分かりやすくするため)。
singlescript_hierarchy.png

AdminオブジェクトでAdd ComponentしてNew Scritpを生成する(スクリプト名は今回はAdminScriptとする)。
以下のようにアタッチする。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// これ追加忘れずに
using UnityEngine.UI;
public class AdminScript : MonoBehaviour
{
    // publicにしないとダメ
    public InputField input_field_id;
    // 入力した文字列保存用
    private string input_txt;
    // input fieldでの入力判定
    public void InputID()
    {
        input_txt = this.input_field_id.text;
        print(input_txt);
    }
    // ボタンが押された時の処理
    public void LoginButtonPush()
    {
        if (input_txt == "abc")
        {
            print("正解です。");
        }
        else
        {
            print("不正解です。");
        }
    }
}

Adminオブジェクトのスクリプト内のメンバ変数input_filed_idにInputFIledオブジェクトを対応付けする。

singlescript_setinputfiled.png

インプットフィールドのオブジェクトを設定する。
singlescript_onendedit.png
singlescript_inputfieldfunctionsetting.png

ボタンのオブジェクトを設定する。
singlescript_onclick.png
singlescript_buttonfunctionsetting.png

これで実行してinputFieldにabcを入力してButtonを押すと"正解です。"と出力され、それ以外を入力した場合は"不正解です。"と出力される。
singlescript_result.png

このように、Adminオブジェクトに対応させたAdminScriptひとつでインプットフィールドとボタンのオブジェクトが操作できるようになる。

参考

https://www.sejuku.net/blog/83582

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

スクリプト内で別スクリプトの関数を実行する

オブジェクトAからオブジェクトBの値を持ってくるとか、実行する的なことができる。
Hierarchyで

"Create Empty"してオブジェクト名をGameObjectからAdminに変える(名前は何でもよい)

次に2D Object → "sprite"でNew Spriteを追加し、Assetsに画像を追加して

New Spriteに対応付けする。

admin_init.png

AdminとNew Spriteにそれぞれ別のスクリプトをAdd Componentする。

admin_scriptadd.png

New Spreteのスクリプトを以下のようにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpriteScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }
    // Update is called once per frame
    void Update()
    {

    }
    // public付けないとダメ
    public void PrintSpriteName()
    {
        print(this.gameObject.name);
    }
}

Adminのスクリプトを以下のようにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AdminScript : MonoBehaviour
{
    // public付けないとダメ
    public GameObject sprite_object;
    // Start is called before the first frame update
    void Start()
    {
        //sprinte_object<>
    }
    // Update is called once per frame
    void Update()
    {
        //test[0].GetComponent<CurrentSelected>().aaa();
        sprite_object.GetComponent<SpriteScript>().PrintSpriteName();
    }
}

すると、AdminのInspectorのAdminScriptコンポーネントにスクリプト内で宣言した

Sprite_object

が追加されるので、の⦿マークを押してNew Spriteを入れる(もしくはドラッグ&ドロップ)。

admin_checkbox.png

これで実行すれば、Adminスクリプト内でNewSpriteスクリプト内の関数を呼び出せる。

admin_result.png

値渡したい場合はset、getのプロパティ関数作ってやればよい。

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

【Unity学习笔记】跟随物体移动的UI的三种实现方案

游戏中跟随物体移动的的UI,大致可以分为以下三种

  1. 显示在画面的最上层,不会被其他游戏物体遮挡。这样的UI一般是属于系统的,例如HUD(抬头显示)中的锁定标志等。
  2. 参与游戏物体的远近关系,会被其他游戏物体所遮挡。这样的UI一般是属于跟随主的,可以称为Billboard,例如血条,名字等。
  3. 同2一样会被其他物体遮挡,但不同是的UI尺寸不会因为远近而变化,始终保持一致。

接下来来介绍一下3中UI在UGUI下的实现方法

1.显示在画面的最上层,不会被其他游戏物体遮挡。这里就用锁定标志来举例

1.gif

创建一个脚本AimMark.cs,装在实际AimMark的UI物体上
コメント 2019-12-22 213838.png
Unity提供了一个RectTransformUtility工具类,可以很方便的实现这个功能。
关键代码:

private void LateUpdate()
{
    // 将Target的世界坐标转先转换到屏幕坐标,再将其转换到父RectTransform内的局部坐标
    var screenPoint = RectTransformUtility.WorldToScreenPoint(MainCamera, Target.position);
    if (RectTransformUtility.ScreenPointToLocalPointInRectangle(ParentRectTransform, screenPoint, CanvasCamera, out var vector2))
    {
        transform.localPosition = vector2;
    }
}
  • 使用RectTransformUtility.WorldToScreenPoint把世界坐标转换到屏幕坐标,但这还不够,因为UI是隶属于Canvas下的RectTranform的,最终决定UI位置的其实是父RectTransform下的局部坐标。

  • 使用RectTransformUtility.ScreenPointToLocalPointInRectangle,将屏幕坐标转换为指定的父RectTranform下的局部坐标。其中CanvasCamera是隶属于的Canvas的RanderCamera,如果Canvas的渲染模式是Overlay,则这个值应为null,否则位置计算会出错。

AimMark.cs的完整代码

AimMark.cs
using UnityEngine;

// 锁定标志的控制类
public class AimMark : MonoBehaviour
{
    [SerializeField]
    private Animator animator;
    private Canvas ParentCanvas { get; set; }                   // 隶属于的Canvas
    private Camera CanvasCamera { get; set; }                   // Canvas的渲染相机
    private RectTransform ParentRectTransform { get; set; }     // 父节点的RectTransform    
    private Transform Target { get; set; }                      // 锁定的目标
    private Camera mainCamera;                                  // 游戏画面的主相机 
    private Camera MainCamera
    {
        get
        {
            if (mainCamera == null)
            {
                mainCamera = Camera.main;
            }
            return mainCamera;
        }
    }

    // 指定一个显示锁定标志的屏幕范围
    private Rect ViewProtRect { get; } = new Rect(0f, 0f, 1f, 1f);
    // 记录上一帧是否在范围内的状态,用于和当前状态对比得到转换的瞬间
    private bool IsInScreen { get; set; }

    public void Setup(Canvas rootCanvas, RectTransform rootRectTransform, Transform target)
    {
        ParentCanvas = rootCanvas;
        ParentRectTransform = rootRectTransform;
        CanvasCamera = ParentCanvas.worldCamera;
        SetAimTarget(target);
    }

    public void SetAimTarget(Transform target)
    {
        Target = target;
        IsInScreen = false;
    }

    public void PlayLockAnimation(bool isLock)
    {
        if (isLock)
            animator.SetTrigger("Lock");
        else
            animator.SetTrigger("UnLock");
    }

    private void LateUpdate()
    {
        if(Target == null)
        {
            Destroy(gameObject);
            return;
        }

        if (IsTargetInScreen())
        {
            if (!IsInScreen)
            {
                IsInScreen = true;
                PlayLockAnimation(true);
            }

            // 将Target的世界坐标转先转换到屏幕坐标,再将其转换到父RectTransform内的局部坐标
            var screenPoint = RectTransformUtility.WorldToScreenPoint(MainCamera, Target.position);
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(ParentRectTransform, screenPoint, CanvasCamera, out var vector2))
            {
                transform.localPosition = vector2;
            }
        }        
        else
        {
            if (IsInScreen)
            {
                IsInScreen = false;
                PlayLockAnimation(false);
            }
        }
    }

    // 判断锁定对象是否处于可视范围内
    private bool IsTargetInScreen()
    {
        // 转换目标的世界坐标至屏幕坐标
        var point = MainCamera.WorldToViewportPoint(Target.position);

        // 屏幕坐标的z值处于相机的进截面和远截面之间,并且处于屏幕范围内,即表示锁定对象在可视范围内
        var isInScreen = point.z > MainCamera.nearClipPlane && point.z < MainCamera.farClipPlane && ViewProtRect.Contains(point);

        return isInScreen;
    }
}

2.参与游戏物体的远近关系,会被其他游戏物体所遮挡。这里用血条和名字作为例子

2.gif

  • 在跟随主物体下建立一个Canvas,RanderMode设置为WolrdSpace

コメント 2019-12-22 220923.png コメント 2019-12-22 221258.png
这里要注意的是,从Canvas的Z轴正方向看去,Canvas上显示的UI是反向的,也就是说需要让Canvas的Z轴正方向和相机的Z轴正方向一致,UI才是正的

  • 接下来就是写一个脚本,控制Canvas的Z轴正向始终保持和相机的Z轴正向一致,即Canvas的旋转值和Camera的旋转值保持一致。

关键代码:

private void LateUpdate()
{
    // 令自身旋转值和相机的旋转值保持一致,使UI始终面向相机
    transform.rotation = MainCamera.transform.rotation;      
}

3. 和2一样会被物体遮挡,但UI在屏幕上的尺寸保持一致,不随距离而变化。

3.gif

实现方法前半和2相同,需要增加的是根据UI距离相机的垂直距离,来缩放UI,实现UI在屏幕上显示的尺寸保持不变。

原理图:
コメント 2019-12-22 205629.png
根据三角形相似的定理,其中需要求的缩放比例 = l/L = d/D。其中d为选定的参考距离,D为实际UI距离相机的垂直距离。

具体方法如下
1. 先通过2中实现的UI,调整相机到UI的距离,取一个认为合适的UI大小,将此时相机和UI间的垂直距离作为一个参考距离,记为d。
コメント 2019-12-22 231338.png
2. 在游戏运行中中取得D。因为UICanvas的旋转值和相机是一致的,所以UI到相机的垂直距离,就等于UI在相机坐标系内的Z值。要做的就是将UICanvas的世界坐标转换到相机坐标系下的局部坐标,然后取z值即可。Unity中也有非常方便在各种坐标系下转换的方法。

// 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离
var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position);

3。计算2中取得的z值和参考距离的比值,即为需要缩放的比例。

// 使用当前垂直距离比上参考距离,即可得出需要缩放的比例
var rate = posInCamera.z / baseDistance;

4。最后将3中计算的出的缩放值乘以原本的缩放值,即可得到最终缩放值。
把1到4整合成一个计算最终缩放值的函数。

// 根据Canvas相对于相机的垂直距离和参考距离的比,来计算出新的缩放比例
private Vector2 CalcScale()
{
    // 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离
    var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position);

    // 使用当前垂直距离比上参考距离,即可得出需要缩放的比例
    var rate = posInCamera.z / baseDistance;

    // 用原本的缩放比例乘以需要缩放的比例,得到最终缩放比例
    return baseScale * rate;
}

Billboard.cs的完整代码

Billboard.cs
using UnityEngine;

public class Billboard : MonoBehaviour
{
    [SerializeField]
    private bool isScaleSize;           // 是否根据距离来缩放大小
    [SerializeField]
    private float baseDistance = 10f;   // 给定距离相机的参考距离。在该距离下的UI大小是我们想要的

    private Camera mainCamera;          // 游戏主相机
    private Camera MainCamera           
    {
        get
        {
            if (mainCamera == null)
            {
                mainCamera = Camera.main;
            }
            return mainCamera;
        }
    }

    private Vector2 baseScale;          // 原本的缩放比例

    private void Start()
    {
        baseScale = transform.localScale;
    }

    private void LateUpdate()
    {
        // 令自身旋转值和相机的旋转值保持一致,使UI始终面向相机
        transform.rotation = MainCamera.transform.rotation;

        if (isScaleSize)
        {
            var scale = CalcScale();
            transform.localScale = new Vector3(scale.x, scale.y, 1);
        }
    }

    // 根据Canvas相对于相机的垂直距离和参考距离的比,来计算出新的缩放比例
    private Vector2 CalcScale()
    {
        // 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离
        var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position);

        // 使用当前垂直距离比上参考距离,即可得出需要缩放的比例
        var rate = posInCamera.z / baseDistance;

        // 用原本的缩放比例乘以需要缩放的比例,得到最终缩放比例
        return baseScale * rate;
    }
}

4.注意事项

可以看到,上述的位置和缩放的操作均在LateUpdate中进行,其目的是为了保证UI的位置和缩放的计算在相机和物体的位置计算之后进行。如果UI的位置和缩放计算先于相机和物体,就会发生UI渲染在上一帧时物体的位置上而导致UI错位和滞后感,在快速运动时极为明显。
4.gif
コメント 2019-12-22 235521.png

但仅使用LateUpdate还不能保证一定脚本的执行在相机和物体后,因为相机运动脚本也可能会使用LateUpdate,例如本方案中相机使用了Cinemachine就是默认使用LateUpdate,并且还指定了其脚本执行顺序在默认之后。
コメント 2019-12-22 235842.png

因此我们需要把AimMark和Billboard的执行顺序添加到CinemachineBrain的后面
コメント 2019-12-22 235926.png

保证UI的位置计算在物体和相机之后,无论如何运动也不会出现UI错位和滞后感了。
5.gif

总结

  1. 有一个公共的Canvas,将跟随主的世界坐标转换成需要该Canvas下的RectTransform坐标系下的局部坐标赋值给需要显示的UI局部坐标。
  2. 在跟随主物体下建立一个Canvas,把UI放入其中。在游戏执行中令Canvas的旋转值和Camera的保持一致。
  3. 在2的基础上,选定一个参考距离,计算出Canvas在相机坐标系下的局部坐标取得当前距离,利用相似三角形定理计算出缩放值。
  4. 需要保证UI的位置和缩放计算在物体和相机的位置计算之后。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでROS#で独自のメッセージをSubscribeする

目的

前回導入したROS#で、前々々回作成した独自のメッセージをUnityで受け取ってみる。
結論から言うと、メッセージを受け取ることはできたが問題ありなので、ここではトラブルシュートの過程を記載する。

SubScriberを作る

Unity側でのメッセージのクラスとSubscriberのクラスを作成する。
メッセージのクラスは、ROS#のEditor拡張から作ることもできる。便利。

MsgGNSS.cs
/*
This message class is generated automatically with 'SimpleMessageGenerator' of ROS#
*/

using Newtonsoft.Json;

using RosSharp.RosBridgeClient.MessageTypes.Std;

namespace RosSharp.RosBridgeClient.MessageTypes
{
    public class MsgGNSS : Message
    {
        [JsonIgnore]
        public const string RosMessageName = "/ros_gnss_test/MsgGNSS";

        public Time stamp;
        public String data;

        public MsgGNSS()
        {
            stamp = new Time();
            data = new String();
        }

        public MsgGNSS(Time stamp, String data)
        {
            this.stamp = stamp;
            this.data = data;
        }
    }
}
GNSSSubscriber.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using RosSharp.RosBridgeClient;
using RosSharp.RosBridgeClient.MessageTypes;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;

namespace RosSharp.RosBridgeClient
{
    public class GNSSSubscriber : UnitySubscriber<MsgGNSS>
    {
        public string messageData;

        protected override void Start()
        {
            Debug.Log("SubscribeStart");
            base.Start();
        }

        protected override void ReceiveMessage(MsgGNSS message)
        {
            Debug.Log("ReceiveMsg "+ message);
            messageData = message.data.ToString();
        }
    }
}

SubscriberをROSConnectorをアタッチしたGameObjectのアタッチして動かしてみたが、メッセージを受信している気配がない。
さて困った。

うまくいかないからパケットキャプチャで確認する

ラズパイのROS側のコンソールログを見てみると、

2019-12-22 14:32:34+0000 [-] [INFO] [1577025154.734286]: [Client 20] Subscribed to /ros_gnss_msg
2019-12-22 14:32:39+0000 [-] [INFO] [1577025159.331651]: [Client 20] Unsubscribed from /ros_gnss_msg

ちゃんとSubscribeできているみたい。
しかたがないので、WireSharkでパケットキャプチャしてみたところ、それらしきWebSocketのパケットは送受信されている。
つまり、ROS#の側まではデータがきているが、自前のSubscriberまで来ていないということに。

C#のソースコードの形でライブラリを導入してデバッグする

Asset Storeや、UnityPackageでの導入だとROS#はRosBridgeClientをdllとして持っていてそれを使う。そのため、そもそもデバッグがしづらい。
そのため、リポジトリをcloneして、DLLの代わりにRosBridgeClientのソースから導入してみる。
ほぼそのまま動くようになったので、Visual Studioでブレークポイントはったり、Debug.Logで値を出力しながらデバッグしていく。

判明した原因

独自メッセージの中の、RosSharp.RosBridgeClient.MessageTypes.St.StringがうまくJSONから変換できていない。Execption出しているのだが、握りつぶされてすごくわかりにくい状態になっていた。

ライブラリ側に手を入れて、むりやりメッセージを受け取る

一旦正攻法での解決は後回しにして、ライブラリ側に手を入れて、メッセージをなんとか無理やり受け取れるようにしてみる。

RosSocket.cs内の、メッセージをディスパッチしているところに、どうせ他のメッセージこないからと、以下のようなコードを入れてみる。

                case "publish":
                    {
                        string topic = jObject.GetValue("topic").ToString();
                        foreach (Subscriber subscriber in SubscribersOf(topic))
                        {
                            //subscriber.Receive(jObject.GetValue("msg"));
                            GNSSMsgBind.sec = uint.Parse((string)jObject["msg"]["stamp"]["secs"]);
                            GNSSMsgBind.nsec = uint.Parse((string)jObject["msg"]["stamp"]["nsecs"]);
                            GNSSMsgBind.data = (string)jObject["msg"]["data"];
                        }
                        return;
                    }

我ながらひどいやりかただ。もちろん同時に以下のようなこれまたひどい設計も何もあったもんじゃないクラスを作っておく。

GNSSMsgBind.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GNSSMsgBind
{
    public static string data;
    public static uint sec;
    public static uint nsec;
}

さらに、表示用として、以下のコードを作成して、GameObjectにアタッチしておく。

GNSSSubscDisplay.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GNSSSubscDisplay : MonoBehaviour
{
    public string data;

    // Update is called once per frame
    void Update()
    {
        data = GNSSMsgBind.data;
    }
}

なかなかひどいやり方だが、メインスレッドとは別スレッドなので、直接呼出しがしにくく、一番手っ取り早い思いついた方法がこれだった。

20191223_rosunity.PNG

ちゃんとラズパイのROSからのメッセージが受信できている。

何が問題か分かったが、納得いかないので今後の課題

この解決方法ではさすがに厳しいので、デシリアライズをちゃんとすることを考えなければいけないが、この確認のためにかなり時間を取ってしまったので、いったんここでおしまい。

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

Unityプログラマにオススメの新しいC#の機能

Unityでも新しいC#!

長い歴史を持つプログラミング言語、C#。C#は着実に進化し、便利な言語機能を追加してきました。ところがゲームエンジンUnityでは少し前まで、古いC#しか使うことができませんでした。

そんなUnityも、現在は特に工夫をせずに比較的新しいC#を使うことができます。(投稿執筆時の最新C#は8.0、最新Unity 2019.2ではC# 7.3を利用可能です。)

ところで、Unityプログラマの方の中には「こんなC#の機能があるのか!」と驚く人や、「新しいC#の機能、わからない」と困っている人もいるのではないでしょうか?

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介します。

プロパティの書き方いろいろ

次のコードはUnityでよく使うプロパティの例です。

ゲッターオンリーのプロパティで、SerializeFieldがついたフィールドをバッキングフィールドとしてもっています。

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

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのゲッターオンリーのプロパティ
    public int Hp { get { return hp; }}
}

新しいC#では次のように、=>を使って短く書けます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#では短く書けるゲッターオンリーのプロパティ
    public int Hp => hp;
}

冗長な部分のコードがなくなり、コードが短く簡潔になったことに注目してください。


次のコードは、古いC#におけるセッター・ゲッター両方をもつプロパティの例です。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get { return hp; }
        set { hp = value; }
    }
}

これらも=>を使って冗長な部分を取り除き、簡潔に記述することができます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get => hp;
        set => hp = value;
    }
}

C#にはもともと自動実装プロパティという機能がありました。
自動実装プロパティは、バッキングフィールドを自分で書かなくてよいプロパティです。

using System;

public class Player
{
    // 自動実装プロパティ
    public int Name { get; private set; }

    public Player (string name) {
        this.Name = name;
    }
}

古いC#では自動実装プロパティが使えない場面がいくつかありました。新しいC#では、自動実装プロパティが使える場面が増えています。


次のコードではreadonlyなフィールドをバッキングフィールドとしてもつNameプロパティです。
古いC#では「コンストラクタで値 or 参照を設定しそれを書き換えない」というプロパティを実現するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では、readonlyのために自動実装プロパティでなく
    // バッキングフィールドを使う
    private readonly string name;
    public string Name { get { return name; } }

    public Player(string name)
    {
        this.name = name;
    }
}

新しいC#では、このようにreadonlyなプロパティを自動実装プロパティのみで簡潔に実現できます。

public class Player
{
    // 新しいC#では、readonlyの自動実装プロパティが使える
    public string Name { get; }

    public Player(string name)
    {
        Name = name;
    }
}

次のコードは、バッキングフィールドに初期値をフィールド初期化子で設定しているプロパティです。
古いC#ではプロパティの初期値を設定するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では初期値を設定するために、バッキングフィールドを使う
    // 自動実装プロパティは使えない
    private  string name = "No Name";
    public string Name {
        get { return name; }
        set { name = value; }
    }
}

新しいC#では、初期値の設定とともに自動実装プロパティが使える。

public class Player
{
    // 新しいC#では初期値の設定とともに
    // 自動実装プロパティを使える
    public string Name { get; set; } = "No Name";
}

新しく加わった機能は便利機能ばかりですが、注意しないといけない機能もあります。

新しいC#では、自動実装プロパティのバッキングフィールドに属性をつけられるようになりました。この機能を使い、SerializeFieldをプロパティのバッキングフィールドに付けたくなります。

残念ながらこれは期待する挙動になりません。(フィールドの名前が変 or インスペクターに出てこない)

「自動実装プロパティのバッキングフィールドに属性付与」と「SerializeField」は合わせて使わないようにしてください。

[Serializable]
public class Monster
{

    // Unityでは使ってはいけない
    [field:SerializeField]
    public int Hp { get; }
}

新しいプロパティは、コードの設計が劇的に変わるわけではありませんが、コードが簡潔になります。ぜひ試してみてください。

複数の値を返したい時・まとめたい時はValueTuple

メソッドで複数の値を返したい時、どうすればいいでしょうか?クラスか構造体を作ればいいでしょうか?

ValueTupleは、クラスや構造体などの型を定義しなくても、複数の値をまとめることができるデータ型です。これを使えば、メソッドで複数の値を簡単に返すことができます。

ToStringや、HashCode、Equals、==での比較も実装されており、データ処理時にとても活躍します。

新しいC#では、ぜひValueTupleを使ってみてください。


ValueTupleは、非常に扱いやすい形で複数の値をまとめることができる構造体です。

ValueTupleは、つぎのように()や要素名を記述し、生成することができます。(これ以外の書き方も存在します)

var person0 = (name: "Ryota", level: 31);

上で作ったValueTupleには、namelevelというメンバがあります。

Debug.Log($"{person0.name} {person0.level}");

メソッドの返値型としてValueTupleを使う時は、このように書きます。

public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

ToStringやHashCode、Equalsや==も実装されています。

var person0 = (name: "Ryota", level: 31);
var person1 = (name: "Ryosuke", level: 30);

Debug.Log(person0 == person1);
Debug.Log(person0.name);
Debug.Log(person0.level);
Debug.Log(person0.ToString());

ValueTupleを扱う際分解を使うと、非常に簡潔にかけます。

// ValueTupleを返すLoadNameAndLevel
public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

public static void Main(string[] args)
{
    // 分解で返値を受け取る
    // stringのnameとintのlevel
    var (name, level) = LoadNameAndLevel();
}

今までの古いC#でも、匿名型という便利な言語機能がありました
匿名型もクラスや構造体を定義しなくても、名前のない型を作れる機能です。
詳しくはこちら「C#の匿名型について調べてみた」。
匿名型は、LINQやRxなどの処理の中間データとしては非常に便利だったのですが、メソッドの返り値型にできませんでした。
ValueTupleはメソッドの返値型にできます。

また、ValueTuple構造体よりも前、クラス型のTupleがありました。
Tupleを使えば複数の値をまとめることはできました。
しかし、メンバの名前がItem1やItem2となっていること、構造体ではなくクラスであったことなど、あまり使い安くありませんでした。


ダメージ計算・特典計算などのロジックにおいて、

「privateメソッドで複数の値をまとめて返したい。しかし型を作るほどではない」

という場面があると思います。

そのような時は、ぜひValueTupleを活用してください。

※ ValueTupleは便利ですが、型を作るべき場面もあります。使いすぎに注意してください。
※ ValueTupleを活用したライブラリ、ImportedLinqもみてみてください。

アセンブリを意識したい時のinternalとprivate protected

今までのC#のアクセスレベルは次のものがありました。

  • private
  • protected
  • internal
  • protected internal
  • public

それに加えて新しいC#では、

  • private protected

が加わりました。


UnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

今までのUnityにおけるアクセスレベルでは、次の3個を使うことが多かったです。

  • private
  • protected
  • public

Assembly Definition Filesにより、Unityでも簡単にアセンブリを分割できるようになりました。これにより、「アセンブリ内に閉じる」ということが大事になりました。

internalアクセス修飾子を使えば、同一アセンブリ内のみにアクセスを制限できるようになりました。Assembly Definition Filesとともに活用してください。

また、protected internalは「同一アセンブリ」もしくは「その型とその派生型」のどちらかであればアクセスできるアクセスレベルです。

新しく加わったprivate protectedは「同一アセンブリ」かつ「その型とその派生型」がアクセスできるアクセスレベルです。


新しいUnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

そこで、internalアクセスレベルとprivate protectedアクセスレベルを活用してください。

合わせて、「C#のアクセス修飾子 2019 〜protectedは 結構でかい〜」も参照してください。

nullの扱いもやりやすく

「null参照の発明は10億ドルにも相当する誤りだった」という言葉もありますが、C#にはnullがあります。nullと上手につきあっていかないといけません。

新しいC#では、そんなnullを上手に扱える記法が追加されています。


次のようなMonsterクラスとPlayerクラスがあります。

public class Monster
{
    public string Name { get; set; }
}

public class Player
{
    public Monster Target { get; set; }
}

MonsterのNameプロパティもPlayerのTargetプロパティもnullになりえます。

そこで次のように三項演算子とnull判定を使って、次のようなコードを書く必要があります。

本当にやりたいことは、メンバへのアクセスだけなのに、非常に冗長です。

// 古いC#では冗長
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : null;

新しいC#ではこのように?.を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name;

「もし対象がnullだったら指定した既定の値を設定したい」という状況があると思います。

古いC#では次のような書き方をする必要がありました。

// 古いC#の書き方
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : "Default Target Name";

新しいC#ではこのように??を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name ?? "Default Target Name";

内部的な話をすると、「player?.Target」と「player == null ? null : player.Target」は等価ではありません。==をその型が実装している時は注意してください。?.??を使う場合、==は呼ばれません。

?.??は非常に便利ですが、UnityのGameObjectやMonoBehaviourの中で使うには注意が必要です。

Unityにおいて、GameObjectやコンポーネントでは、?.??には注意が必要です。GameObjectやコンポーネントでは==が実装されています。

?.??を使った際に、何が起こるか考えてみてください。

進化したSwitch

プログラミング言語C#を学び始めた時、ほとんど全ての人はswitchを勉強したと思います。

新しいC#では、switchはとても強化されています。


今までのC#でのswitch文では、列挙型の値、数値の値、文字列の値で分岐するだけでした。

例えば次のコードのようにです。

public enum Shape
{
    Circle,
    Triangle,
    Polygon
}
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Shape.Circle:
            Debug.Log("Circleだよ");
            break;
        case Shape.Triangle:
            Debug.Log("Triangleだよ");
            break;
        case Shape.Polygon:
            Debug.Log("Polygonだよ");
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(shape), shape, "Un expected shape.");
    }
}

新しいC#では型で分岐できるようになりました。次のようなことができるようになったのです。

// objはどんな型がくるかわからない
public static void SwitchExample0(object obj)
{
    switch (obj)
    {
        case int n when n < 0:
            Debug.Log("負の数だよ!");
            break;
        case 7:
            Debug.Log("ラッキーセブンだよ!");
            break;
        case int n:
            Debug.Log($"整数だよ! {n}");
            break;
        case string s:
            Debug.Log($"文字列だよ : {s}");
            break;
        case null:
            Debug.Log("nullだよ");
            break;
        default:
            Debug.Log("それ意外だよ");
            break;
    }
}

より具体的で実用的なコードだとこのようなことができるようになりました。

public abstract class Shape
{
    public abstract double Area { get; }
}

public class Rect : Shape
{
    public int Height { get; set; }
    public int Width { get; set; }
    public override double Area => Width * Height;
}

public class Circle : Shape
{
    public int Radius { get; set; }
    public override double Area => Radius * Radius * Math.PI;
}

Shape型を継承したRect型とCircle型があります。これとswitchを使って、次のようなコードを書くことができます。

// 抽象型のShape。列挙型じゃないよ!
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Rect r when r.Width == r.Height:
            Debug.Log($"正方形だよ! 面積: {r.Area}");
            break;
        case Rect r:
            Debug.Log($"長方形だよ! 面積 : {r.Area}");
            break;
        case Circle c:
            Debug.Log($"円だよ! {c.Area}");
            break;
    }
}

ダメージ計算やポイント計算で活用できそうですね!


switchはC# 7.3のさらに先、C# 8.0でさらに進化しています。また今後のC#でさらに強くなっていくでしょう。

ダメージ計算、特定計算などで活躍すること間違いなしです。今後の強化にも期待しましょう。

構造体をより効率よく扱う

C#は、Unityそして.NET Coreの躍進により、よりいろいろな領域で活躍するようになりました。

領域が広がったことにより、パフォーマンスを求められることも増えてきました。

新しいC#では、パフォーマンス改善で活躍する多くの機能が追加されました。一例をあげると、

  • 参照ローカル変数
  • 参照戻り値
  • 読み取り専用参照
  • readonly 構造体
  • ref 構造体

などです。

これらの機能に関して、neueccさんのUnite 2019の公演、「Understanding C# Struct All Things」というとても素晴らしい公演を参照してください。

まとめ

C#は着実に進化し便利な言語機能を追加してきました。
今までUnityでは古いC#しか使えませんでしたが、最近新しいC#が使えるようになりました。
Unityプログラマの方に使って欲しい新しいC#の機能がたくさんあります!

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介しました。

この投稿で紹介していない、便利な新しいC#の機能もたくさんあります。
次の公式ドキュメントや、ufcppさんのとてもわかりやすいブログでぜひ調べてみてください。

MSDN

ufcppさんのC# によるプログラミング入門

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