20191204のUnityに関する記事は15件です。

リンクスリングスのアウトゲーム設計

はじめに

サムザップ #2 Advent Calendar 2019 の12/3の記事です。

株式会社サムザップの尾崎です。Unityエンジニアです。

内容

リンクスリングスのアウトゲームの設計について紹介したいと思います。
また扱いやすいAPI(プログラムインターフェース)を目指しているのでそのコードを紹介します。

※ アウトゲームとはキャラクター選択画面など4v4バトルのゲーム本体以外の機能を指します
※ 紹介するコードはエラー処理を省いて記載してきます

リンクスリングスについて

公式サイト

https://linqsrings.jp/

画面イメージ

スクリーンショット 2019-12-04 午後9.25.20.png スクリーンショット 2019-12-04 午後9.32.08.png スクリーンショット 2019-12-04 午後9.26.49.png

linqs_sample.gif

ゲーム画面動画

https://www.youtube.com/watch?v=XFfSejixKfE

設計方針

  • 分かりやすいシンプルな構成
  • 使いやすいAPI
  • メンテナンスしやすい
  • 簡単に動作確認できる

主な採用技術

async/awaitUniTask

async/awaitはC#標準の非同期処理のための機能です。
コルーチンの代わりとして使っていて、画面遷移や通信やアニメーションなどの非同期系処理はasync/awaitに統一しています。
コールバックがなく読みやすいコードになっています。

Zenject

オブジェクト同士を参照させるのにZenjectを採用しています。
staticやシングルトンがなくなり、整理されたクラス関係を構築できました。

Pusher

アウトゲームでのリアルタイム通信のために採用しています。
マッチング、チャット、ゲーム内通知などに使用しています。
HTTPポーリングに比べて高速なレスポンスが得られています。
ちなみにインゲームではPhotonを採用しています。

プログラム構成

MVC(Model-View-Controller)パターンです。
MVVM、MVPと比較検証した結果、シンプルなMVCを採用しました。

Model

Model=データはxxxDataというクラスに定義しています。
データそのものとそれを扱うメソッドを持ちます。
サーバーから受け取ったjsonをC#オブジェクトにする役割もあります。

public partial class SomeData : ISerializationCallbackReceiver
{
    // サーバーから受け取ったintへのプロパティ。読み取り専用
    public int SomeCount => someCount;

    public enum SomeTypes
    {
        None,
        Type1,
        Type2
    }

    // サーバーから受け取ったstringをenumに変換
    public SomeTypes SomeType;

    // データを元に判定を行ったりするプロパティ
    public bool SomeUsefulProperty
    {
        get
        {
            ...
        }
    }

    // データ検索などを行うメソッド
    public int SomeUsefulMethod(SomeTypes type)
    {
        ...
        ...
    }

    public void OnAfterDeserialize()
    {
        // 文字列をenumに変換
        Enum.TryParse(someType, out SomeType);
    }

    public void OnBeforeSerialize() { }
}

// サーバーから受け取るjsonをデシリアライズするためのクラス
// 半自動生成
[Serializable]
public partial class SomeData
{
    [SerializeField]
    private int someCount;

    [SerializeField]
    private string someType;
}

Controller

画面を制御する部分です。
ModelとViewの橋渡しをします。
1画面につき1つのメインコントローラーを用意します。
複雑な画面ではメインコントローラー1つだとクラスが大きくなるので画面内の一部分を制御するサブコントローラーを作成します。

// 画面のメインコントローラー
public class SomeScene : MonoBehaviour, IAdditiveSceneTask
{
    // 画面遷移システム
    [Inject]
    private SceneLoader _sceneLoader;

    // View
    [SerializeField]
    private Text _text;

    // サブコントローラー
    [SerializeField]
    private SomeSubController _subController;

    // 画面遷移トゥイーン
    // インスペクタでリストにトゥイーンを登録するコンポーネントです
    [SerializeField]
    private Tweens _tweens;

    // 初期化
    private void Start()
    {
        _text.text = "";
    }

    // 画面遷移システムから画面開始時に呼び出される独自のコールバックです
    // IAdditiveSceneTaskを実装すると呼ばれます
    public async Task Activate()
    {
        /* 画面開始時の処理 */
        // 通信
        var someData = await WebRequest.Factory.SomeInfo(param).Send();

        // データをUIにセット
        _text.text = someData.name;

        // サブコントローラーの実行
        _subController.Execute();

        // UI出現アニメーション
        await _tweens.PlayInAnimations();
    }

    public async Task Inactivate()
    {
        /* 画面終了時の処理 */
        // UIを消すアニメーション
        await _tweens.PlayOutAnimations();

        // 各種アンロード
    }

    private void OnDestroy()
    {
        // 後処理
    }

    // ボタンが押されたときの処理
    // インスペクタでButtonコンポーネントから呼び出すように設定します
    public void OnClickButton()
    {
        // 例でバトルトップ画面に遷移
        // 画面はシーンをAdditiveロードする仕組み
        // 次シーンをロードしてActivate()を呼び出し、現在シーンのInactivateを呼び出します
        _sceneLoader.LoadSceneAdditive(ScenesEnum.BattleTop, false);
    }
}

View

Unity UIのCanvasやImage、ScrollRect、LayoutGroupなど見た目を制御するコンポーネントをViewコンポーネントと位置付けています。
それら見た目を制御するコンポーネントを組み合わせてHierarchyを構築してファイル化したSceneやPrefabがViewの扱いです。
基本的にはUnity UI標準コンポーネントを利用して、独自のViewコンポーネントを組み合わせています。
独自コンポーネントにはタブ、トゥイーン、スプライトアニメなど多数あります。
WebでいうHTMLのイメージです。

アウトゲームの機能

画面遷移

// 画面遷移のためのクラス
[Inject]
private SceneLoader _sceneLoader;

// シーンをAdditiveロード
_sceneLoader.LoadSceneAdditive(
    Scenes.SomeFunc,
    new SomeFuncScene.Arguments
    {
        TargetId = 1001
    }
);

ダイアログ (ポップアップウインドウ)

// ダイアログ開く
var dialog = await DialogLoader.Load<SomeDialog>();
dialog.Execute(param);
// ボタンが押されて閉じられるまで待つ
bool isOk = dialog.WaitClose();
if (isOk)
{
    // OKが押されたときの処理
}
public class SomeDialog : MonoBehaviour
{
    // ダイアログ共通処理コンポーネント
    [SerializeField]
    private DialogCommon _common;

    // OKボタンを押した?
    private bool _isOk = false;

    private void Awake()
    {
        // 初期化
        // 開く処理はDialogCommonによって自動的に行われます
    }

    public void Execute(int param)
    {
       // 引数を使った処理 
    }

    // OKボタンを押した
    public void OnClickOkButton()
    {
        _isOk = true;
        _common.Close();
    }

    // キャンセルボタンを押した
    public void OnClickCancelButton()
    {
        _common.Close();
    }

    // ボタンが押されてダイアログが閉じるまで待つ
    // 選択結果を返す
    public async Task<bool> WaitClose()
    {
        await Common.WaitClose();
        return _isOk;
    }
}

通信

try
{
    var webRequest = new WebRequest<SomeData>(APIType.SomeInfo, param);
    var responseData = await webRequest.Send();
}
catch (WebRequestException e)
{
    // 通信エラー時
}

リアルタイム通信

[Inject]
private IPusher _pusher;

await _pusher.Subscribe("channel_name",);
_pusher.Bind<SomeRealtimeData>("channel_name", "event_name", (someData) => {
    // サーバーからデータ受信したときの処理
    // 例. マッチングしたプレイヤーの情報を表示、チャットメッセージを表示
});

アセットバンドル

アセットバンドルシステムはIAssetBundleLoaderとして抽象化してサーバーからロードするクラスとローカルファイルからロードするクラスを切り替えられるようにしています。
Loadメソッドの第三引数ownerはGameObject型の引数でownerがDestroyされるとアセットバンドルもアンロードされる仕組みにしています。

// アセットバンドルロードシステム
[Inject]
private IAssetBundleLoader _assetBundleLoader;

var prefab = await _assetBundleLoader.Load<GameObject>(assetBundleName, assetName, owner);

設計で気をつけていること

コンポーネント指向

Unityの設計に習いコンポーネント指向で開発しています。
小さい機能を実現するコンポーネントを組み合わせて大きな機能を作ります。
コンポーネントが充実してくると組み合わせて新しい機能を効率よく作れます。
コードを書く必要がなく、非エンジニアにも優しいです。

これはコンポーネントの組み合わせで作ったボタンです。
スクリーンショット 2019-11-18 午後3.21.51.png

各コンポーネントの役割です

  • xxxButton: 独自ボタン制御(小さなコントローラー。クリック時の画面遷移などを行う)
  • Image: 見た目
  • Button: ボタン
  • CanvasGroup: 透明度とクリック可否
  • SwitchSprite: Imageに割り当てるスプライトの切り替え
  • ScaleInTween: UI出現時のトゥイーンアニメ
  • ScaleOutTween: UI消失時のトゥイーンアニメ
  • ClickTween: クリック時のトゥイーンアニメ
  • Se: クリック時のSE再生

各種TweenやSeはボタン以外でも利用しています。

コンポーネント指向の逆はオブジェクト指向の継承だと思います。
継承で上記ボタンを作ると標準Button継承したCustomButtonを作成しその中でトゥイーンやSe再生を作り込むことになり、それらは再利用しにくいものになります。
また大規模プログラムで継承を多用すると基底クラスに不必要な機能が入って肥大化することが多いです。コンポーネントの組み合わせで作ることでコード重複が少なく、再利用性の高いプログラムになります。
リンクスでは使い所をわきまえて継承階層が深くならないようにしています。

依存性の注入

Zenjectを利用しています。
1つの実装に依存しない柔軟性のあるプログラムにしています。
複数の実装が必要のないものはinterfaceを定義せずにクラス1つにしています。

シングルトンは禁止しています。

テストしやすい環境

シーンやコンポーネントをテストしやすくしています。
例えばシーンではバトル後の結果画面は正規フローだとログイン、マッチング、バトルを経るため動作確認までにとても手間がかかります。
バトル結果画面のシーンを開いた状態でUnity再生するとダミーデータで動作させて素早く確認できるようにしています。
コンポーネントはインスペクタにデバッグボタンを用意して確認しやすくしています。

UniRxオペレーターを多用しない

UniRxには多数のオペレーターが用意されていますが習得コストが高いと判断し、Whereなど超基本的なもののみを使うようにしています。
UniRxで使用しているのはSubject、ReactiveProperty、MicroCoroutineです。

  • Subject
    • C#標準eventの代わりに使用。解放が楽です。
  • ReactiveProperty
    • 値の変化を購読するときに使用しています。
  • MicroCoroutine
    • 高速なUpdate、コルーチンとして使用しています。

通信などの非同期処理にもRxを使わずasync/awaitかコルーチンを使っています。
手続き型で記述することで分かりやすくしています。

シングルトンを使用しない

シングルトンをアンチパターンと捉えて使用しないようにしています。
1つの実装に依存することになるのと、グローバル変数と同じく様々なところからアクセスされると分かりにくいコードになってしまうためです。

シーン構成

Unityのマルチーシーン機能を活用して1画面1シーンの構成にしています。
この構成にすることで作業分担しやすくなっています。
またシーンを開いて再生することで編集中画面の動作を素早く確認することもできます。

この画面はホーム画面でミッション画面を開きアイテム詳細ダイアログを開いた状態です。
スクリーンショット 2019-12-04 午後9.45.32.jpg
このときのHierarchyはこのようになっています。
スクリーンショット 2019-12-04 午後9.45.21.png

最後に

明日は @tomeitou さんの記事です。

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

【腹筋VR】Webエンジニアが「VRアプリ開発」やってみた→3日後

自己紹介

SESはWeb系じゃないけど、Web界隈のエンジニアです。VRの開発とかUnityとか何も知らない状態から3日でつくりました。

腹筋VR?

腹筋の回数をカウントしてくれるVRアプリです。いずれリリースできればと思います。
3日で動画のレベルまで形にできるんです。良くないですか? 3日間でやったことについて書きます。

やったこと

Oculus Goで開発しています。メルカリで1.8万で買いました。Oculus Questも近々購入予定。

環境構築

Udemyがサイバーマンデーセールだったので下記講座を購入しました

Unity 3D 超入門:UnityでVRゲームやエンドレスランなど4つのゲームを作ろう!
5つのゲームを作る講座もありますが、どう違うのか存じません

環境構築の記事もありますが、Vulkanを外す手順とか抜けてる記事が多いです。記事で何度かハマりましたが、上記の動画なら一発で環境構築できました。

注意事項
ただし、紹介した動画ではUnityの2017年版で環境構築させようとしてきます。インポートできないAssets(3Dモデルなどの素材)も多いので「2019.1」版でやりました。Gradleの設定だけはできなかったので飛ばしましたが、他の手順はそのままでビルドできました。

開発

Asset

環境構築が終わったらAssetを入れます。開発に必要な3Dモデル等を入れていく必要があります。最初にオススメしたUdemyの動画もいろいろな記事やサイトもOculus Integrationをオススメしてきます。しかし僕はOculus Integrationを即不採用にしました。

Oculusが公式で出している「Oculus Integration」を入れれば、Oculus Go向けのカメラ設定やコントローラの設定ができます。すぐにVR空間上に自分のコントローラを表示できるでしょう。ただし、メニュー画面の操作とかできません。

image.png

上記はプロトタイプの段階で作った3D空間のUI(メニュー画面)です。Oculus Goのコントローラが画面上に表示されるだけでメニューに表示されるボタンを押すことはできません。

下記のツイートの動画はもう少しブラッシュアップした版です。UI(メニュー画面)を操作するにはコントローラーからレーザーポインター等を出して、メニュー画面を操作する必要があるのです。動画序盤でコントローラーからでているレーザーポインターをやってくれているのはVIVE Input Utilityです。サンプルをみればすぐに下記のようなレーザーポインターを付けることができます。

角度の取得

動画を見ていただくとわかりますが腹筋するときは首が動きますね。顔が向いている角度で腹筋の回数をカウントします。UnityなのでC#です。

void Update () {
    Quaternion quaternion = this.transform.rotation;
    double x = quaternion.eulerAngles.x;
}

なんとこれだけで角度が取得できます。あとはプログラムをカメラにドラッグアンドドロップしてやれば、this(カメラ)の向いてる角度がxに入ります。Unity簡単ですね。

「真正面」を向いているときが0度、首を1度上に傾けると359度になるというルールがあります。

起き上がったとき=「カメラの向きが350度以上」
横になったとき =「カメラの向きが320度以下300度以上」

// 顔の角度
private const double up = 320.0;
private const double down = 350.0;
private const double limit = 300.0;

// 横になっているかどうか
private bool isDown = false;

// 腹筋回数
public int count = 0;

void Update () {

    // カメラの角度を取得
    Quaternion quaternion = this.transform.rotation;
    double x = quaternion.eulerAngles.x;

    // 300度未満なら何もしない
    if (x < limit) {return;}

    // 横になっていなかった かつ 320度未満300度以上
    if(isDown == false && x < up)
    {
        // 横になったと判定
        isDown = true;
        return;
    }

    // 横になっていた かつ 顔の角度が350度以上
    if(isDown == true && x > down)
    {
        // 横になっているかフラグをfalseにする
        isDown = false;
        count++; // 腹筋カウント
    }
}

このルールで腹筋のカウントをしています。単純でしょ?ヘドバンしないでね
C#の書き方いい加減すぎるか…

あとは…

ここまで腹筋VRを支える技術を説明しました。あとは最初に紹介した動画を見れば、開発に必要な知識を得られます。

最後に

なぜVRの開発を始めたのか、その動機が一番重要なので語らせてください。

最初はVRに失望していた

もともとVRには興味がありませんでした。学生時代に発売したOculus Riftの本体は高くないものの、「ゲーミングPC必須」でした。入門するだけで20~30万は軽く飛んでしまいます。これでは一般人には普及しない、と私は失望しました。当時スマホVRもコンテンツ数はとても少なかったのです。

XR燻製会の存在

それから数年間VRに対してアンテナを張っていませんでしたが、XR燻製会という煙的にもくもく会wイベントに参加しました。

僕は勢いで申し込んだのですが、内輪だけでやる予定のイベントだったのです。後日知ったのですが、「かんちゃんって誰だろう? 多分、他の参加者の知り合いなのかな」と当日まで思われていたそうです。

そこで燻製された美味しい食事とOculus GoやOculus QuestをはじめとするxR(VRやARの総称)コンテンツに触れることができました。「今はPCなくてもVRできる」という衝撃の事実を知りました。Oculus Goなら2万円、Oculus Questなら5万円でVRできるんです!すごい!

HoloLens ミートアップの参加

厳密にVRの勉強会ではないですが、ホロレンズミートアップに参加しました。xRの話が中心なので、VRの話も聞けました。具体的な業務利用の話やVRアプリの体験、とても新鮮でした。このイベントが終わってお金がたまった後にOculus Goポチりました。しばらく遊んだあと、開発を始めて3日で最初の動画の腹筋VRができたわけです。

何が言いたいのか

VRは安くなったことで「誰もが手が出せる」素晴らしいものになりました。
まだ【xR元年】と言われています。元年から次のステップに進むには【普及】が必要です。日本語コンテンツや開発に関するドキュメントは多くはありません。もっと開拓する人が必要だと感じました。

そこで遠目に見ているWeb界隈のあなたも例外ではありません。まずは、一緒にVR沼に浸かりませんか?

追記

NT札幌に参加したら、東京でもVRで腹筋するアプリを開発している人がいたと聞きました。Unityちゃんに踏んでもらえるとか…!その人と繋がりたい!

腹筋VRをリリースできたら、またリリースまでの記事書きます。

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

【Unity】gameObjectのgameObject

スクリプトからゲームオブジェクトを取得する

gameObjectからアタッチしているゲームオブジェクトのインスタンスを取得することが可能(リファレンス)。
MonoBehaviourの継承が必要だがそもそもアタッチしている時点で継承する必要あり。

例えばCubeオブジェクトにスクリプトをアタッチしてデバッグログでgameObjectを出力した場合。

SampleObject.cs
public class SampleObject : MonoBehaviour {

    private void Start () {
        Debug.Log(gameObject);
    }
}
Console
Cube (UnityEngine.GameObject)
UnityEngine.Debug:Log(Object)

こんなこと誰でも知ってるよね

gameObjectのgameObject

gameObjectGameObjectだが、GameObjectgameObjectを持っている(下記参照)。

GameObject.cs
namespace UnityEngine
{
    [ExcludeFromPreset]
    [NativeHeaderAttribute("Runtime/Export/GameObject.bindings.h")]
    [UsedByNativeCodeAttribute]
    public sealed class GameObject : Object
    {
        ...
        //
        // Summary:
        //     Scene that the GameObject is part of.
        public Scene scene { get; }
        public GameObject gameObject { get; }
        ...
    }
}

つまりgameObject.gameObjectみたいな書き方も可能。インスタンスも同じである。

SampleObject.cs
private void Start () {
    Debug.Log(gameObject.gameObject);
    Debug.Log(gameObject == gameObject.gameObject);
}
Console
Cube (UnityEngine.GameObject)
UnityEngine.Debug:Log(Object)

True
UnityEngine.Debug:Log(Object)

その気になればgameObject.gameObject.gameObjectなんて書くことも可能、たくさん繋げられる。

SampleObject.cs
private void Start () {
    Debug.Log(gameObject.gameObject.gameObject);
    Debug.Log(gameObject == gameObject.gameObject.gameObject);
}
Console
Cube (UnityEngine.GameObject)
UnityEngine.Debug:Log(Object)

True
UnityEngine.Debug:Log(Object)

何故こういう動きになるのか気になって調べてみたのですがあまり有力な情報は得られず。
何か知っている人がいたらコメント等で教えて下さい。

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

AssetBundle置き場を探す

どこに置く?

先日AssetBundleを使う機会がありました。
UnityRoomで利用したかったので外部でホスティングする必要があるのですが、どこに置いたらいいんでしょうか?
GCSとかS3とかでいいんじゃ、と思われるかもしれませんがこれらのストレージサービスは金がかかります。僕が作ったAssetBundleのサイズが55.6MBなのでもしも月1億回プレイされたとすると転送量は5.56PBとなり毎月278,000\$(2千7百万円)の出費となります。カンボジアに学校を作ろうと思うと30000\$ほどかかるらしいので毎月カンボジアに9個の校舎を建設するのと同じぐらいのお金がかかるわけです。毎年カンボジアに100校を乱立できる富豪でない限り破産ですね。

冗談はさておき、なるべく無料の範囲で抑えたいのが人情でしょう。今回はAssetBundle置き場として、無料のストレージサービスやホスティングサービスをいくつか試してみました。

調査対象

Dropbox, GoogleDrive, netlify, github, githubpages
の5つです。
なおこの記事はこれらのサービスでAssetBundleのホスティングが可能であることを保証するものではありません。商用非商用を問わず各サービスの利用規約を読んで自己責任でお願いします。ダメだよとかいけるよとか知っている人はコメントください。

調査方法

cds.png

DLManager.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.Networking;

public class DLManager : MonoBehaviour
{
    public InputField urlField;
    public Text status;
    public Text time;
    public Text dlSpeed;

    bool isDownloading = false;

    public void StartDownLoad()
    {
        if (!isDownloading)
        {
            StartCoroutine(DLCoroutine());
        }
    }

    IEnumerator DLCoroutine()
    {
        isDownloading = true;
        using (var request = UnityWebRequestAssetBundle.GetAssetBundle(urlField.text))
        {
            var asyncOpe = request.SendWebRequest();
            float startTime = Time.time;

            yield return new WaitUntil(() => {
                int percentage = (int)(asyncOpe.progress * 100);
                status.text = "downloading..." + percentage.ToString() + "%";
                return asyncOpe.isDone;
            });

            if (request.isHttpError || request.isNetworkError)
            {
                status.text = request.error;
                isDownloading = false;
                yield break;
            }

            float finishTime = Time.time;

            status.text = "Complete!";

            var abHandler = request.downloadHandler as DownloadHandlerAssetBundle;
            var assetbundle = abHandler.assetBundle;

            float deltaTime = finishTime - startTime;

            time.text = string.Format("{0:#.##}", deltaTime) + "[sec]";
        }

        isDownloading = false;
    }
}

こういうものを作りました。InputFieldに打ち込んだURLからAssetBundleをダウンロードし、そのダウンロードにかかった時間を表示するという素敵アプリです。これで各サービスからのDL時間を計測して最速を決定したいと思います。

調査環境

Unity 2019.2.0f1
AssetBundleサイズ:55.6MB
ネット環境
speedResult.png

調査1:ローカル実行

僕の当初の目的はWebGLアプリケーションなのですがWebGLとローカル(スタンドアロンとかエディタ上とか)では結果が変わってくるかもしれないのでまずはエディタ上で見ていきたいと思います。

GoogleDrive

ストレージサービスと言えばまずはこれでしょう。ちなみに共有用のリンクを少し書き換える必要があります。こちらの記事を参考にさせて頂きました。
googledrive_local.png

Dropbox

こちらもまたストレージサービスの代名詞ですね。
dropbox_local.png
こちらも共有リンクを少し書き換える必要があります。末尾のdl=0をdl=1にするだけです。

netlify

ウェブ屋さんなら誰でも知ってる話題のホスティングサービスですね。僕はウェブ屋さんじゃないのでつい最近まで知りませんでした。
netlify_local.png

github

あのgithubです。期待できそうですね。
github_local.png

githubpages

あのgithubのホスティングサービスです。期待できそうですね。
githubio_local.png

結果

サービス名 結果[sec]
GoogleDrive 28.36
Dropbox 77.69
netlify 169.11
github 24.21
githubpages 15.84

githubpages, github, GoogleDriveが速いですね。netlifyはちょっと実用には堪えない遅さ。ADSL等のもっと低速な環境では下手をすれば4分ぐらいかかりそうですね。意外なのはgithubよりもgithubpagesの方が速いことです。リポジトリ管理サービスよりもウェブサイトのホスティングの方が速度が求められるからでしょうか?不思議です。

調査2:WebGL

大本命のWebGL上での速度を計測していきましょう。Build&Runでローカルサーバを建ててその上で実行していきます。ローカルでダウンロードするのとブラウザからダウンロードするのに違いはあるのでしょうか?

GoogleDrive

googledrive_cors.png
あれ?

Dropbox

dropbox_cors.png
ん?

netlify

netlify_cors.png
おい。

github

github_cors.png
なんで?

githubpages

githubio_webgl.png
ようやく結果が出ました。

結果

サービス名 結果[sec]
GoogleDrive 失敗
Dropbox 失敗
netlify 失敗
github 失敗
githubpages 19.72

なめてんのか。
githubpagesを除く全てのサービスで失敗してしまいました。一体どうしたことでしょう。

その名もCORS

F12を押してエラーメッセージを見てみましょう。
cors.png
ウェブを触ったことある人なら見たことあるのではないでしょうか。
これはCORSという「異なるドメイン間でのリソースの共有」に関する仕組みに起因するエラーです。UnityはWebGLアプリケーションでAssetBundleをロードする際"XmlHttpRequest"という形でサーバにリクエストを送ります。この際WebGLアプリが動作しているドメイン(つまりブラウザで開いているページのドメイン)とAssetBundleが置いてあるドメインが同一の場合は何も問題はありません。(実際に上の実験ではnetlifyもCORSではじかれていますがnetlifyにアプリをデプロイすると読み込めるようになります。)
しかし二者のドメインが異なる場合はセキュリティ上の問題が発生します。そこで定められたのがCORSという仕組みで、ざっくり言うとリソース(ここではAssetBundle)が置いてあるサーバ側でどのドメインからならアクセスを許可するか、を設定でき、許可されていないドメインからのアクセスは全て拒否されます。
つまり今回の場合だとGitHubがlocalhostドメインからのXMLHttpRequestを許可していないから失敗した訳です。
一方githubpagesでは上手くいきました。githubpagesはCORSを*(ワイルドカード)で許可しているらしく、つまりどのドメインが要求してもリソースを渡すようになっているんですね。すばらしい寛容さです。
ちなみにnetlifyもCORSの設定が行えるようなので設定を変更すれば別のドメインからのアクセスを許可することができます。

結果

調査した結果WebGLアプリケーションのAssetBundle置き場として最も適しているのはgithubpagesとなりました。githubpagesはプライベートリポジトリでも利用できるようなので、プロジェクトのリポジトリの/docsフォルダにAssetBundleを置くようにすればプロジェクトの管理とAssetBundleのホスティングを一本化できていいと思います。
ちなみに無料で使えるサービスとしてはOracleCloudFreeTierというのがあり、ある程度の使用量までは無料でOracleCloudを使えるようです。たぶんCORS等の問題も無く月10TBまで転送できるようなので趣味で使う分にはかなり良さそうに思えます。今回の調査対象に含めようとも思ったのですが無料で使用するにもクレジットカードの登録が必要なタイプのサービスだったので断念しました。社会的信用のある方はぜひこちらもお試しください。

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

【Unity2019 LWRP】ポストエフェクトを自作してゲームから利用する

環境

Unity2019.1.0f2
LWRP - Version 5.7.2

はじめに

ポストエフェクトを自作する場合、従来のUnityではOnRenderImage()を実装したMonoBehaviourスクリプトをカメラにアタッチすることでこれを実装することができました。

ExampleClass.cs
using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    public Material mat;
    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        Graphics.Blit(src, dest, mat); // ポストエフェクト
    }
}

しかし、この方法はLWRP環境では使うことができません

LWRPのようなSRP環境ではレンダリングパイプラインに自作レンダリングパスを追加することで、ポストエフェクトを実装します。
今回はLWRP環境でポストエフェクトを実装するまでの手順を紹介します。

ポストエフェクトの実装例

Unity1Weekというオンラインイベントで制作したToTheRightというゲームでは2つのポストエフェクトを実装しました。

・死亡時に画面の色を反転させるポストエフェクト
色反転2.gif

・画面を歪ませるグリッチエフェクト
gif_animation_005.gif

今回はこの二つのポストエフェクトの実装について解説します。

ポストエフェクトの実装の流れ

ポストエフェクトを実装するには以下を作成する必要があります。
・CustomForwardRendererアセット
・ScriptableRendererFeatureクラス (パラメータやレンダーパスの追加はここで行う)
・ScriptableRendererPassクラス (レンダーパスの実装。実際の描画処理はここで実装)

処理の流れとしては以下のようになります。
LWRPSettingsへ登録したForwardRendererDataが実行される
-> ScriptableRendererFeatureが実行される
-> ScriptableRendererPassが実行され、描画が行われる。

ポストエフェクトの作成手順

1. CustomForwardRendererアセットの作成

メニューから Rendering > Lightweight Pipeline > Forward Rendererを選択し、CustomForwardRendererを作成します。
image.png

2. Forward Rendererアセットを登録

LWRP SettingsアセットのRendererType をCustomに設定。
Dataの部分に先ほど作成したForward Rendererアセットを登録します。
image.png

これでCustomForwardRendererが実行されるようになります。

補足 : LWRP Settingsアセットについて

Project SettingsのScriptable Render Pipeline Settingsの部分に登録しているアセットがLWRP Settingsアセットになります。
image.png

3. RendererFeatrureスクリプトの作成

ScriptableRendererFeatureの派生クラスを作成します。

GameRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.LWRP;

public class GameRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private bool hoge = true; // テキトーな変数
    [SerializeField] private int fuga = 123; // テキトーな変数

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
    }

    public override void Create()
    {
    }
}

4.RendererFeatureの登録

CustomForwardRendererにRendererFeatureを登録し、RendererFeatureが実行されるようにします。

+ボタンをクリックし、RendererFeatureを登録します。
image.png

登録すると以下のようになります。
image.png

Projectビューを見るとCustomForwardRendererの中にRendererFeatureができています。
image.png

RendererFeatureは、通常のScriptableObjectと同じようにInspectorタブからシリアライズ変数の値を編集できます。
レンダリングに関係するパラメータはRendererFeatureの中に定義しておくと良いでしょう。
image.png

ToTheRightで実装したポストエフェクト

1. RendererFeatureクラスの実装

今回のゲームToTheRightでは、色反転Passとグリッチ効果Passの二つを作成しています。
RendererFeatureクラスのコードは以下のようになりました。

GameRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.LWRP;

public sealed class GameRendererFeature : ScriptableRendererFeature
{
    [field: SerializeField] public bool ReverseActive { get; set; } = false;
    [field: SerializeField] public bool GlitchActive { get; set; } = false;
    [field: SerializeField, Range(0, 1)] public float GlitchWeight { get; set; } = 1f;
    [field: SerializeField, Range(0, 1)] public float NoiseWeight { get; set; } = 1f;
    private ReverseColorRendererPass reverseColorPass = null;
    private GlitchEffectRendererPass glitchPass = null;

    [SerializeField] private Material reverseColorMaterial;

    // レンダーパスの作成を行う
    public override void Create()
    {
        reverseColorPass = reverseColorPass ?? new ReverseColorRendererPass();
        glitchPass = glitchPass ?? new GlitchEffectRendererPass();
    }

    // レンダーパスの追加を行う
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // 色反転 ポストエフェクトPass 追加
        reverseColorPass.SetRenderTarget(renderer.cameraColorTarget);
        reverseColorPass.Active = ReverseActive;
        renderer.EnqueuePass(reverseColorPass);

        // グリッチ ポストエフェクトPass 追加
        glitchPass.SetRenderTarget(renderer.cameraColorTarget);
        glitchPass.Active = GlitchActive;
        glitchPass.GlitchWeight = GlitchWeight;
        renderer.EnqueuePass(glitchPass);
    }
}

2. 色反転エフェクトの実装

色を反転するShader

ReverseColor.shader
Shader "Hidden/ReverseColor"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

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

            sampler2D _MainTex;

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                // just invert the colors
                col.rgb = 1 - col.rgb;
                return col;
            }
            ENDCG
        }
    }
}

色の反転Passの実装

ReverseColorRendererPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.LWRP;

public sealed class ReverseColorRendererPass : ScriptableRenderPass
{
    private const string Tag = nameof(ReverseColorRendererPass);

    private RenderTargetIdentifier currentTarget;

    public bool Active { get; set; }

    public ReverseColorRendererPass()
    {
        renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
    }

    public void SetRenderTarget(RenderTargetIdentifier target)
    {
        currentTarget = target;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

        var shader = Shader.Find("Hidden/ReverseColor"); // 色を反転するシェーダー取得
        if (!shader) { return; }

        var material = new Material(shader);
        var commandBuffer = CommandBufferPool.Get(Tag);
        var renderTextureId = Shader.PropertyToID("_SampleLWRPScriptableRenderer");
        var cameraData = renderingData.cameraData;
        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        int shaderPass = 0;

        commandBuffer.GetTemporaryRT(renderTextureId, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        commandBuffer.Blit(currentTarget, renderTextureId);
        commandBuffer.Blit(renderTextureId, currentTarget, material, shaderPass);

        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}

3. グリッチエフェクトの実装

グリッチエフェクトのシェーダー

今回のグリッチ効果は以下のリンク先のGlitchShader.shaderを利用させていただきました。
https://github.com/staffantan/unityglitch

GlitchShader.shader
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// This work is licensed under a Creative Commons Attribution 3.0 Unported License.
// http://creativecommons.org/licenses/by/3.0/deed.en_GB
//
// You are free:
//
// to copy, distribute, display, and perform the work
// to make derivative works
// to make commercial use of the work


Shader "Hidden/GlitchShader" {
    Properties{
        _MainTex("Base (RGB)", 2D) = "white" {}
        _DispTex("Base (RGB)", 2D) = "bump" {}
        _Intensity("Glitch Intensity", Range(0.1, 1.0)) = 1
        _ColorIntensity("Color Bleed Intensity", Range(0.1, 1.0)) = 0.2
    }

        SubShader{
            Pass {
                ZTest Always Cull Off ZWrite Off
                Fog { Mode off }

                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest 

                #include "UnityCG.cginc"

                uniform sampler2D _MainTex;
                uniform sampler2D _DispTex;
                float _Intensity;
                float _ColorIntensity;
                float _GlitchWeight;

                fixed4 direction;

                float filterRadius;
                float flip_up, flip_down;
                float displace;
                float scale;

                struct v2f {
                    float4 pos : POSITION;
                    float2 uv : TEXCOORD0;
                };

                v2f vert(appdata_img v)
                {
                    v2f o;
                    o.pos = UnityObjectToClipPos(v.vertex);
                    o.uv = v.texcoord.xy;

                    return o;
                }

                half4 frag(v2f i) : COLOR
                {
                    half4 normal = tex2D(_DispTex, i.uv.xy * scale);
                    fixed2 uv = i.uv;

                    i.uv.y -= (1 - (i.uv.y + flip_up)) * step(i.uv.y, flip_up) + (1 - (i.uv.y - flip_down)) * step(flip_down, i.uv.y);

                    i.uv.x += (normal.x - 0.5) * displace * _Intensity;

                    i.uv = lerp(uv, i.uv, _GlitchWeight);

#define DirectionMulti 0.005
                    half4 baseColor = tex2D(_MainTex, i.uv.xy);
                    half4 color = baseColor;
                    half4 redcolor = tex2D(_MainTex, i.uv.xy + direction.xy * DirectionMulti * filterRadius * _ColorIntensity);
                    half4 greencolor = tex2D(_MainTex,  i.uv.xy - direction.xy * DirectionMulti * filterRadius * _ColorIntensity);

                    color += fixed4(redcolor.r, redcolor.b, redcolor.g, 1) *  step(filterRadius, -0.001);
                    color *= 1 - 0.5 * step(filterRadius, -0.001);

                    color += fixed4(greencolor.g, greencolor.b, greencolor.r, 1) *  step(0.001, filterRadius);
                    color *= 1 - 0.5 * step(0.001, filterRadius);

                    //return color;
                    return baseColor + color * _GlitchWeight;
                }
                ENDCG
            }
        }

            Fallback off

}

グリッチエフェクトPassの実装

GlitchEffectRendererPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.LWRP;

public sealed class GlitchEffectRendererPass : ScriptableRenderPass
{
    private const string Tag = nameof(GlitchEffectRendererPass);

    private RenderTargetIdentifier currentTarget;

    public bool Active { get; set; } 
    public float GlitchWeight { get; set; }
    public Material Material { get; set; }

    private GlitchEffect glitchEffect = null;

    public GlitchEffectRendererPass()
    {
        renderPassEvent = RenderPassEvent.AfterRendering;
    }

    public void SetRenderTarget(RenderTargetIdentifier target)
    {
        currentTarget = target;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

        if (glitchEffect == null || glitchEffect.gameObject == null)
        {
            glitchEffect = Camera.main.gameObject.GetComponent<GlitchEffect>();
        }
        //glitchEffect = glitchEffect ?? Camera.main.gameObject.GetComponent<GlitchEffect>();
        if (glitchEffect == null) { return; }

        glitchEffect.Execute();
        if (glitchEffect.Material == null) { return; }

        var commandBuffer = CommandBufferPool.Get(Tag);
        var renderTextureId = Shader.PropertyToID("_SampleLWRPScriptableRenderer");
        var cameraData = renderingData.cameraData;
        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        int shaderPass = 0;

        glitchEffect.Material.SetFloat("_GlitchWeight", GlitchWeight);

        commandBuffer.GetTemporaryRT(renderTextureId, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        commandBuffer.Blit(currentTarget, renderTextureId);
        commandBuffer.Blit(renderTextureId, currentTarget, glitchEffect.Material, shaderPass);

        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}

ポストエフェクトの制御について

今回はGameRendererFeatureで定義したパラメータを変更することでポストエフェクトを制御しました。
・色反転エフェクトのON/OFF
・グリッチエフェクトのかかり具合

ポストエフェクトのON/OFF

色反転エフェクトのON/OFFを切り替えたい場合はReverseActiveの値を変更します。

色反転エフェクトのON/OFF
    /// <summary>
    /// 色を反転
    /// </summary>
    public void SetReverseColor(bool active)
    {
        if (gameRendererFeature == null) return;
        gameRendererFeature.ReverseActive = active;
    }

ReverseActiveはGameRendererFeature.cs内にてReverseColorRendererPassに値をそのまま渡しています。

GameRendererFeature.cs
        // 色反転 ポストエフェクトPass 追加
        reverseColorPass.SetRenderTarget(renderer.cameraColorTarget);
        reverseColorPass.Active = ReverseActive;
        renderer.EnqueuePass(reverseColorPass);

ReverseColorRendererPassではActiveの数値を見て、falseの場合にスキップさせるようにしています。

ReverseColorRendererPass.cs
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

グリッチのかかり具合変更

グリッチのかかり具合を変更したい場合はGlitchWeightの数値を変更します。

グリッチエフェクトの重み変更
gameRendererFeature.GlitchWeight = scale * postEffectConfig.EnemyDeathGlitchCurve.Evaluate(time);

GameRendererFeatureのGlitchWeightをGlitchEffectRendererPassへ値を流しています。

GameRendererFeature.cs
        // グリッチPass 追加
        glitchPass.SetRenderTarget(renderer.cameraColorTarget);
        glitchPass.Active = GlitchActive;
        glitchPass.GlitchWeight = GlitchWeight;
        renderer.EnqueuePass(glitchPass);

GlitchEffectRendererPassでは GlitchWeight の値をシェーダーへ流しています。

GlitchEffectRendererPass.cs
        glitchEffect.Material.SetFloat("_GlitchWeight", GlitchWeight);

シェーダー内では _GlitchWeight はディストーションをかけたUVと元のUVの線形補間する際の数値として利用しています。
_GlitchWeight=0.0なら歪みなしUV、_GlitchWeight=1.0の場合は歪みありUVになります。

GlitchShader.shader
    fixed2 uv = i.uv;
    i.uv.y -= (1 - (i.uv.y + flip_up)) * step(i.uv.y, flip_up) + (1 - (i.uv.y - flip_down)) * step(flip_down, i.uv.y);
    i.uv.x += (normal.x - 0.5) * displace * _Intensity;
    i.uv = lerp(uv, i.uv, _GlitchWeight);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【unity】入手したアイテムのログを流すUIを作ってみた

どうも、N高等学校の生徒のシュンです。Unityが本当の意味でチョットデキルだけのよくある名前の人です。

この記事は、N高等学校アドベントカレンダー6日目の記事です。枠が空いているということで書くことにしました。
僕はょゎょゎえんじにあかつQiita自体初めて投稿するので期待せず生暖かい目でじっとりと見てくださると幸いです。

作ったもの

gif
このように、アイテムを入手した時に「〇〇を手に入れた」とログを表示して、一定時間経過後フェードアウトしていくUIを作ってみました。
また、文字数が長い場合改行されるようにしてみました。

ちなみにこれは今作っている脱出ゲームのようなサムシングのために作ったものなので、関係ないUIが映り込んでますが気にしないでください(?)

実行環境

  • Unity 2019.2.14f1

作り方

前提となるオブジェクトやコンポーネントを配置

892742318081356.png
このようにCanvasの子に空のゲームオブジェクト、その子にTextを任意の数配置します。
配置したら、空のゲームオブジェクトにVertical Layout GroupとContent Size Fitterコンポーネントを追加します。
42456234356410.png
コンポーネントの設定は画像の通りです。
Spacingは子オブジェクトの間隔なので任意の値にしてください。
またChild Alignmentは子のTextの文字の開始場所になるので、これも適当に設定してください。
位置は適当に調整してください。また、Widthを変更することでTextの文字が改行される位置を調整することができます。

Text達は特に変更する必要はありません。必要に応じてFont Sizeなどを変更してください。(ただし、Horizontal Overflowの設定をOverflowにすると改行されなくなります。)

スクリプトを書く

OutputLog.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class OutputLog : MonoBehaviour
{
    private int textCount; // 子オブジェクト(Text)の数を格納
    private Text[] logTexts;
    private textProperty[] textProperty;
    [SerializeField] float fadeoutSpeed; // フェードアウトの速度(0~1以内)
    [SerializeField] float fadeoutStartTime; // フェードアウトが始まる時間

    void Start()
    {
        textCount = transform.childCount;
        logTexts = new Text[textCount];
        textProperty = new textProperty[textCount];
        for(int i = 0; i < textCount; i++)
        {
            logTexts[i] = transform.GetChild(i).GetComponent<Text>();
            Color color = logTexts[i].color;
            logTexts[i].color = new Color(color.r, color.g, color.b, 0);
            textProperty[i].alfa = 0;
            textProperty[i].elapsedTime = 0;
        }
    }

    void FixedUpdate()
    {
        // 一番上に来たテキストのフェードアウトが始まってない場合開始
        if (textProperty[0].alfa == 1)
        {
            textProperty[0].elapsedTime = fadeoutStartTime;
        }
        // 経過時間のカウントやフェードアウトの処理
        for (int i = textCount - 1; i >= 0; i--)
        {
            if (textProperty[i].alfa > 0)
            {
                if (textProperty[i].alfa == 1)
                {
                    textProperty[i].elapsedTime += Time.deltaTime;
                }

                if (textProperty[i].elapsedTime >= fadeoutStartTime)
                {
                    textProperty[i].alfa -= fadeoutSpeed;
                }

                if (textProperty[i].alfa < 0)
                {
                    textProperty[i].alfa = 0;
                }
                Color color = logTexts[i].color;
                logTexts[i].color = new Color(color.r, color.g, color.b, textProperty[i].alfa);
            }
            else
            {
                break;
            }
        }
    }

    public void Hoge(string name) // ログを流したい時に呼び出す
    {
        // テキスト、透明度、経過時間を一つ上にずらす
        if (textProperty[textCount - 1].alfa > 0)
        {
            for (int i = 0; i < textCount - 1; i++)
            {
                logTexts[i].text = logTexts[i + 1].text;
                textProperty[i].alfa = textProperty[i + 1].alfa;
                textProperty[i].elapsedTime = textProperty[i + 1].elapsedTime;
            }
        }
        // 一番下のテキストを変更して表示、経過時間をリセット
        logTexts[textCount - 1].text = name + "を手に入れた"; // ここの文章を変えることで流れるメッセージが変わります
        textProperty[textCount - 1].alfa = 1f;
        textProperty[textCount - 1].elapsedTime = 0f;
    }
}
// テキストごとに透明度と表示されてからの経過時間を格納する
struct textProperty
{
    public float alfa;
    public float elapsedTime;
}

これを空のゲームオブジェクト(Text達の親)につければ完成です。
後はログを流したいタイミングでHogeメソッドを呼び出せばOKです。
fadeoutSpeedとfadeoutStartTimeはそれぞれフェードアウトの速度とフェードアウトが始まる時間なので、インスペクタから設定してください。なおfadeoutSpeedは0~1の範囲内にしてください。(まあ0だとフェードアウトしなくなりますが)

終わりに

いかがでしたか。
まあ本当にいかがでしたかサイトレベルの内容なんですが、N高ガチプロ勢から突っ込みをもらわないよう祈りたいと思います。っょっょえんじにあ怖いめう><
実際突っ込みどころは色々あると思うので、そこはお手柔らかにお願いします…(怖いとか言ってますが改善点あったらガンガン言ってください)(関係ないですが変数とかの命名苦手です)

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

UniRx を用いた PlayerPrefs との値のやりとりをする汎用 BaseModel

どうやって使うの?何に使うの?

PlayerPrefs を使って、ゲームのデータを永続化したりするのに、
BaseModel を継承した Model を作って、やりとりを簡潔にしましょう。
というもの。

以下がその BaseModel。

やってることは
1. Register メソッドで登録された ReactiveProperty を監視、値の変更があったら PlayerPrefs に保存
2. 次回 Awake 時に PlayerPrefs の情報と、 Register された情報をもとに、インスタンスと、そのデータを復元

BaseModel のコードの後に、その使用例を載せておきます。

BaseModel
using System.Collections.Generic;
using UnityEngine;

using System.Linq;

using UniRx;

namespace Models
{
    abstract public class BaseModel : MonoBehaviour
    {
        public class Model
        {
            public int id;

            public List<FloatReactiveProperty> floatAttrs = new List<FloatReactiveProperty>();
            public List<IntReactiveProperty> intAttrs = new List<IntReactiveProperty>();
            public List<StringReactiveProperty> stringAttrs = new List<StringReactiveProperty>();

            protected void Register(IntReactiveProperty attr)
            {
                intAttrs.Add(attr);
            }
            protected void Register(FloatReactiveProperty attr)
            {
                floatAttrs.Add(attr);
            }
            protected void Register(StringReactiveProperty attr)
            {
                stringAttrs.Add(attr);
            }

            virtual protected void InitInstance() { }

            virtual protected void RegisterAttributes() { }

            protected string modelName
            {
                get { return this.GetType().ToString().Split(new char[] { '+' })[0]; }
            }

            public Model()
            {
                InitInstance();
                RegisterAttributes();

                floatAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetFloat(modelName + floatAttrs.IndexOf(a).ToString() + "float" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                intAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetInt(modelName + intAttrs.IndexOf(a).ToString() + "int" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                stringAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetString(modelName + stringAttrs.IndexOf(a).ToString() + "string" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });

                PlayerPrefs.SetInt(modelName + "count", id + 1);
            }
        }

        virtual protected Model Instantiate()
        {
            return new Model();
        }

        public void Awake()
        {
            string modelName = this.GetType().ToString();

            int instanceCount = PlayerPrefs.HasKey(modelName + "count") ? PlayerPrefs.GetInt(modelName + "count") : 0;
            Enumerable.Range(0, instanceCount).ToList().ForEach(i =>
            {
                Model instance = Instantiate();

                instance.floatAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetFloat(modelName + instance.floatAttrs.IndexOf(a).ToString() + "float" + instance.id);
                });
                instance.intAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetInt(modelName + instance.intAttrs.IndexOf(a).ToString() + "int" + instance.id);
                });
                instance.stringAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetString(modelName + instance.stringAttrs.IndexOf(a).ToString() + "string" + instance.id);
                });
            });
        }
    }
}

使用例

Score(BaseModelの継承先)
using System.Linq;

using UniRx;

namespace Models
{
    public class Score : BaseModel
    {
        new public class Model : BaseModel.Model
        {
            public IntReactiveProperty score = new IntReactiveProperty();

            public bool isHighScore { get { return score.Value > 100; } }

            override protected void RegisterAttributes()
            {
                Register(score);
            }

            // 共通部分
            override protected void InitInstance()
            {
                id = instances.Count;
                instances.Add(this);
            }
        }

        static ReactiveCollection<Model> instances = new ReactiveCollection<Model>();

        public static ReactiveCollection<Model> All() { return new ReactiveCollection<Model>(instances); }
        public static int count { get { return All().Count; } }
        public static Model First() { return instances.First(); }

        override protected BaseModel.Model Instantiate()
        {
            return new Model();
        }

        new public void Awake()
        {
            base.Awake();

            if (All().Count == 0)
            {
                // new Model();
            }
        }
    }
}

Presenter
using UnityEngine;

using Models;

public class Presenter : MonoBehaviour
{
    void Start()
    {
        if (Score.count == 0)
        {
            Score.Model score = new Score.Model();
        }

        Debug.Log(Score.All().Count);
        Debug.Log(Score.First().score.Value);
    }
}

まだ改善の余地がたくさんあると思うので、まさかりください。
C# 全然わからん。

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

UniRx を用いた PlayerPref との値のやりとりをする汎用 BaseModel

どうやって使うの?何に使うの?

PlayerPref を使って、ゲームのデータを永続化したりするのに、
BaseModel を継承した Model を作って、やりとりを簡潔にしましょう。
というもの。

以下がその BaseModel。

やってることは
1. Register メソッドで登録された ReactiveProperty を監視、値の変更があったら PlayerPref に保存
2. 次回 Awake 時に PlayerPref の情報と、 Register された情報をもとに、インスタンスと、そのデータを復元

BaseModel のコードの後に、その使用例を載せておきます。

BaseModel
using System.Collections.Generic;
using UnityEngine;

using System.Linq;

using UniRx;

namespace Models
{
    abstract public class BaseModel : MonoBehaviour
    {
        public class Model
        {
            public int id;

            public List<FloatReactiveProperty> floatAttrs = new List<FloatReactiveProperty>();
            public List<IntReactiveProperty> intAttrs = new List<IntReactiveProperty>();
            public List<StringReactiveProperty> stringAttrs = new List<StringReactiveProperty>();

            protected void Register(IntReactiveProperty attr)
            {
                intAttrs.Add(attr);
            }
            protected void Register(FloatReactiveProperty attr)
            {
                floatAttrs.Add(attr);
            }
            protected void Register(StringReactiveProperty attr)
            {
                stringAttrs.Add(attr);
            }

            virtual protected void InitInstance() { }

            virtual protected void RegisterAttributes() { }

            protected string modelName
            {
                get { return this.GetType().ToString().Split(new char[] { '+' })[0]; }
            }

            public Model()
            {
                InitInstance();
                RegisterAttributes();

                floatAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetFloat(modelName + floatAttrs.IndexOf(a).ToString() + "float" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                intAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetInt(modelName + intAttrs.IndexOf(a).ToString() + "int" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                stringAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetString(modelName + stringAttrs.IndexOf(a).ToString() + "string" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });

                PlayerPrefs.SetInt(modelName + "count", id + 1);
            }
        }

        virtual protected Model Instantiate()
        {
            return new Model();
        }

        public void Awake()
        {
            string modelName = this.GetType().ToString();

            int instanceCount = PlayerPrefs.HasKey(modelName + "count") ? PlayerPrefs.GetInt(modelName + "count") : 0;
            Enumerable.Range(0, instanceCount).ToList().ForEach(i =>
            {
                Model instance = Instantiate();

                instance.floatAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetFloat(modelName + instance.floatAttrs.IndexOf(a).ToString() + "float" + instance.id);
                });
                instance.intAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetInt(modelName + instance.intAttrs.IndexOf(a).ToString() + "int" + instance.id);
                });
                instance.stringAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetString(modelName + instance.stringAttrs.IndexOf(a).ToString() + "string" + instance.id);
                });
            });
        }
    }
}

使用例

Score(BaseModelの継承先)
using System.Linq;

using UniRx;

namespace Models
{
    public class Score : BaseModel
    {
        new public class Model : BaseModel.Model
        {
            public IntReactiveProperty score = new IntReactiveProperty();

            public bool isHighScore { get { return score.Value > 100; } }

            override protected void RegisterAttributes()
            {
                Register(score);
            }

            // 共通部分
            override protected void InitInstance()
            {
                id = instances.Count;
                instances.Add(this);
            }
        }

        static ReactiveCollection<Model> instances = new ReactiveCollection<Model>();

        public static ReactiveCollection<Model> All() { return new ReactiveCollection<Model>(instances); }
        public static int count { get { return All().Count; } }
        public static Model First() { return instances.First(); }

        override protected BaseModel.Model Instantiate()
        {
            return new Model();
        }

        new public void Awake()
        {
            base.Awake();

            if (All().Count == 0)
            {
                // new Model();
            }
        }
    }
}

Presenter
using UnityEngine;

using Models;

public class Presenter : MonoBehaviour
{
    void Start()
    {
        if (Score.count == 0)
        {
            Score.Model score = new Score.Model();
        }

        Debug.Log(Score.All().Count);
        Debug.Log(Score.First().score.Value);
    }
}

まだ改善の余地がたくさんあると思うので、まさかりください。
C# 全然わからん。

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

Compute ShaderとFragment Shaderのレイマーチング での速度検証

はじめに

Pixel Sortを実装しようと四苦八苦していた時にCompute ShaderでのRWTextureへのランダムアクセスがかなり遅い印象を受けた。そこで、テクスチャへのランダムアクセスを使わないGPU処理においては、ラスタライズなどのパイプラインの機能をフルに起動しないことでCompute Shaderの方がFragment Shaderより処理速度が早くなるのではないかと思い、実験した。結果としてはほぼ変化がなかったが、私の実行環境ではRadeonと古いGeforceでのみFragment Shaderの方が若干速くなっていた。ここでは実験方法、得られた結果のプロットと実験環境をまとめる。
また、実験に使ったプロジェクトは以下のリポジトリで公開している。
https://github.com/Kuyuri-Iroha/compute_fragment_test

実験

実験方法

まず、今回行う実験では実際の処理速度に重きを置いて実験をしたかったこともあり、Unityのレンダリング統計ウィンドウのTime per frame(ms)を評価することとした。Time per frameの値は、実行画面を動画として撮影。撮影した動画の開始20秒を除いた30秒間を3秒ごとにサンプリングしたものを各実行環境で取得し、その平均値を処理時間として評価した。

実験環境

また、実験環境は以下の5つのPCとする。

  • Windows Desktop 1
    • Intel Core i9-9900K (オーバークロックなし)
    • 16GB RAM
    • GeForce RTX 2070 (8GB VRAM)
  • Windows Desktop 2
    • Intel Core i7-4770K(オーバークロックなし)
    • 16GB RAM
    • GeForce GTX 770 (2GB VRAM)
  • Windows Laptop
    • Intel Core i7-4710MQ
    • 8GB RAM
    • GeForce GTX 960M (2GB VRAM)
  • Macbook Pro
    • Intel Core i7-7567U
    • 16GB RAM
    • Intel Iris Plus Graphics 650 (1.5GB VRAM)
  • iMac
    • Intel Core i5
    • 16GB RAM
    • Radeon Pro 555 (2GB VRM)

実験用プログラム

実験用プログラムはinigo quilez氏のIterations - inversion 2をCgへの書き換えた後、#define AA 4に変更、72行目をコメントアウトしたものを使用した。また、描画サイズは400x400で統一した。
Compute Shaderでは、ARGB32のRenderTextureに出力して、PlaneのUnlitマテリアルのTextureとして設定することでレイマーチングを実行。スレッド数は(20, 20, 1)、グループ数は(20, 20, 1)としている。
Fragment Shaderでは、ARGB32の空のTexture2Dをsrc、同じくARGB32のRenderTextureをdestとしてGraphics.Blitを実行する形の実装とした。マテリアルとTextureの設定はCompute Shaderと同様の処理にて行っている。

実験結果

Windows Desktop 1 (ms) Windows Desktop 2 (ms) Windows Laptop (ms) Macbook Pro (ms) iMac (ms)
Compute Shader 15.5 18.6 40.6 146.1 110.3
Fragment Shader 15.3 18.7 37.8 147.7 81.2

図1.png

結果としてはこのようになった。どのPCにおいても実行速度にほとんど差はなかったが、RadeonのiMacのみFragment Shaderの方が速いという結果になった。
正直この差についてはサンプル数が少ないためなんとも言えないが、5つの中では一番古いWindows Laptopも若干Fragment Shaderの方が速かった。そのため、2次元の画像として扱えるデータならFragment Shaderで計算した方が良さげである。

おわりに

最初はCompute Shaderの方が速いんじゃないかという期待を持って実験を始めたので、結果としてかなり残念なものとなった。しかし、Compute Shader使いたいマンであった私に待ったをかけることになったので、それだけでも私にとっては有意義である。また、Compute Shaderは1次元であっても3次元であっても直感的に記述できるため、この程度の処理速度差であれば十分選択肢に入るということが検証できたわけでもあるので、今後より身近に使っていければと思う。

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

ムービーデータの納品方法を簡単にする

はじめに

本記事は、サムザップ Advent Calendar 2019 #2 の12/10の記事です。

株式会社サムザップの黒澤です。Unityエンジニアをしています

きっかけ

Unity上で再生するムービーを作る際に外部会社に依頼することなどが多いと思いますが

その際に、以下に納品方法を簡単かつミスが少なくなる方法はないかと考えていました

その時にやった手法を説明したいと思います

今回ムービーを再生する手段として、CRIWAREのManaを利用させて頂いています

手法

  • 外部会社さんとのやり取りは、基本git上でやり取りする
  • 納品は全てアセットバンドルとする
  • 再生を管理する設定は、Scriptable Objectを自動生成する
  • フォルダにムービーデータを配置するだけで納品できるようにする

上記のことを踏まえて、実際に下記のように実装しました

コード例

  • データ側(ScriptableObject)
MovieBundleBuildData.cs
namespace Sumzap.Movie
{
    /// <summary>
    /// Movieバンドルデータ
    /// </summary>
    public class MovieBundleBuildData : ScriptableObject
    {
        /// <summary> TotalByte </summary>
        [SerializeField]
        private long totalbyte;

        /// <summary> SoundBundleData </summary>
        [SerializeField]
        private List<MovieBundleData> movieBundleDatas;

        /// <summary>
        /// トータルのバイト数
        /// </summary>
        public long Totalbyte { get { return totalbyte; } set { totalbyte = value; } }

        //---------------------------------------------------------------------------------//
        /// <summary>
        /// MovieBundleDatas
        /// </summary>
        /// <value>Movile Bundle file.</value>
        //---------------------------------------------------------------------------------//
        public List<MovieBundleData> MovieBundleDatas { get { return movieBundleDatas; } set { movieBundleDatas = value; } }
    }

    //---------------------------------------------------------------------------------//
    /// <summary>
    /// Movie bundle data.
    /// </summary>
    //---------------------------------------------------------------------------------//
    [Serializable]
    public class MovieBundleData
    {
        /// <summary>
        /// インデックス
        /// </summary>
        [SerializeField]
        private int index;

        /// <summary> usmファイル名. </summary>
        [SerializeField]
        private string usmFileName;

        /// <summary>
        /// インデックス
        /// </summary>
        public int Index { get { return index; } set { index = value; } }

        //---------------------------------------------------------------------------------//
        /// <summary>
        /// Usmファイル名
        /// </summary>
        /// <value>The UsmFileName.</value>
        //---------------------------------------------------------------------------------//
        public string UsmFileName { get { return usmFileName; } set { usmFileName = value; } }
    }
}
  • エディタ側
CreateMovieBundleBuildData.cs
namespace Sumzap
{
    /// <summary>
    /// ムービーで使用するバンドルデータを作成するクラス
    /// </summary>
    public class CreateMovieBundleBuildData
    {
        //---------------------------------------------------------------------------------//
        /// <summary>
        /// メニューからBuildData作成
        /// </summary>
        //---------------------------------------------------------------------------------//
        [MenuItem("Assets/Sumzap/Movie/Create BuildData")]
        public static void CreateBuildData()
        {
            string selectPath = string.Empty;
            foreach(Object selectedObj in Selection.objects)
            {
                selectPath = AssetDatabase.GetAssetPath(selectedObj);
                break;
            }

            // ファイルが選択されていたらフォルダに変換
            if(File.Exists(selectPath))
            {
                selectPath = Path.GetDirectoryName(selectPath);
            }

            //----------------------------------------------------------------
            // ScriptableObject作成
            //----------------------------------------------------------------
            string basePath = selectPath;
            string buildDataPath = basePath + "/" + "MovieBundleBuildData.asset";
            string label = "MovieBundleBuildData";
            MovieBundleBuildData buildData;
            if(!File.Exists(buildDataPath))
            {
                buildData = ScriptableObjectToAsset.Create<MovieBundleBuildData>(buildDataPath, label);
            }
            else
            {
                buildData = AssetDatabase.LoadAssetAtPath<MovieBundleBuildData>(buildDataPath);
            }
            // 初期化
            buildData.Totalbyte = 0;
            // usmFiles
            List<string> usmFiles = FindFiles(selectPath, @"*.usm.bytes", SearchOption.AllDirectories, true);
            if(usmFiles.Count > 0)
            {
                List<MovieBundleData> usmDataList = new List<MovieBundleData>();
                usmFiles.ForEach(
                    (file, index) =>
                    {
                        MovieBundleData data = new MovieBundleData();
                        data.Index = index;
                        data.UsmFileName = Path.GetFileName(file);
                        FileInfo fileInfo = new FileInfo(Path.GetFullPath(file));
                        buildData.Totalbyte += fileInfo.Length;
                        usmDataList.Add(data);
                    });
                buildData.MovieBundleDatas = usmDataList;
            }
            else
            {
                buildData.MovieBundleDatas = new List<MovieBundleData>();
            }

            AssetDatabase.Refresh();
        }

        //---------------------------------------------------------------------------------//
        /// <summary>
        /// ファイル検索
        /// </summary>
        /// <returns>The files.</returns>
        /// <param name="path">ベースパス.</param>
        /// <param name="searchPattern">検索パターン.</param>
        /// <param name="searchOption">検索オプション.</param>
        /// <param name="includeDirectoryName">ディレクトリ名を含めるか.</param>
        //---------------------------------------------------------------------------------//
        public static List<string> FindFiles(string path, string searchPattern, SearchOption searchOption, bool includeDirectoryName = true)
        {
            List<string> files = new List<string>();
            string[] fileArray;

            fileArray = Directory.GetFileSystemEntries(path, searchPattern);
            if(fileArray != null)
            {
                foreach(string fileName in fileArray)
                {
                    files.Add(fileName);
                }
            }

            if(searchOption == SearchOption.AllDirectories)
            {
                string[] subFolders = System.IO.Directory.GetDirectories(path, "*", System.IO.SearchOption.AllDirectories);

                if(subFolders != null)
                {
                    foreach(string folderName in subFolders)
                    {
                        fileArray = Directory.GetFileSystemEntries(folderName, searchPattern);
                        if(fileArray != null)
                        {
                            if(includeDirectoryName)
                            {
                                foreach(string fileName in fileArray)
                                {
                                    files.Add(fileName);
                                }
                            }
                            else
                            {
                                foreach(string fileName in fileArray)
                                {
                                    files.Add(Path.GetFileName(fileName));
                                }
                            }
                        }
                    }
                }
            }
            return files;
        }
    }
}

使い方

フォルダにムービーを登録してもらったら、フォルダを選択して、メニューのCreate BuildDataを実行するだけです。
出来上がった、Scriptable Objectはこんな感じ

スクリーンショット 2019-12-04 10.57.00.png

最後に

この手のコードは、普段から書いていると簡単なのですが意外に忘れがちになってしまいます。

なので、備忘録として記事にしました。

明日は @ohbashunsuke さんの記事です。

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

MacでUnity ML-Agentsの環境を構築する(v0.11.0対応)

Unityで機械学習を利用するML-Agentsというライブラリがv0.11.0(beta)にバージョンアップしていたので試してみました。(2019/11/05時点)

v0.9.1からしばらく動作確認をサボっていたらだいぶと変更点があって戸惑いました。

Release ML-Agents Beta 0.11.0 · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/releases/tag/0.11.0

Unity ML-Agentsについては下記が参考になります。

【Unity】Unityで機械学習する「ML-Agent」を色々と試して得た知見とか
http://tsubakit1.hateblo.jp/entry/2018/02/18/233000

Unityをまだインストールしていないという方は下記をご参考ください。

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

手順

基本的には公式にある下記ドキュメントに沿えばよい感じです。

ml-agents/Installation.md at master · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md

ml-agents/Basic-Guide.md at master · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Basic-Guide.md#setting-up-ml-agents-within-unity

Pythonをインストールする

現在、Python 3.6.1以上での動作がサポートされています。3.5以前のバージョンはサポートされていないのでご注意ください。(2019/11/05現在)

ml-agents/Installation.md at master · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md

In order to use ML-Agents toolkit, you need Python 3.6.1 or higher. Download and install the latest version of Python if you do not already have it.

We do not currently support Python 3.5 or lower.

v0.10.0でようやく3.7以上に対応してくれたみたいです。

Release ML-Agents Beta 0.10.0 · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/releases/tag/0.10.0

ML-Agents is now compatible with Python v3.7 and newer versions of Tensorflow up to 1.14.

お手元にPythonの環境がない方は下記をご参照ください。

MacでanyenvをつかってPython環境構築(bash、fish対応) - Qiita
https://qiita.com/kai_kou/items/f54931991a781b96bb9c

ML-Agentsリポジトリをダウンロード

適当なディレクトリにリポジトリをダウンロードします。

> mkdir 適当なディレクトリ
> cd 適当なディレクトリ
> git clone https://github.com/Unity-Technologies/ml-agents.git

必要なライブラリをインストールする

ML-Agentsのパッケージをpipを利用してPyPIからインストールします。
ここではPythonの仮想環境を作ってインストールします。

仮想環境?なにそれ?な方は下記をご参照(再掲
https://qiita.com/kai_kou/items/f54931991a781b96bb9c

> python --version

Python 3.7.4


> python -m venv venv
> . venv/bin/activate

# fishな方はこちら
> . venv/bin/activate.fish

> pip install mlagents

はい。

Unityアプリからサンプルプロジェクトを開く

Unity Hubでアプリを立ち上げます。Unity Hubがインストールされていない場合は下記をご参考ください。

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

ML-Agentsを利用するにはUnityのバージョン2017.4 以上が必要となります。今回は2019.2.10f1 を利用しました。

アプリが立ち上がったら「開く」ボタンから任意のディレクトリ/ml-agents/UnitySDK フォルダを選択します。

2019_11_05_16_58.png
2019_11_05_17_00のコピー.png
2019_11_05_17_01のコピー.png

Unityエディタのバージョンによっては、アップグレードするかの確認ダイアログが立ち上がります。

2019_11_05_17_02のコピー.png

「確認」ボタンをクリックして進めます。アップグレード処理に少し時間がかかります。

スクリーンショット 2018-09-13 10.50.01.png

Scenes(シーン)が開くか確認する

サンプルが動作するか、Unityでプロジェクトを読み込み、動作させてみます。

  • Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
  • 開いたら、[3DBall]ファイルがあるので、ダブルクリックして開く

スクリーンショット 2019-11-06 10.13.57.png
なんかでてきたー(感動

Unityの上にある再生ボタンをクリックします。

スクリーンショット 2019-11-06 10.14.19.png
なんかうごいたー(感動
そして、かわいくなってるwww

読み込んだサンプルが動作することを確認できました。
現時点ではボールがボックス?からすぐに落ちてしまいます。これを機械学習で、落とさないようにさせるわけです。

Scenes(シーン)の設定

ML-Agentsで学習させるための設定です。

  • Unityアプリの[Edit]メニューから[Project Settings]を開く
    スクリーンショット 2019-08-19 14.23.05.png

  • [Inspector]パネルで以下の設定を確認する

    • [Resolution and Presentation]の[Run In Background]がチェックされている
    • [Display Resolution Dialog]がDisableになっている スクリーンショット_2019_08_19_14_25のコピー.png

学習させる

学習に必要な設定ができましたので、Unityアプリ上で学習させてみます。

コマンドの実行

コンソールに戻り、学習開始のコマンドを実行します。オプションは結構数がありますが、最低限を指定します。

> cd 適当なディレクトリ
> mlagents-learn ml-agents/config/trainer_config.yaml --train

WARNING:tensorflow:
The TensorFlow contrib module will not be included in TensorFlow 2.0.
For more information, please see:
  * https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md
  * https://github.com/tensorflow/addons
  * https://github.com/tensorflow/io (for I/O related ops)
If you depend on functionality not listed there, please file an issue.



                        ▄▄▄▓▓▓▓
                   ╓▓▓▓▓▓▓█▓▓▓▓▓
              ,▄▄▄m▀▀▀'  ,▓▓▓▀▓▓▄                           ▓▓▓  ▓▓▌
            ▄▓▓▓▀'      ▄▓▓▀  ▓▓▓      ▄▄     ▄▄ ,▄▄ ▄▄▄▄   ,▄▄ ▄▓▓▌▄ ▄▄▄    ,▄▄
          ▄▓▓▓▀        ▄▓▓▀   ▐▓▓▌     ▓▓▌   ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌  ╒▓▓▌
        ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓      ▓▀      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌   ▐▓▓▄ ▓▓▌
        ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄     ▓▓      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌    ▐▓▓▐▓▓
          ^█▓▓▓        ▀▓▓▄   ▐▓▓▌     ▓▓▓▓▄▓▓▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▓▄    ▓▓▓▓`
            '▀▓▓▓▄      ^▓▓▓  ▓▓▓       └▀▀▀▀ ▀▀ ^▀▀    `▀▀ `▀▀   '▀▀    ▐▓▓▌
               ▀▀▀▀▓▄▄▄   ▓▓▓▓▓▓,                                      ▓▓▓▓▀
                   `▀█▓▓▓▓▓▓▓▓▓▌
                        ¬`▀▀▀█▓


INFO:mlagents.trainers:CommandLineOptions(debug=False, num_runs=1, seed=-1, env_path=None, run_id='ppo', load_model=False, train_model=True, save_freq=50000, keep_checkpoints=5, base_port=5005, num_envs=1, curriculum_folder=None, lesson=0, slow=False, no_graphics=False, multi_gpu=False, trainer_config_path='ml-agents/config/trainer_config.yaml', sampler_file_path=None, docker_target_name=None, env_args=None, cpu=False)
INFO:mlagents.envs:Start training by pressing the Play button in the Unity Editor.

INFO:mlagents.envs:Start training by pressing the Play button in the Unity Editor. と出力されたら、Unityアプリの上部にある[▶]ボタンをクリックします。初期設定だと50,000ステップ実行するので少々時間がかかります。

スクリーンショット 2019-11-06 10.51.01.png

(略)
INFO:mlagents.trainers: ppo: 3DBall: Step: 2000. Time Elapsed: 22.201 s Mean Reward: 1.167. Std of Reward: 0.766. Training.
(略)
INFO:mlagents.trainers: ppo: 3DBall: Step: 10000. Time Elapsed: 113.151 s Mean Reward: 30.106. Std of Reward: 27.092. Training.
(略)
INFO:mlagents.trainers: ppo: 3DBall: Step: 49000. Time Elapsed: 542.744 s Mean Reward: 100.000. Std of Reward: 0.000. Training.
(略)
IGNORED: Cast unknown layer
IGNORED: StopGradient unknown layer
GLOBALS: 'is_continuous_control', 'version_number', 'memory_size', 'action_output_shape'
IN: 'vector_observation': [-1, 1, 1, 8] => 'sub_3'
IN: 'epsilon': [-1, 1, 1, 2] => 'mul_1'
OUT: 'action', 'action_probs'
DONE: wrote ./models/ppo-0/3DBall.nn file.
INFO:mlagents.trainers:Exported ./models/ppo-0/3DBall.nn file

Python 3.7.4で動作させているからか、TensorFlow 2.0だからなのか、WARNING が結構出力されますが、動作はしているので置いておきます。

学習結果をアプリに組み込む

学習が完了すると学習結果がmodels 配下に*.nn ファイルとして保存されます。
それをUnityアプリに組み込むことで学習結果をUnityアプリに反映できます。

> tree models/
models/
└── ppo-0
    ├── 3DBall
    │   ├── checkpoint
    │   ├── frozen_graph_def.pb
    │   ├── model-50000.cptk.data-00000-of-00001
    │   ├── model-50000.cptk.index
    │   ├── model-50000.cptk.meta
    │   ├── model-50001.cptk.data-00000-of-00001
    │   ├── model-50001.cptk.index
    │   ├── model-50001.cptk.meta
    │   └── raw_graph_def.pb
    └── 3DBall.nn

2 directories, 10 files

Unityアプリの設定

Playerの設定を行います。

  • Unityアプリの[Edit]メニューから[Project Settings]を選択する
  • [Inspector]ビューの[Other Settings]欄で以下を確認・設定する
    • Scripting BackendがMono になっている
    • Api Conpatibility Levelが.NET 4.x になっている スクリーンショット 2019-11-06 11.23.45.png

学習結果ファイルの取り込み

ターミナルかFinderで学習結果を以下フォルダにコピーします。

  • 学習結果ファイル: models/ppo-0/3DBall.nn
  • 保存先: UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/

※すでに保存先に3DBall.nn ファイルが存在していますので、リネームします。

> cp models/ppo-0/3DBall.nn ml-agents/UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/3DBall_new.nn
  • Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
  • 開いたら、[3DBall]ファイルがあるので、ダブルクリックして開く
  • [Hierarchy]パネルから[Agent]を選択する
  • Unityアプリの[Project]パネルで以下フォルダを選択する
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [TFModels]
  • Unityアプリの[Inspector]パネルにある[Model]という項目に[TFModels]フォルダ内の3DBall_new.nnファイルをドラッグ&ドロップする
    2019_11_06_11_28のコピー.png

  • Unity上部にある[▶]ボタンをクリックする

これで、学習結果が組み込まれた状態でアプリが起動します。
50,000ステップ学習すると動きが穏やかでもはやプロです。なんのプロかはわかりませんが^^
スクリーンショット 2019-11-06 10.14.19.png

サンプルは他にもあるので、いろいろとお試しあれ。
ML-Agentsはv0.5.0くらいから触っていますがだいぶと利用方法がかんたんになってきていますが、v0.11.0ではさらに設定箇所が減っていて良い感じになっています。

参考

【Unity】Unityで機械学習する「ML-Agent」を色々と試して得た知見とか
http://tsubakit1.hateblo.jp/entry/2018/02/18/233000

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

ml-agents/Installation.md at master · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md

ml-agents/Basic-Guide.md at master · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Basic-Guide.md#setting-up-ml-agents-within-unity

Release ML-Agents Beta 0.10.0 · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/releases/tag/0.10.0

MacでanyenvをつかってPython環境構築(bash、fish対応) - Qiita
https://qiita.com/kai_kou/items/f54931991a781b96bb9c

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

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

UnityのARFoundationでAR空間に豆腐を召喚する

はじめに

TechTrain Advent Calendar 2019の4日目は、UnityでARアプリを開発する際に使用できるSDKの一つであるARFoundationを使って「AR空間に豆腐を召喚するまで」の記事を書いてみました!

ARFoundationとは

今回紹介するARFoundationとは、Unityが開発しているマルチプラットフォームのAR開発フレームワークです。Unity’s Handheld AR Ecosystem: AR Foundation, ARCore and ARKitで紹介されています。原文は英語なのですが、要点だけ翻訳しておくと、

ARFoundationを使用することで、ARCoreとARKit両方のプラットフォームで共通した実装ができます。これは、一度アプリを開発すれば両方のデバイスに一切の修正なしでデプロイできるということです。しかし、ARFoundationはARKitとARCore全ての機能を実装し終えたわけではありません。なので、ARFoundationがまだ実装していない任意の機能を実装する場合は、対応するSDKそれぞれを利用して開発する必要があります。これからも機能を追加していくので、ARCoreとARKit全ての機能をARFoundationから扱えるように慣れればいいよね( ̄▽ ̄)

って感じのことをBlogの一部で書いていました。

開発環境

  • Unity2019.1.10
  • ARFoundation2.1.4
  • ARKit XR Plugin2.1.2(iPhone実機で試すため)

AR Foundation SamplesのREADMEに記載があるのですが、ARFoundationのmasterブランチ(最新版)を使う場合はUnity2019.2かそれ以降のバージョンが必要です。

豆腐を召喚する

1. カメラの用意

まずは、カメラを用意します。ARFoundationには専用のカメラがあるのでそちらを利用するため、まずはMain Cameraを削除します。

そして、HierarchyViewから、XR>AR Session Originを作成します。
スクリーンショット 2019-12-04 1.41.11.png

すると、AR Session Originの子要素にAR Cameraが新しく作成されていると思います。それがAR空間でのカメラとなります。

XR>AR Sessionを作成します。

スクリーンショット 2019-12-04 1.41.13.png

ARSessionとは何か、まずはUnity公式の説明を原文ママ紹介します。

Class ARSession. Controls the lifecycle and configuration options for an AR session. There is only one active session. If you have multiple ARSession components, they all talk to the same session and will conflict with each other. Enabling or disabling the ARSession will start or stop the session, respectively.

翻訳すると、

ARSessionクラスはAR Sessionのライフサイクルなどを制御します。これは、アクティブなシーンに対してただ一つのみ存在します。もし、複数のARSessionコンポーネントがある場合はコンフリクトを起こします。Enable/Disableを切り替えることで、セッションを再開、停止することができます。

となります。AR機能を管理してくれていて、シーンに一つおけばいいんだよって感じですね。

2. 平面の検出

次に、ARアプリなどでは、カメラに移った特徴点から平面を検知します。今回も平面を検知した上で、Rayを飛ばし豆腐を召喚したいので、まずは平面を検知しましょう。

平面を検知するには、先ほど作成したARSessionOriginに、AR Plane Managerというコンポーネントを追加します。

スクリーンショット 2019-12-04 1.50.45.png

平面検知は、水平方向と、垂直方向の両方を検知することが可能であり、床に豆腐を置いたり、壁にカレンダーをかけたりなどができます。

今回は床に豆腐を召喚したいので、ModeをHorizontalのみにしておきましょう。

デバッグ時などに、どこを平面として検知しているのかなどを確認したい時があると思います。その際には、Plane Prefabにオブジェクトをセットすることで、検知した平面にそのオブジェクトを描画してくれます。

3. Rayを飛ばして豆腐召喚!

さぁ、いよいよ豆腐を召喚していきましょう?

AR空間でRayを飛ばして衝突を判定するには、ARRaycastManagerコンポーネントを使用します。そのため、まずはARSessionOriginオブジェクトにAR Raycast Managerコンポーネントを追加しましょう。

スクリーンショット 2019-12-04 2.19.18.png

次に、実際にRayを飛ばして豆腐を召喚するコードを書いていきます。複雑なコードなどは出てこないので、なんとなく読めると思いますが、RaycastはPhysicsクラスではなく、ARRaycastManagerクラスのものを使うところなどは、ARFoundation独自の実装方法になっていると思います。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

[RequireComponent(typeof(ARRaycastManager))]
public class PlaceOnPlane : MonoBehaviour
{
    [SerializeField, Tooltip("AR空間に召喚する豆腐")] GameObject tohu;

    private GameObject spawnedObject;
    private ARRaycastManager raycastManager;
    private static List<ARRaycastHit> hits = new List<ARRaycastHit>();

    private void Awake()
    {
        raycastManager = GetComponent<ARRaycastManager>();
    }

    void Update()
    {
        if (Input.touchCount > 0)
        {
            Vector2 touchPosition = Input.GetTouch(0).position;
            if (raycastManager.Raycast(touchPosition, hits, TrackableType.Planes))
            {
                // Raycastの衝突情報は距離によってソートされるため、0番目が最も近い場所でヒットした情報となります
                var hitPose = hits[0].pose;

                if (spawnedObject)
                {
                    spawnedObject.transform.position = hitPose.position;
                }
                else
                {
                    spawnedObject = Instantiate(tohu, hitPose.position, Quaternion.identity);
                }
            }
        }
    }
}

ちょっと補足ですが、今回は検知した平面に豆腐を召喚したかったため、TrackableTypeのPlanesを指定しましたが、enumのTrackableTypeは他にも種類があります。TrackableTypeの実装を記載しておきます。

[Flags]
public enum TrackableType
{
    None = 0x0,
    PlaneWithinPolygon = 0x1,
    PlaneWithinBounds = 0x2,
    PlaneWithinInfinity = 0x4,
    PlaneEstimated = 0x8,
    Planes = 0xF,
    FeaturePoint = 0x10,
    Image = 0x20,
    Face = 0x40,
    All = 0x7F
}

ビルドして実行

よし!!出来たぞ、ビルドして豆腐召喚やああぁぁ!!!

って感じでやってしまうと、アプリ起動時にいきなりエラーで落ちてしまいます。案外盲点かもしれないので最後に対処法を追記しておきます。

原因としては、ARアプリなので、カメラを使います。カメラを使う際にはプライバシーの関係上ユーザからの使用許可を得る必要があります。位置情報とかもそうですね。その際に表示する「なぜ、カメラを使うのか」の説明文を記入しておかないと、実行時エラーとなりアプリが終了してしまいます。

なので、Project Settings>Player>Other Settings>Camera Usage Descriptionの欄になんでもいいので記入しておいてください。(なんでもいいと言いましたが、テキトーなこと書いてリリース申請出すとappleさんに弾かれます…)
もしくは、ビルド後にXCodeからInfo.plistというファイルを探し、Privacy - Camera Usage Descriptionを追加して解決することもできます。お好きな方で。

では、そこを記入したらいよいよビルドです!!

tohugif-compressor.gif

ファイルサイズの都合上ほんの一瞬しかうつせてないのですが、豆腐を召喚する前に、平面を検知するためにスマホを上下左右に動かしています。ある程度動かしてから画面をタップすると豆腐が召喚できると思います!!?

最後に

ARFoundationには、他にもFace TrackingやLight Estimationなど様々機能があるので、是非色々使ってみてください!!
この記事がAR開発を始めるきっかけになれば幸いです!( ̄▽ ̄)

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

VTuberのリアルライブの裏側

概要

VTuberのリアルライブイベントにおける技術的な裏側のお話を、筆者の経験に基づいてお話します。
今回は歌って踊る楽曲パートから実際に喋るMCパートへの切り替えについてお話します。

VTuberのリアルライブイベントとは

VTuberの方々が普段活動しているネット上のイベントではなく、現実世界のライブホール等で行う音楽ライブのことを指しています。
観客の前に大きなスクリーンを用意して、そこにVTuberが動いている様子を投影して、歌ったり踊ったりしている様子を観て盛り上がります。

ライブ中の楽曲パートとMCパート

VTuberの音楽ライブでは、現実の人間が行うように楽曲パートとMCパートがあります。
MCパートは楽曲と楽曲の間に少しの時間挟むものが一般的です。
現実の人間が行うライブと異なる点としては、それぞれのパートをどのように見せるかで次の3つに分かれることです。

① 楽曲パートもMCパートもリアルタイムで行う
② 楽曲パートは事前収録でMCパートはリアルタイムで行う
③ 楽曲パートもMCパートも事前収録

①については、かなり中の人(魂)の能力に依存しますが、専属のダンサーを付けて役割分担しているところもあります。

②については、個人的に最も一般的な方法かと感じています。
楽曲パートで再生するダンスや歌は事前に収録した映像やモーションデータ、音源を利用しています。

③については、かなり割り切った方法ですが、一番安定した方法です。
ライブの最中にモーションセンサーの不具合やネットワークの輻輳で事故ることを避けたい場合にとることが多いです。

事前収録からリアルタイムで行うMCパートへの切り替え

先程の②については、さらに分類すると2つに分けられます。

a. 事前収録した映像の終了後に暗転を挟む
b. 事前収録したモーションとモーションセンサーの現在のモーションをブレンドして切り替える

a.については②の方法の中で一番良く見かける方法です。
歌って踊っている映像の後に、暗転(または何かしらの演出)を挟み、完全にVTuberの姿を消してから、観客から姿が見えなくなっている間に、モーションセンサーでリアルタイムで動いている姿に切り替えてから姿を見せます。

b.を行っている現場は稀にしかみません。
踊っているモーションが終わった段階でリアルタイムのモーションセンサーの動きとブレンドして自然な形で切り替えます。この方法の難しいところは、モーションを線形にブレンドしてしまうと機械的な動きに見えてしまうため、非線形に切り替える必要があります。
ただ、プロのダンサーの方にお願いすると、指定したポーズを完璧にとってくれるので意外とうまくいきます。

以上、VTuberのリアルライブの裏側の話でした。
具体的な例を出すと色々と怒られるので文字だけになってしまい申し訳ないです。

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

VisualStudioの拡張機能をうまく活用すると人生が楽になる 【VisualStudio】【Unity】

gif.gif
floatの変数0.0fで初期化、stringはstring.Emptyで初期化...など
コードを書くときある程度プロジェクト毎にお約束のテンプレートがあったりすると思うのですが、何回も書くのが少し面倒になる時があります。

VisualStudioの拡張機能SnippetDesignerを利用すると自分の好きなコードのテンプレートが作れ、その問題が解決されます。 

実装手順

1.[ツール] ->[拡張機能と更新プログラム] ->[オンライン]からSnippetDesignerをインストール

snippet.PNG

2.テンプレート化したいコードを選択して右クリックからExportasSnippetを選択

prop.gif

3.最後に名前など書き換える必要のある部分を$$で囲みShotcut名を付ければ完成!

shotcut.PNG

使用した結果

gif2.gif

早い・・

よく使うコーディングに利用すれば大幅に効率を上げることが出来そうです。
プロジェクト内でしか利用できないショートカットを作ったとしても後で編集や削除が容易にできるのでどんどん活用していきましょう!

若者のhoge離れが始まっているようだがhogeで説明し続ける。

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

SteamVR unity Plugin v2系で詰まったところ覚え書き

この記事の目的と背景

本記事はノンプログラマ(グラフィッカー)である筆者が
SteamVR Unity Pluginでコントローラのボタン入力などを
取ろうとして詰まったところとその対応の忘備録である。

実行環境

unity 2017.4.28f1
SteamVR Unity Plugin - v2.4.5 (sdk 1.7.15)

HTC VIVE
steamVR1.8.21
OS:windows 10
Google Chorome

筆者スペック:unity歴2年(グラフィッカー)

実施内容

参考にした記事と手順

VIVEのコントローラからボタン入力を出来るようにしたいと考え調べたところ、
下記記事で丁寧に方法が紹介されていた。
https://qiita.com/htpn/items/9838f5cb5d78de90a5c2

詰まった所

ただし、Open binding UIをクリックしても

このサイトにアクセスできませんlocalhost で接続が拒否されました。
localhost 8998 dashboard controller binding を Google で検索してください
ERR_CONNECTION_REFUSED

と表示されるだけで先に進めなかった。
何らかのローカルパスエラーとして調べたが、
どこが問題なのか判然としなかったので
別の解決手段を探った。

対応

https://steamcommunity.com/games/250820/announcements/detail/3809361199426010680?l=japanese
steamコミュニティの方にコントローラバインドについての記事があったので
HMDを被り、settingを開き、コントローラ割り当てを選択。
コントローラのバインドの編集のアプリケーションからunityで制作中のアプリ[testing]を選択すると
コントローラのバインド設定画面を呼び出すことができ、unity側で登録したアクションを設定できた。

(HMDでの設定画面についてはスクリーンショットの取り方がわからなかった。画像が無くて申し訳ない)

まとめ

今回の環境と時間内で調べた限りでは、
これがバージョンなどによるものなのか、環境によるものなのか等は解らなかったが
いちいちHMDを被って設定を行うのは面倒くさいので、出来ればブラウザで完結できるようにしたい。
引き続き時間を見つけて調査と作業を進めていく。

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