20190909のUnityに関する記事は8件です。

UnityでLWRP+PostProcessingStackをVRで使う際の注意点

はじめに

こんにちは、最近ゲームエフェクトの勉強をしているZeniZeniです。
ゲームエフェクトの見栄えをよくするためにはPostProcessは非常に便利なんですが、LWRP(Light Weight Render Pipeline)と併用して、かつVRでやりたい!となると、かなりつまづくところが多いです。
この記事では、LWRP+PostProcessingStackをVRで使う場合の設定方法と注意点を説明します。

動作環境

まず、現行はどのバージョンでもAndroidではLWRP+PostProcessingStackをVRで使うことはできないようなので、OculusQuestでやりたいと思っている方々はUnityの正式対応を待ちましょう。
その他のPCVRなども、Unity2019以上でないと動作はしません。

たとえsceneビューや(実行前の)Gameビューでポストエフェクトがかかっていたとしても、実行中にVR画面から見た映像にはポストエフェクトはかかりません。VRではない(CameraのTarget EyeがNoneになっている)カメラならちゃんと映ります。

またUnity2018以下のLWRP対応したプロジェクトを、Unity2019.1以上のバージョンにアップグレードすると、大量のエラーが発生しどうしようもなかったので、おとなしくUnity2019.xで新規に作成するのが良いと思います。

今回は

  • Unity 2019.1.14f1
  • Lightweight RP version 5.7.2

でやっていきます。

初期設定

まず新規プロジェクトを作成します。versionは2019.1.14f1。TemplateはVR Lightweight RPです。

そして最初に開かれる下の画像のようなSampleSceneでは、もうすでにLWRP+PostProcessingStackをVRで見ることができます。
Post Process VolumeのBloomの値を変えたりして試してみてください。

注意点

Color Gradingがうまくいかない

ポストエフェクトのColor Gradingは明るさと色調の調整を行うエフェクトですが、HDR(High Dinamic Range)設定をちゃんとしてやらないとうまく動作しません。詳しい説明は【Unite 2017 Tokyo】ゲームの見た目も盛ったら変わる!!!!ヤバい!!ポストプロセス!!入門!!!!!!!!! を見てみてください。

まずEdit>Project Settings>GraphicsScriptable Render Pipeline Settingsを確認しましょう。

そこで設定されているLWRP assetを選択します。
そこのQualityのHDRのチェックボックスを付けておかないと、HDRが動作しません。
ここでは影の細かい設定なんかも行えます。

またCameraのHDR設定もOffになっていると使えないので、Use Pipeline Settingsにしておきましょう。

一部動作しないエフェクトがある

こちらのPost Processing in the Lightweight Render Pipelineで説明されていますが、以下のエフェクトはLWRP環境では使えません。

  • Motion Blurや一時的なアンチエイリアスを含むモーションベクトルベースのエフェクト
  • Screen Space Reflectionsといった高価なレンダリング計算が必要なエフェクト
  • ターゲットハードウェアがCompute Shaderをサポートしていない場合は Auto-exposureやAmbient Occlusion (MSVO)、Debug Monitorsといったエフェクト

VR環境ではVignetteエフェクトをつけるとよい

上述したPost Processing in the Lightweight Render Pipelineで、VRアプリでは、特定のポストエフェクトが吐き気と見当識障害を引き起こす可能性があります。 テンポの速いゲームまたは高速ゲームで乗り物酔いを軽減するために、UnityではVRにVignette効果を使用することをお勧めします。と説明されています。
Vignetteとはなんぞやと思うかもしれませんが、要は視界の縁まわりを少し暗くするエフェクトのことです。Unityのドキュメントにわかりやすい説明が書いてあります。

その他Tips

LWRP対応していないアセットを一括でLWRP対応させる。

LWRPに対応していない(LWRPのShaderを使っていない)マテリアルは真っピンクで表示されてしまいます。これはEdit>Render Pipeline>Upgrade Project Materials To LightweightRP Materialsで一括でいい感じに変更してくれます。

開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。

  1. Unity Blog 2019.1リリース
  2. Post Processing in the Lightweight Render Pipeline
  3. テラシュールブログ -【Unity】既存のプロジェクトにLightweight RenderPipelineを導入する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity玉転がしをスマホにビルド

はじめに

Unityの玉転がしをスマホに入れる。
Unity Hubを使ってUnityを入れているとAndroidのビルドがすごく簡単。
Unity Hubをダウンロード:https://unity3d.com/jp/get-unity/download
iOSのビルドもしますが基本的にMacが必要なので注意。
また、私はAndroidなのでiOSの説明は雑になります注意。

スマホ操作用にスクリプトの書き換えもします。
完成したUnityチュートリアルの玉転がしを書き換える。
玉転がし:https://unity3d.com/jp/learn/tutorials/s/roll-ball-tutorial

Unityバージョンは2019.2.1f1
Unity Hubは 2.1.0

準備

Unity Hubを開いてインストール>3つの点>モジュールを加える。

Androidなら
Android Build Support
Android SDK & NDK Tools
OpenJDKの3つを選択して実行。

iOSならiOS Build Supportを選択して実行。

何か聞かれるかもしれないけど同意してインストール。

インストールが終わったら準備ok。
プロジェクトを開く。

スクリプト書き換え

キーボードで操作していた部分をスマホでも操作できるようにする。
だいたいUpdate()の中身を書き換える。

PlayerController.cs
public class PlayerController : MonoBehaviour
{
    //重力加速度
    const float GRAVITY = 9.81f;    // 追加

    public float speed;
    public Text scoreText;
    public Text winText;

    private Rigidbody rb;
    private int score;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        score = 0;
        SetCountText();
        winText.text = "";
    }

    void Update()
    {
        Vector3 vec = new Vector3();   // 追加

        //エディターか実機か判定する
        if (Application.isEditor)
        {
            // こちらが元々の操作
            // ここに元々Update()に入っていたコードを移す
            var moveHorizontal = Input.GetAxis("Horizontal");
            var moveVertical = Input.GetAxis("Vertical");
            var movement = new Vector3(moveHorizontal, 0, moveVertical);
            rb.AddForce(movement * speed);
        }
        else
        {
            // こちらがスマホでの操作
            vec.x = Input.acceleration.x;
            vec.z = Input.acceleration.y;
            vec.y = Input.acceleration.z;

            //シーンの重力を入力ベクトルの方向に合わせて変化させる
            Physics.gravity = GRAVITY * vec.normalized * speed/10;
        }
    }

キーボードでの操作から端末の傾きでの操作に変える。

タッチでの操作。

if (Input.GetTouch(0).phase == TouchPhase.Ended)
{
    // タッチで行いたい処理、ジャンプとか
}

これでスマホで操作できるはず。

設定

メニューからFile>Build Settingsを開く。
使うシーンを入れる

Scenes In Buildに使うシーンをAssetsからドラッグドロップ。
シーンが複数あるなら最初に来て欲しいシーンを一番上にする。
玉転がしならシーンはひとつのはず。

プラットフォームを切り替える

iOSAndroidを選択してSwitch Platform
現在選んでいるプラットフォームにはUnityアイコンがついている。

Player Settings
Player Settingsを開く。

Company NameProduct Nameを適当に変える。

変えないとビルドに失敗する。

◆Resolution and Presentation>Orientation>Default OrientationLandscape Rightにする。
スマホの向きを横向きで固定している。
他の向きにしたり傾き検知で回転もできる。

◆Other Settings>Identification>PackageNameを一応確認する。iOSはBundle Identifier
先ほど変えたCompany NameとProduct Nameの組み合わせになっていれば○
例:com.watasi.ball

 
これで設定ok。
ここからAndroidとiOSで分かれる。

Android

端末側の設定を変える。

◆ 設定>端末情報>ソフトウェア情報 からビルド番号を7回タップし開発者モードにする。
◆ 設定>開発者向けオプション からUSBデバッグをオンにする。
◆ 端末とPCをデータ通信が可能なケーブルでつなぐ。

これで端末側の準備はok。

ケーブルを繋いだらBuild SettingsBuild And Runをクリック。

適当に名前をつけSave
保存場所もそのままで良い。

これで自身のスマホにゲームをビルドできたはず。
できなかった人は解決策をまとめるので見てみて。

iOS

Macがないとダメよ?
Xcodeが必要になるのでApp Storeからインストールする。

とりあえずBuild SettingsBuild And Runをクリック。

適当に名前をつけSave
保存場所もそのままで良い。

ビルド後にXcodeが開くはず。

左の一番上(Unity-iPhone)をクリック。
タブのGeneralを選択。
Teamを選んでAdd An Account、自身のApple IDでログイン、戻る。
またTeamを選んで先ほど追加したアカウントを選択。

端末とPCをケーブルで繋ぎ、上側の~~ Generic iOS Deviceから自身の端末を選ぶ。

再生マークのボタンを押す。

端末側でMacと制作者を信用する。

ビルド成功でok。

エラーが起きたら

体験したビルドの失敗と解決策を書いておく。

Androidエラー

No Android devices connected

◆端末をPCと繋がずにビルドした
繋ぐ。

◆ケーブルがデータ通信に対応していない
ケーブルがデータ通信に対応している場合、PCと繋いだ時端末側のステータスバーに「通常充電」の他に、「USBをファイル転送に使用」という項目が出てくるはず。

◆端末側でUSBデバッグがオンになっていない
上のAndroidを見てもう一度。

◆逆に繋いでいないのにこのエラーが出ない
プラットフォームがAndroidに切り替わってないかも。

CommandInvokationFailure: Unable to install APK to device. Please make sure the Android SDK is installed and is properly configured in the Editor. See the Console for more details.

実際に体験していないエラーなので解決策だけをつらつらと。
◆タスクマネージャーでadb.exeのプロセスを終了する
よく分からないけどこれでビルドが通るようになるらしい。

◆Package Nameを変える
Player SettingsCompany NameProduct Nameを変え、Package Nameが変わっていることを確認したはず。
それでもダメならさらにPackage Nameを変えてみる。

◆保存したビルドのパスに日本語を入れない
C:aaa/bbb/ccc/ddd.apk
ビルドすると拡張子がapkで保存されると思いますが、そこまでのパスに日本語が入っているとダメだそうです。

iOSエラー

なんのエラーがあったかふわっと忘れました。

https://qiita.com/segur/items/bef54efa7764885173bb
この方の「ケース:プライバシー情報がロックされてしまった」の症状でした。

おわり

だんだんと雑になっていく記事になりました。
大変ですがスマホに入れられるとわくわくしますね。

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

【Unity(C#)】自作VIVEコンテンツでスタート位置を気にしなくて済む実装

VIVEのリセンター機能

Standing ModeSeated ModeにはSteamVRが用意している機能で
リセンター(再トラッキングしてポジションを中央に戻す)が可能ですが、Room-Scaleでのプレイには対応していません。

なので、VIVEを担いで営業デモに行った際にはスタート位置をバミるという手法を使っていました。

バミらないと、いきなりポリゴンにめり込んだ状態から始まったり、
明後日の方向を向いてしまって、後ろからしゃべりかけられてストーリーが展開する...なんてことが起きます。

位置と向きを補正

しかし、バミリは複数のコンテンツを体験していただく際には厄介です。(あれがこっち、これはあっちと余計なことに気を使います)
さらに、コンテンツの内容、作り方によってはスタート位置がまちまちになってしまうので、
部屋の大きさによっては、
・コンテンツAでは前方のスペースに十分なエリアが必要
・コンテンツBでは後方のスペースに十分なエリアが必要
といった具合に、それぞれのコンテンツで必要なエリアの幅が異なることで正しく体験できないという現象が起きかねません。

また、私は製作者側なのでデモ時に上記のような状況でも何をどう対処すればいいか瞬時に判断できますが、
コンテンツを使うユーザーからしたら意味不明で、ストレスでしかありません。

なので、コンテンツにリセンター機能を実装して組み込むことで、開始位置の固定を取り払いました。

コード

やっていることはシンプルで、
スタートと同時にコルーチン内でCameraRigの角度を回転させてCamera(プレイヤー)の向きを補正し、
その後にCameraRigの位置にCamera(プレイヤー)のトラッキング位置を考慮した補正をかけています。

using UnityEngine;
using System.Collections;
using UnityEngine.Events;
/// <summary>
/// Recenter your camera when you start VR. Attach to CameraRig.
/// </summary>
public class ReCenterCamera : MonoBehaviour
{
    [SerializeField, Tooltip("Set this child camera")]
    GameObject eyeCamera;

    [SerializeField,Tooltip("This event is skiped one frame")]
    UnityEvent startEvent;

    void Start()
    {
        StartCoroutine(ReCenterCoroutine());
    }

    IEnumerator ReCenterCoroutine()
    {
        Vector3 cameraRig_Angles = this.gameObject.transform.eulerAngles;
        Vector3 eyeCamera_Angles = eyeCamera.transform.eulerAngles;

        this.gameObject.transform.eulerAngles += new Vector3(0, cameraRig_Angles.y  - eyeCamera_Angles.y, 0);

        Vector3 cameraRig_StartPos = this.gameObject.transform.position;
        Vector3 eyeCamera_Pos = eyeCamera.transform.position;

        this.gameObject.transform.position += new Vector3(cameraRig_StartPos.x - eyeCamera_Pos.x, 0, cameraRig_StartPos.z - eyeCamera_Pos.z);

        yield return null;
        startEvent.Invoke();
    }
}

位置の補正に関しては前回の記事の内容をそのまま転用しています。

今回苦労したのは角度に関してです。

角度の調整

図解します。

黒い四角がカメラ(プレーヤー)、青い四角がCameraRigだとします。
それぞれのY軸回りのローテーションはy₁ , y₂です。
カメラ(プレーヤー)をVR起動時に、必ずA地点の方に向けた状態でスタートしたいという想定です。

シンプルな計算で、実装できます。
カメラ(プレーヤー)とCameraRigのY軸回りのローテーションの差を計算して、CameraRigのY軸回りのローテーションに足すだけです。

今回のプログラムでは加算代入によってy₂+y₂-y₁の部分を実装しています。

this.gameObject.transform.eulerAngles += new Vector3(0, cameraRig_Angles.y  - eyeCamera_Angles.y, 0);

なぜコルーチンなのか

なぜ、コルーチンにしているのかにも、きちんとした理由があり、今回苦労した原因です。

その原因とはトラッキングが完了してから補正する必要があるということです。
自分の今いる座標、回転座標が定まっていない(トラッキングが完了していない)状態で補正をかけても
意味がありませんよね。

なので、
①Updateでトラッキングの処理が終わる
②Updateより後に実行されるコルーチン内で位置、角度の補正
という流れになっているということです。

※コルーチンのUnityの実行順についてはこちらで図解してあります。

この実装の欠点

この実装にはいくつか不具合の元がありまして、既存のコンテンツに後入れで導入した場合、バグを生む可能性があります。

例えば、カメラの座標がワールド座標の○○を超えたらイベント発生、などの実装があった場合、
最初の1フレームでそのイベントが発生してしまう恐れがあります。

なぜそんなことが起きる可能性があるかというと、最初の1フレームをトラッキングに使用するので、
実際に目に見えて始まる位置(補正後の位置)とは異なる座標を1フレーム分通過してしまうからです。

なので、最初の1フレームの間、そのイベントのトリガーはオフにしておいて、
位置補正終了と同時にオンにするなどの工夫が必要です。
今回のコードにもイベントを登録できる形で組み込んでいます。

        yield return null;
        startEvent.Invoke(); //ここにトリガーをオンにするイベントを登録

また、トラッキングがMissingになった状態でUpdateの最初の1フレームを通過してしまうと
補正が正しくかからない等の問題点も残しており、まだまだ改善の余地はありそうです。

あと今回の内容はOculusコンテンツに導入しても正しく動きます。
むしろ、最初はOculus向けに作ってた機能で、VIVEにつっこんだらなんか動いたからヨシ!って感じです。

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

Unityでオンラインマルチプレイなゲームを作りたい その8 プレイヤーの同期

前回の記事でゲーム開始前の準備を行うところまで作りました。
今回からはようやくゲーム部分に取り掛かっていきます。

今回からの目標


赤い〇で囲っている部分を実装していきます。
その3の最終目標で書いてる通り、ローグライク * アクションな2Dのゲームを作っていきます。

プレイヤーの同期

とりあえず、最初は自分が操作するプレイヤーとその同期を実装していきます。

実装内容としては、

  1. プレイヤーオブジェクトの生成
  2. 移動の処理の同期
  3. 弾の発射と同期
  4. パラメータの同期
  5. プレイヤーオブジェクトの削除

の5つになります。

1.プレイヤーオブジェクトの生成

まず、操作するプレイヤーのプレハブを作っていきます。

Hierarchyで空のオブジェクトを生成し、わかりやすいような名前に変更します。(図ではPlayerという名前にしてます)

作成した空のオブジェクトに同期を行なうための機能を追加します。

MonobitViewコンポーネント

ネットワーク越しでオブジェクトの通信/同期を行なうために必要な機能です。

MonobitTransformViewコンポーネント

空のオブジェクトに元からついているTransform(主にPosition, Rotate, Scale)をネットワーク越しで同期を行なってくれるコンポーネントです。
これを追加することで、位置, 向き, 大きさの同期を行なってくれます。

この二つを先ほど作ったオブジェクトにAdd Componentして設定していきます。


こんな感じですね。 次に、追加したMonovitViewコンポーネントの設定を行います。
MonovitViewコンポーネントのAdd Observed Component List Columnボタンを押します。
押した後に表示されたところにMonobitTransformViewをアタッチします。

これで下準備が出来たので、このオブジェクトを操作するためのPlayer.csスクリプトを作成します。
Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

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

    /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }
}

MUNの機能を利用すために、public class Player : MonobitEngine.MonoBehaviourと継承元をMUNの物に変えています。
後は、とりあえずシンプルな移動操作のみ実装しています。
このスクリプトを先ほどのオブジェクトに追加し、プレハブ化しておいてください。

プレイヤーのプレハブが作成できたので、このプレハブをネットワーク越しに生成してみます。

ネットワーク越しのオブジェクトの生成/MonobitEngine.MonobitNetwork.Instantiate メソッド(1)

この機能を使ってネットワーク越しにプレイヤーオブジェクトを生成させます。

適当に空のオブジェクトを作り、新しくSceneGameというスクリプトを作成し追加します。

SceneGame.cs
using UnityEngine;

using MonobitEngine;
public class SceneGame: MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private GameObject m_MinePLayerObj;

    // Start is called before the first frame update
    void Start()
    {
        m_MinePLayerObj = MonobitNetwork.Instantiate("Player", Vector3.zero, Quaternion.identity, 0);
    }

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

    }
}

これでネットワーク越しにプレイヤーの生成が行えました。

2.移動の処理の同期

続いて、移動の同期処理を実装していきます。
とはいったものの、実はMonobitTransformViewを追加していることにより位置の同期は既に行われるようになっています。
ただ、これだけだと自分が操作しているプレイヤー以外の他のプレイヤーが操作しているはずのオブジェクトまで操作できてしまい、思ったような実装にはなりません。
ということで、自分が操作するプレイヤーのみを操作できるようにPlayer.csに処理を追加します。

オブジェクト所有権チェック/MonobitEngine.MonobitView.isMine プロパティ
monobitView.isMine

生成したオブジェクトが自身で生成したものか、他のルームメンバーがネットワーク越しに生成してきたものかを判別することができます。
MonobitViewコンポーネントを追加し、MonobitEngine.MonoBehaviourを継承しているもので利用できます。
これを使用して、自身のみが移動操作でオブジェクトを動かせるように変更します。

Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();
    }

    /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }
}

Updare()内の処理をオブジェクトの所有権が自分にあるとき以外は処理させないようにしました。

これで移動の同期がきちんとできるようになりました。

3.弾の発射と同期

弾を発射するタイミングを同期させます。
弾の移動同期については、別途弾の記事で説明するため、今回は省きます。

ということで弾を飛ばす処理を書いていきます。

c#Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();

        Shot();
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        Debug.Log("Shot Bullet!!!");
    }
}

マウスの左クリックが押されている間m_ShotInterval間隔で弾を発射する処理を追加しました。

では、この発射する処理をルームメンバー間で同期させる処理に変更していきます。

RPC(Remote Procedure Call)/送信制御:MonobitEngine.MonobitView.RPC メソッド (1)

この機能を使い、発射タイミングの同期を実装します。

Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        Move();

        Shot();
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        /// <summary>自分も含めたルームメンバーに弾の発射を行わせる</summary>
        monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);
    }

    /// <summary>弾発射RPC</summary>
    [MunRPC]
    private void RecvShot(Vector3 position)
    {
        Debug.Log("Shot Bullet!!! Position(" + position.x + ", " + position.y + ")");
    }
}

Shot()内に記述しているmonobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);でルームメンバーに対して、発射のタイミングを通知しています。
発射のタイミングを受け取ったメンバーはprivate void RecvShot(Vector3 position)が呼ぶようになっています。

これで、弾の発射を同期することが出来るようになりました。

4.パラメータの同期

体力やプレイヤーの状態等、ルームメンバー間で共有しておきたい数値を同期させる処理を作っていきます。
今回は、体力、自身のスコア、状態の3つを同期させるように作ります。

[Tips] RPCメッセージの送信量・頻度の軽減/Tips(2) : ある特定のオブジェクト(prefab)の情報同期が目的である場合、RPCメッセージを使わず、OnMonobitSerializeView() を使う

こちらを参考に、数値の同期を実装していきます。

OnMonobitSerializeView メソッド
public void OnMonobitSerializeView( MonobitEngine.MonobitStream stream, MonobitEngine.MonobitMessageInfo info )

この機能をPlayer.cs内で使用し、体力、スコア、状態をルームメンバー間で共有します。

Player.cs
using UnityEngine;

using MonobitEngine;

 /// <summary>プレイヤーの状態</summary>
public enum PlayerState
{
    Active    = 0,
    Deactive  = 1,
    Unrivaled = 2,
    Dead      = 3
}

 /// <summary>プレイヤーの制御</summary>
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>移動方向</summary>
    private Vector3 m_MovementAmount = Vector3.zero;

    /// <summary>移動速度</summary>
    private float m_Velocity = 1.0f;

    /// <summary>発射間隔</summary>
    private float m_ShotInterval = 0.1f;

    /// <summary>発射間隔のカウント</summary>
    private float m_ShotIntervalCount = 0.0f;

    /// <summary>プレイヤーの状態</summary>
    private PlayerState m_State;

    /// <summary>体力</summary>
    private int m_HealthPoint;

    /// <summary>体力の上限</summary>
    private int m_MaxHealthPoint = 10;

    /// <summary>スコア</summary>
    private int m_Score;

    // Start is called before the first frame update
    void Start()
    {
        m_State = PlayerState.Deactive;

        m_HealthPoint = m_MaxHealthPoint;
    }

    // Update is called once per frame
    void Update()
    {
        if (!monobitView.isMine){ return; }

        switch (m_State)
       {
           case State.Active:
               Move();
               Shot();

               break;
           case State.Deactive:
               break;

           case State.Unrivaled:
               break;

           case State.Dead:
               break;
           default:
               break;
        }
    }

   /// <summary>移動</summary>
    private void Move()
    {
        m_MovementAmount.x = Input.GetAxis("Horizontal");
        m_MovementAmount.y = Input.GetAxis("Vertical");

        transform.localPosition += m_MovementAmount * m_Velocity * Time.deltaTime;
    }

    /// <summary>弾発射</summary>
    private void Shot()
    {
        if (!Input.GetMouseButton(0))
        {
            m_ShotIntervalCount = 0.0f;

            return;
        }

        m_ShotIntervalCount += Time.deltaTime;

        if (m_ShotIntervalCount < m_ShotInterval) { return; }

        m_ShotIntervalCount = 0.0f;

        /// <summary>自分も含めたルームメンバーに弾の発射を行わせる</summary>
        monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition);
    }

    /// <summary>弾発射RPC</summary>
    [MunRPC]
    private void RecvShot(Vector3 position)
    {
        Debug.Log("Shot Bullet!!! Position(" + position.x + ", " + position.y + ")");
    }

    /// <summary></summary>
    /// <param name="stream">MonobitAnimatorViewの送信データ、または受信データのいずれかを提供するパラメータ</param>
    /// <param name="info">特定のメッセージやRPCの送受信、または更新に関する「送信者、対象オブジェクト、タイムスタンプ」などの情報を保有するパラメータ</param>
    public void OnMonobitSerializeView(MonobitEngine.MonobitStream stream, MonobitEngine.MonobitMessageInfo info)
    {
        if (stream.isWriting)
        {
            stream.Enqueue(m_HealthPoint);
            stream.Enqueue(m_MaxHealthPoint);
            stream.Enqueue(Score);
            stream.Enqueue((int)m_State);
        }
        else
        {
            m_HealthPoint = (int)stream.Dequeue();

            m_MaxHealthPoint = (int)stream.Dequeue();

            m_Score = (int)stream.Dequeue();

            m_State = (State)stream.Dequeue();
         }
     }
}

単に体力, スコア, 状態の変数を用意し、OnMonobitSerializeView内でデータの書き込み/読み込みを行っているだけです。
これで数値を変化させても常に情報を共有してくれます。

5.プレイヤーオブジェクトの削除

最後に、自身で生成したプレイヤーオブジェクトの後片付けを行います。
とはいえ、ルームを抜けた際に所持者が居なくなったオブジェクトは自動的に削除されるため、自分が操作するプレイヤーなど、ゲーム中に削除を行わないようなものに関しては削除の処理は実装する必要がありません。
ここでは、任意で削除したい、というときに使用する機能を記載しておこうと思います。

ネットワーク越しのオブジェクトの破棄/MonobitEngine.MonobitNetwork.Destroy メソッド(1)
MonobitNetwork.Destroy();

この関数に削除したいオブジェクトのmonobitViewもしくはGameObjectを引数に渡してあげることで、ネットワーク越しに生成されたものを含めて削除してくれます。

これで、簡単ではありますが、プレイヤーに必要な処理の同期が実装できたかと思います。
次回はプレイヤーが発射する弾の実装を行おうと思います。

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

Azure PlayFabでPlayStreamを使ってユーザーのBANを実装する

まえがき

Azure PlayFab を使用すると、ユーザーの操作やステータスの変化が PlayStream イベントとして PlayFab に記録されます。

今回はこの PlayStream をフックしてユーザーの BAN を実装してみます。

シナリオ

ゲーム内通貨の無限増殖バグなどが利用されたことを想定して、所持通貨が 10,000,000 を超えたユーザーをリアルタイムに BAN してみます。

※こんなものを実装するイメージです。

手順

1. 新しいルールを作成する

GameManager へログインし、 自動化 > ルール > 新しいルール を押します。

2. 名前、イベントタイプ、条件、アクションを設定する

それぞれの項目を設定します。

名前:
任意のわかりやすい名前を設定します。

イベントタイプ:
com.playfab.player_virtual_currency_balance_changed を選択します。

条件:
VirtualCurrencyName を選択して、チェックしたい通貨の名称を指定します。今回は GO (ゴールドの略)という通貨をチェック対象にします。
さらに AND 条件で VirtualCurrencyBalance を選択して、 BAN 対象の閾値を指定します。今回は 10,000,000 を閾値にします。

アクション:
種類プレイヤーをプレイ禁止にする を指定します。
禁止の理由にはわかりやすい任意のメッセージを入力します。

PlayFab 側の設定はこれで完了です。
簡単ですね。

3. Unity 側で BAN されている場合のエラーメッセージを表示する

ユーザーが BAN されている状態で PlayFab の API を呼び出した場合、AccountBanned というエラーコードが返ってきます。

これを利用して例えば雑に IF 文を挟んでエラーメッセージを表示することができます。

        var request = new LoginWithCustomIDRequest { CustomId = ApplicationContext.PlayFab.PlayFabDebugUserId, CreateAccount = true };
        PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnLoginFailure);

        void OnLoginSuccess(LoginResult result)
        {
            // ログイン成功したときの処理
        }

        void OnLoginFailure(PlayFabError error)
        {
            // ログイン失敗したときの処理

            if (error.Error == PlayFabErrorCode.AccountBanned)
            {
                // BAN されているときの処理
            }
        }

4. 動作確認

ユーザーに 10,000,000 ゴールドを付与したところ、その瞬間に自動的に BAN されたことが PlayStream モニターで確認できました。
Unity 側でも意図したメッセージが表示されました。

あとがき

PlayFab の PlayStream は強力なので使いこなして行きたいですね。
実績解除やログイン報酬を配ったりもできますし、アウトゲームのロジックはこれにどんどん寄せて行きたいなと思います。

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

【ReSharper】ReSharper の使い方に関する記事まとめ(75個)

備考

この投稿は自分のブログの記事の転載になります
http://baba-s.hatenablog.com/entry/2019/06/07/090000

はじめに

自分のブログで公開した ReSharper の使い方に関する記事を75個まとめました

おすすめ

Unity

コードクリーンナップ

自動生成

Inspect

ファイルレイアウト

ショートカットキー

オプション

トラブルシューティング

拡張機能

その他

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

【Unity】BurstCompilerをJobSystem以外でも使いたい

BurstCompiler(以降、Burstと省略して記載)に関する小ネタです。

※BurstCompilerとは?と言う方はこちらを参照

BurstCompilerとは?

UnityにはPackageManagerから取得可能な拡張機能として、BurstCompilerと言うパッケージがあります。
概要を簡単に説明すると、JobSystemで走らせるコードを特定の条件下の時に最適化(SIMD化、一部の計算の精度を調整)してくれるパッケージになります。
※その為に何でもかんでも早くなってくれるわけではない。

特定の条件下として挙げられる内容としては主に以下のものが有り、いずれもC#を書く上では厳しい制約となりますが...処理が適していれば劇的なパフォーマンス改善が期待できるものであり、最適化の性質上から特に計算処理とは相性が良いかと思われるので、使えるなら積極的に使っていきたい機能ではあります。

  • マネージドオブジェクト(参照型とか)アクセス不可
  • static変数アクセス不可
  • 副作用のないコード
    • ※状態変化のないコード。言わば参照透過性の高いコードとも言い換えられるかも

※「なんで上記の制約があるのか?」について、CEDEC2018の以下の講演にて少し触れられているので興味のある方は御覧ください。

設定するだけなら簡単に設定可能

名前に「コンパイラ」とか付いているので小難しそうな印象を受けるかもしれませんが...実際にはそんなことはなく、設定自体は非常に簡単であり、一言で言うと「コンパイル対象のJob構造体やメソッドに[BurstCompile]と言う属性を付けるだけ」と言えるかもしれません。

Burstの機能としては↑の属性を付ける以外にも計算精度の指定と言った細かいオプションの方も用意されているので、先ずは公式ドキュメントの方をざっくりと目を通してみることをおすすめします。
→ 実装例の他にも内部挙動の話やビルド要件についてなど記載されている。(AndroidならNDKが必要とか)

Burst User Guide

こちらのBurstですが、Jobベースの処理を最適化する機能となるために基本的にはJob以外の処理(例えば通常のMainThreadで行う計算とか)に適用すると言ったことが出来ません。
そこで「何とかBurstを通常のMainThreadで行う計算処理などにも適用できないか?」と思い、調べてみた感じだと、それっぽいやり方を見つけたので備忘録序にメモしていければと思います。

※注意点

まだ実戦投入や様々なケースでのテストなどは出来てません。。
ひょっとしたら地雷が埋まっている可能性も無きにしもあらずなので...実戦投入される方はご注意ください。。
(今後問題点など見つけたら随時更新予定)

バージョン情報

  • Unity
    • 2018.4.7f1
  • Packages
    • Burst 1.1.2

検証環境

  • Standalone(Windows) + IL2CPP
  • CPU : Intel Core i7-8700K

Burst適用前について

先ずはBurst適用前のコード及び実行速度から載せていきます。

内容としては負荷計測用に用意した「千万回近く適当に演算して値(float3)を返すだけ」のコードになります。(もっと良い例を用意したかったが...思いつかなかった..)

public static float3 Calc()
{
    var vec = new float3();
    for (var i = 0; i < 10000000; i++)
    {
        var mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
        vec = math.transform(mat, vec);

        i += 1;
        mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
        vec = math.transform(mat, vec);

        i += 1;
        mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
        vec = math.transform(mat, vec);

        i += 1;
        mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
        vec = math.transform(mat, vec);

        i += 1;
        mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
        vec = math.transform(mat, vec);
    }

    return vec;
}

上記のCalc関数をUI.Buttonから実行できるようにして冒頭の検証環境で実行したところ、以下の計測結果が出ました。

Art000.png

MainThread上で1803.28ms(大体1.8秒近く)掛かっていることが確認できます。

Burstの適用方法/計測結果について

次に本題であるMainThread上からBurstを適用できるようにしたパターンについて解説していきます。

やり方は調べた範囲だと2つほどありました。
順に解説していきます。

BurstCompiler.CompileFunctionPointerでコンパイル

1つ目に紹介するのはBurstCompiler.CompileFunctionPointerと言うAPIを用いてコンパイルした関数ポインタを受け取る方法についてです。

先ずは実装コードから載せます。

[BurstCompile] // 必要
public static class Math
{
    // CompileFunctionPointerに渡すデリゲート.
    delegate void CalcDelegate(ref float3 vec);
    static CalcDelegate _calcDelegate;

    [RuntimeInitializeOnLoadMethod]
    static void Initialize()
    {
        // コンパイルした計算処理の関数ポインタを受け取ってデリゲートに保持.
        var funcPtr = BurstCompiler.CompileFunctionPointer<CalcDelegate>(CalcBurstImpl);
        _calcDelegate = funcPtr.Invoke;
    }

    public static float3 CalcBurst()
    {
        var vec = new float3();
        _calcDelegate(ref vec);
        return vec;
    }

    // コンパイル対象のメソッド
    // ※現状は戻り値を返すことが出来ないので、「参照渡し」か「ポインタ渡し」にする必要がある.
    [BurstCompile]
    static void CalcBurstImpl(ref float3 vec) => vec = Calc();  // Calcは上述のと同じ

    public static float3 Calc()
    {
        var vec = new float3();
        for (var i = 0; i < 10000000; i++)
        {
            var mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
            vec = math.transform(mat, vec);

            i += 1;
            mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
            vec = math.transform(mat, vec);

            i += 1;
            mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
            vec = math.transform(mat, vec);

            i += 1;
            mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
            vec = math.transform(mat, vec);

            i += 1;
            mat = new float4x4(quaternion.Euler(i, i, i), new float3(i, i, i));
            vec = math.transform(mat, vec);
        }

        return vec;
    }
}

上記のCalcBurstと言う関数を呼び出すことでBurstで最適化されたCalc関数を呼び出すことが可能です。
詳細についてはコメントにも記載してますが、順に解説していきます。

コンパイル手順

コンパイルまでの手順を以下に載せます。

[BurstCompile] // 必要
public static class Math
{
    // CompileFunctionPointerに渡すデリゲート.
    delegate void CalcDelegate(ref float3 vec);
    static CalcDelegate _calcDelegate;

    [RuntimeInitializeOnLoadMethod]
    static void Initialize()
    {
        // コンパイルした計算処理の関数ポインタを受け取ってデリゲートに保持.
        var funcPtr = BurstCompiler.CompileFunctionPointer<CalcDelegate>(CalcBurstImpl);
        _calcDelegate = funcPtr.Invoke;
    }

    // コンパイル対象のメソッド
    // ※現状は戻り値を返すことが出来ないので、「参照渡し」か「ポインタ渡し」にする必要がある.
    [BurstCompile]
    static void CalcBurstImpl(ref float3 vec) => vec = Calc();  // Calcは上述のと同じ

肝となるのは表題にもある「BurstCompiler.CompileFunctionPointer」と言うAPIです。

こちらにデリゲートの型を指定した上で対象の関数を渡すことで、コンパイルした関数ポインタを受け取ることが可能です。
※対象のクラスやメソッドにはBurstCompile属性を付ける必要があるので注意。
※「関数ポインタ→デリゲート」の変換はCompileFunctionPointerの実際の戻り値であるFunctionPointer<T>の中で行われている。ここについては記事後半の「おまけ」章で解説。

補足として、現時点ではコンパイル対象の関数は戻り値を返すことが出来ないらしく...返そうとすると以下のエラーが返ってきました。

error: The struct Unity.Mathematics.float3 cannot be returned by value from an external function in burst. The workaround is to return it by reference or pointer. This restriction will be removed in a future version of burst

将来的には制限は無くなるみたい?ですが、現状は「参照渡し」か「ポインタ渡し」を経由する必要があったので、CalcBurstImplと言う関数を経由する形で渡してあります。

呼び出しと実行結果

呼び出しとしてはコンパイル済み関数を保持しているデリゲートを呼び出すだけです。
今回の例ではCalcBurst内部でローカル変数を宣言 → コンパイル済み関数に参照渡しで計算 → 結果を返すと言う流れにしてます。

public static float3 CalcBurst()
{
    var vec = new float3();
    _calcDelegate(ref vec);
    return vec;
}

こちらをUI.Buttonから実行できるようにして冒頭の検証環境で実行したところ、以下の計測結果が出ました。

Art000.png

結果としてはMainThread上で551.27(大体0.55秒近く)になりました。
適用前と比べると1252.01ms(大体1.25秒近く)高速化出来ていることが確認できます。

Jobとは違い、Profiler上に明示的に(Burst)と言った表記は出ていないものの...結果の方から最適化された処理がMainThreadで呼び出されていることが確認できます。

Burstを有効にしたJobをMainThreadで実行

2つ目のやり方です。
正直1つ目のやり方で正しく動いてくれれば問題なさそう感ありますが...ちょっとした補足レベルで解説して行ければと思います。

詳細について解説していくとこちらも大体はコメントに記載してますが、やっている事としては極端な話「計算処理をBurstを適用したJobで動かすようにした」だけです。

肝となるのはIJobExtensions.Runと言うJob向けの拡張メソッドであり、こちらを用いることでJobをScheduleせずに同スレッドで直接実行するようにしてます。

後はJobで動かす都合上、戻り値を直接受け取ることが出来ないので、こちらに関してはローカル変数のポインタをJobに渡して実行後に結果を入れると言った形で対応してます。

※故にこのやり方で動かす場合にはAllow unsafe codeを有効にする必要あり。

public static unsafe float3 CalcBurst()
{
    // 結果を入れる変数
    var ret = new float3();

    // Jobから直接戻り値を受け取れないので結果のポインタを渡して入れる
    var job = new CalcJob() {Result = &ret};

    // MainThreadで実行
    job.Run();    // IJobExtensions.Run
    return ret;
}

[BurstCompile]    // Burst適用
unsafe struct CalcJob : IJob
{
    [NativeDisableUnsafePtrRestriction] public float3* Result;

    public void Execute()
    {
        var ret = Calc();
        *Result = ret;
    }
}

上記のCalcBurst関数をUI.Buttonから実行できるようにして冒頭の検証環境で実行したところ、以下の計測結果が出ました。

Art001.png

結果としてはMainThread上で549.13ms(大体0.55秒近く)になりました。
適用前と比べると1254.15ms(大体1.25秒近く)高速化出来ていることが確認できます。

他にもMainThread上で(Burst)と付いた処理が走っているので、目的であるMainThread上での実行も出来ているように見受けられます。

おまけ

BurstCompiler.CompileFunctionPointerの中身について

こちらの実装としてはパッケージの中身を見ることで確認可能です。
現時点でのバージョンでは以下のようになってました。

BurstCompiler.cs
/// <summary>
/// Compile the following delegate into a function pointer with burst, only invokable from a burst jobs.
/// </summary>
/// <typeparam name="T">Type of the delegate of the function pointer</typeparam>
/// <param name="delegateMethod">The delegate to compile</param>
/// <returns>A function pointer invokable from a burst jobs</returns>
public static unsafe FunctionPointer<T> CompileFunctionPointer<T>(T delegateMethod) where T : class
{
    // We have added support for runtime CompileDelegate in 2018.2+
    void* function = Compile(delegateMethod);
    return new FunctionPointer<T>(new IntPtr(function));
}

コンパイルしてますね。(見たままの感想)(深そうだったのでこれ以上深追いしてない)

他にも今回は2018.4で動かしているので普通に使えましたが、Unityのバージョンが古いと使えないと言ったようにも見受けられました。
※全部追えてはいないので..ひょっとしたら別名の代替関数があったりするかもしれない。古いバージョンで動作させる方は要チェック。

ちなみに戻り値であるFunctionPointer<T>について、こちらは元からC#にあるクラスではなく、同じくBurst内で定義されているものとなります。
やっている事としては単純であり、関数ポインタ(IntPtr)をGetDelegateForFunctionPointerでデリゲートに変換する為のサポートクラスのように見受けられました。

実装としては以下。

FunctionPointer.cs
namespace Unity.Burst
{
    /// <summary>
    /// Base interface for a function pointer.
    /// </summary>
    public interface IFunctionPointer
    {
        /// <summary>
        /// Converts a pointer to a function pointer.
        /// </summary>
        /// <param name="ptr">The native pointer.</param>
        /// <returns>An instance of this interface.</returns>
        IFunctionPointer FromIntPtr(IntPtr ptr);
    }

    /// <summary>
    /// A function pointer that can be used from a burst jobs. It needs to be compiled through <see cref="BurstCompiler.CompileFunctionPointer{T}"/>
    /// </summary>
    /// <typeparam name="T">Type of the delegate of this function pointer</typeparam>
    public struct FunctionPointer<T> : IFunctionPointer
    {
        [NativeDisableUnsafePtrRestriction]
        private readonly IntPtr _ptr;

        /// <summary>
        /// Creates a new instance of this function pointer with the following native pointer.
        /// </summary>
        /// <param name="ptr"></param>
        public FunctionPointer(IntPtr ptr)
        {
            _ptr = ptr;
        }

        /// <summary>
        /// Invoke this function pointer. Must be called from a Burst Jobs.
        /// </summary>
        public T Invoke => (T)(object)Marshal.GetDelegateForFunctionPointer(_ptr, typeof(T));

        IFunctionPointer IFunctionPointer.FromIntPtr(IntPtr ptr)
        {
            return new FunctionPointer<T>(ptr);
        }
    }
}

参考/関連サイト

Forum

API

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

【Unity】謎のアサーション'IsNormalized(dir)'&'IsNormalized(ray.GetDirection())'

バージョン

Unity 2019.1.9f1

はじめに

こんなアサーションに遭遇しました。
Assertion failed on expression: 'IsNormalized(dir)'
Assertion failed on expression: 'IsNormalized(ray.GetDirection())'

クリックしてもそれ以上の情報はなく、どこに問題があるのかどころか、誰がこのアサーションを吐いているのかさえ分からず。

とりあえず調べてみると公式フォーラムでFlare Layerを切ると消えたという情報が出てきます。
切ってみると確かに出なくなりましたが、このままではレンズフレアが使えない、ということで色々試して回避できそうな方法が分かったので共有します。

再現手順

  1. 空のシーンを作成します。
  2. Directional LightにLens Flareを追加します。
  3. Main CameraにFlare Layerを追加します。

ここまではレンズフレアを使うときにやる普通の作業かと思います。
このあと下記の2パターンで確認できました。

パターン1(Orthographicカメラ)

  1. Main CameraのProjectionをOrthographicにします。
  2. 再生します。
  3. Main CameraとDirectional LightのPositionを一致させます。

パターン2(Perspectiveカメラ)

  1. Main CameraのProjectionをPerspectiveにします。
  2. 再生します。
  3. Main CameraとDirectional LightのTransformを一致させつつ、PositionとRotationを適当に動かします。 (例えばPosition: -2, -13, -10、Rotation: 77.2, 112.32, 0とか)

原因ははっきりとは分からないのですが、カメラとLens Flareの位置関係によってはレンズフレアの計算ができない状況になるのでしょう。

対処法

このアサーションに遭遇する原因として一番ありそうなのがカメラにLens Flareが付いてしまっているパターン。
この場合、Lens Flareの使い方が間違っていると思われますので、まずは使い方が合っているか確認するのがいいかと思います。
こちらこちらを参考にさせて頂くと、空のオブジェクトやLightに付けるのが正しいかと思いますので、そのようにしましょう。

それでも、再現手順でやったように、カメラとLens Flareの位置が一致する状況があれば発生してしまうことでしょう。
その場合は両者の位置関係を意識したうえで、スクリプトやオブジェクト配置を見直してみるのがいいかと思います。

おわりに

すぐに原因箇所が分からないアサーションなので、解決に時間を取られてしまわないよう、一助になれば幸いです。

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