20190714のUnityに関する記事は6件です。

AzurePlayFabのとても便利なマッチメイキング(マッチング)機能を使ってみた

まえがき

マッチメイキングの処理はオンラインゲームにはかかせないですよね。

でもマッチメイキングを実装しようとすると、やりたいことはプレイヤーに適切なルームやバトルのIDを渡したいだけなんですが、様々なルール(プレイヤーのレベルやレートの考慮、1vs1 or チームバトル or バトルロイヤル)や排他制御を考える必要があって大変です。

そんなマッチメイキングを PlayFab を使えば簡単に実装できると聞いたので試してみました。

※今回は複雑なルールは取り上げません。
※マッチメイキングは Public Preview の機能です。

作ってみたもの

matchmakingsample.gif

PlayFab を使ったマッチングの流れ

  • 事前に GameManager でマッチング処理用のキューを作り、バトルの人数やマッチングする条件を定義します。
  • Player が上記のキューに MatchmakingTicket というチケットを投げます。
  • キューにバトルの人数分のチケットが貯まるとマッチングされます。
  • Player が投げたチケットをポーリングしておけば MatchID が取得できるので、同じMatchID の Player を集めてバトルを開始したりします。

使用する PlayFab の API

1. 事前準備(GameManager でマッチング処理用のキューを作っておく)

GameManager へログインし、マルチプレイヤー > マッチメイキング > 新しいキューをクリックします。
image.png

キューの構成を変更してキューを作成します。
必須項目は キュー名対戦人数 です。
今回は 1vs1 のマッチングを作りますので、それっぽいキュー名を指定し、対戦人数は必ず2人なので 2to2 に設定しておきます。
image.png

キューができました。
GameManager での事前準備はここまでです。
image.png

2. 最低限のコードでシンプルなマッチングを動かしてみる

Unity でこんなコードを書いてみました。
これを動かすと先に張ったサンプルのようにシンプルな1vs1のマッチングを行うことができます。

using PlayFab;
using PlayFab.ClientModels;
using PlayFab.MultiplayerModels;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class SampleSceneController : MonoBehaviour
{
    // 処理中のメッセージは雑に全部これに表示します。
    [SerializeField] Text textBox;

    public void Start()
    {
        textBox.text = "ログイン中...\n";

        // PlayFabにいつも通りログインします。
        var request = new LoginWithCustomIDRequest { CustomId = "MyCustomId", CreateAccount = true };
        PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnFailure);

        void OnLoginSuccess(LoginResult result)
        {
            textBox.text += "ログインしました!\n\n";

            // ログインできたので続けてマッチングの処理を呼びます。
            Matchmaking();
        }
    }

    private void Matchmaking()
    {
        textBox.text += "マッチメイキングチケットをキューに積みます...\n";

        // プレイヤーの情報を作ります。
        var matchmakingPlayer = new MatchmakingPlayer
        {
            // Entityは下記のコードで決め打ちで大丈夫です。
            Entity = new PlayFab.MultiplayerModels.EntityKey
            {
                Id = PlayFabSettings.staticPlayer.EntityId,
                Type = PlayFabSettings.staticPlayer.EntityType
            }
        };

        var request = new CreateMatchmakingTicketRequest
        {
            // 先程作っておいたプレイヤー情報です。
            Creator = matchmakingPlayer,
            // マッチングできるまで待機する秒数を指定します。最大600秒です。
            GiveUpAfterSeconds = 30,
            // GameManagerで作ったキューの名前を指定します。
            QueueName = "1vs1Battle"
        };

        PlayFabMultiplayerAPI.CreateMatchmakingTicket(request, OnCreateMatchmakingTicketSuccess, OnFailure);

        void OnCreateMatchmakingTicketSuccess(CreateMatchmakingTicketResult result)
        {
            textBox.text += "マッチメイキングチケットをキューに積みました!\n\n";

            // キューに積んだチケットの状態をマッチングするかタイムアウトするまでポーリングします。
            var getMatchmakingTicketRequest = new GetMatchmakingTicketRequest
            {
                TicketId = result.TicketId,
                QueueName = request.QueueName
            };

            StartCoroutine(Polling(getMatchmakingTicketRequest));
        }
    }

    IEnumerator Polling(GetMatchmakingTicketRequest request)
    {
        // ポーリングは1分間に10回まで許可されているので、6秒間隔で実行するのがおすすめです。
        var seconds = 6f;
        var MatchedOrCanceled = false;

        while (true)
        {
            if (MatchedOrCanceled)
            {
                yield break;
            }

            PlayFabMultiplayerAPI.GetMatchmakingTicket(request, OnGetMatchmakingTicketSuccess, OnFailure);
            yield return new WaitForSeconds(seconds);
        }

        void OnGetMatchmakingTicketSuccess(GetMatchmakingTicketResult result)
        {
            switch (result.Status)
            {
                case "Matched":
                    MatchedOrCanceled = true;
                    textBox.text += $"対戦相手が見つかりました!\n\nMatchIDは {result.MatchId} です!";
                    return;

                case "Canceled":
                    MatchedOrCanceled = true;
                    textBox.text += "対戦相手が見つからないのでキャンセルしました...";
                    return;

                default:
                    textBox.text += "対戦相手が見つかるまで待機します...\n";
                    return;
            }
        }
    }

    void OnFailure(PlayFabError error)
    {
        Debug.Log($"{error.ErrorMessage}");
    }
}

3. プレイヤーのレートやレベル差を考慮したマッチングを動かしてみる

実際のゲームではレートやレベル差によって、初心者と上級者のマッチングを分けたりしますよね。

PlayFab ではどのように制御すればよいのか試してみました。

3.1. キューにルールを追加する

先程キューを作ったときは最低限の内容しか定義しませんでした。
今回は以下のようなルールを追加してみます。

このルールを追加することで、Rate の差が 101 以上のプレイヤー同士はマッチングしなくなります。
image.png

3.2. プレイヤーの Rate 情報をチケットに含める

先程のコードではチケット内の MatchmakingPlayer には Entity 情報しか含んでいませんでした。
今回は Rate の情報をもたせるために Attributes を使用します。

        // マッチングさせるプレイヤーの情報を作ります。
        var matchmakingPlayer = new MatchmakingPlayer
        {
            // Entityは下記のコードで決め打ちで大丈夫です。
            Entity = new PlayFab.MultiplayerModels.EntityKey
            {
                Id = PlayFabSettings.staticPlayer.EntityId,
                Type = PlayFabSettings.staticPlayer.EntityType
            },
            // これ以下を追記
            Attributes = new MatchmakingPlayerAttributes
            {
                // このプレイヤーは Rate 900~1100 のプレイヤーとしかマッチングしない
                DataObject = new { Rate = 1000 }
            }
        };

これだけでレートやレベル差を考慮したマッチングを実装することができました。
これは楽で良いですね。

あとがき

今回はマッチメイキングの基本の部分を触ってみました。

まだ公式ドキュメントを読み切れていないのですが、PlayFab のマッチメイキング機能はもっと複雑なマッチングルールやチームバトルにも対応しているということですので、引き続き触ってみようと思います。

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

UnityのEditor拡張から値を更新したあとUnity再起動すると値がもとに戻る問題

環境

Unity 2018.3.12f1

はじめに

Inspector上で値を変更した変数がUnityを再起動したときにもとに戻ってしまう原因はおもに2つあります。

  • 変数がSerializeされていないもしくはSerializeできない型を使っている
  • Sceneが値の変更を検知していない(本題)

変数がSerializeされていないもしくはSerializeできない型を使っている

これに関してはUnityのドキュメントを参照すると詳しく書いてありますが、Serialize属性をつけるかシリアライズ可能な型を使うことで回避できます。
https://docs.unity3d.com/ja/current/Manual/script-Serialization.html

  • Serializable 属性をもつカスタムの非抽象クラスと非ジェネリッククラス
  • Serializable 属性をもつカスタム構造体
  • UnityEngine.Object から派生するオブジェクトへの参照
  • プリミティブなデータ型 (int、float、double、bool、string など)
  • Enum 型
  • 特定の Unity ビルトイン型: Vector2、Vector3、Vector4、Rect、Quaternion、Matrix4x4、Color、Color32、LayerMask、AnimationCurve、Gradient、RectOffset、GUIStyle

Sceneが値の変更を検知していない(本題)

ここからが本題ですがまず問題のコードを見てください。

問題のコード

Testclass.cs
using UnityEngine;
using UnityEditor;
public class Testclass : MonoBehaviour
{
    [HideInInspector]
    public int hoge = 0;
}

[CustomEditor(typeof(Testclass))]
public class TestclassInspector : Editor {
    public override void OnInspectorGUI() {
        var obj = target as Testclass;
        base.OnInspectorGUI();
        obj.hoge = EditorGUILayout.IntField(obj.hoge);
    }
}

Testclassはint型の変数hogeを持っています。customEditorから値を操作するためにpublicにした上で[HideInInspector]でInspector上で非表示にしていますが、プリミティブ型なのでserializeされているはず。

適当なオブジェクトにアタッチして
image.png
値を変更。Ctrl+Sで保存
image.png
Unityを再起動。値は変更前に戻ってしまっています。:sweat_drops:
image.png

原因

このようなことが起きる理由はEditorGUILayout.〇〇Fieldで変更したときにsceneに変更された情報が反映されておらず、保存できてないからです。
反映されたかどうかはHierarchy上のシーン名の隣に*がつくことでわかります。
例えば、値を9999にしたあとTransformのPositionのxをちょっとだけ動かすことで*がつきました。
image.png

保存してUnityを再起動すると値も保存されてたことがわかります。
image.png

なぜこういうことがおきるのか

EditorからもとのコンポーネントにアクセスするにはSerializedObjectを通す必要があり、EditorGUILayout.〇〇Fieldで取得した値を再代入する方法それを介さないはSceneに変更が通知されないということでしょう。
ただ、SerializedPropertyを使用したアクセスの方法は、クラスや配列を扱うのが大変なのでできればEditorGUILayout.〇〇Fieldで扱いたいわけです。

解決法

Testclass.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
public class Testclass : MonoBehaviour
{
    [HideInInspector]
    public int hoge = 0;
}

[CustomEditor(typeof(Testclass))]
public class TestclassInspector : Editor {
    public override void OnInspectorGUI() {
        var obj = target as Testclass;
        base.OnInspectorGUI();

        EditorGUI.BeginChangeCheck();

        obj.hoge = EditorGUILayout.IntField(obj.hoge);

        if (EditorGUI.EndChangeCheck())
        {
            var scene = SceneManager.GetActiveScene();
            EditorSceneManager.MarkSceneDirty(scene);
        }
    }
}

解説

まず必要な名前空間を呼び出し

using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;

値を変更する部分をEditorGUI.BeginChangeCheck();とEditorGUI.EndChangeCheck()ではさみます。
変更時にEndChangeCheck()がtrueを返すので、そのたびにEditorScenemanager.MarkSceneDiryを使ってシーンが汚れたことを手動で書き込みます。

        EditorGUI.BeginChangeCheck();

        obj.hoge = EditorGUILayout.IntField(obj.hoge);

        if (EditorGUI.EndChangeCheck())
        {
            var scene = SceneManager.GetActiveScene();
            EditorSceneManager.MarkSceneDirty(scene);
        }

結論

EditorGUILayout.〇〇Fieldを使う場合は必ずMarkSceneDirtyしましょう。

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

Babylon.js で Oculus Quest で気軽に入れる VRクラブを作る (2)

はじめに

本記事の内容は :arrow_down: の続きになります。
Babylon.js で Oculus Quest で気軽に入れる VRクラブを作る (1)

前回は Oculus Quest で動き回れる VR クラブを作成しました :night_with_stars:
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f38363037302f38623338343261372d623732652d363162312d633765382d3933336437626138633031352e676966.gif

今回はもっと VR 的な演出を盛り込んだり、実際に音楽を再生したりして、
実際に盛り上がれる感じの空間として作り込んでいきたいと思います :sunglasses: :cd:

また、今回は音楽再生や波形解析のために Tone.js を使用します :musical_note:

動作環境

  • ホスティングサーバ
    • Node.js v11.14.0
    • Express 4.17.1
  • フロントエンド
    • Babylon.js 4.0.3
    • Tone.js 13.8.18

音楽の再生/停止を Babylon.js で制御出来るようにする

VR クラブ内で再生する音楽を 3D空間上から制御できるようにします :control_knobs:

1. Tone.js を使用して音楽を再生してみる

まずは VR クラブにマッチしそうな音楽を選定します :white_check_mark:

僕は Sonic Pi で作った音源を BGM にすることにしました :loud_sound:

ファイルフォーマットは mp3 にしました。
audio フォルダを作成して music.mp3 を配置しています :arrow_down:

babylon-js/public
.
├── audio # 新たに audio フォルダを作成し music.mp3 ファイルを配置
│   └── music.mp3
├── images
├── index.html
├── javascripts
├── shaders
└── stylesheets

それでは 前回作成した index.html に Tone.js を CDN 経由で取り込み、
実際に用意した mp3 ファイルを再生してみましょう:thumbsup:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <!-- Tone.js を CDN 経由で取り込む -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>
    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // Tone.js の Player を使用して、用意した mp3 の再生を準備する
        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).toMaster();

        // mp3 ファイルのロード完了後、自動で mp3 の再生を開始する
        Tone.Buffer.on('load', function () {
            player.start();
        });

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);
            scene.render();

            time += 0.01;
        });

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

index.html を更新して、npm run start を実行後 http://localhost:3000 にアクセスした際に音楽が流れていれば成功です :tada:

2. Babylon.js で 3D空間上に再生/停止を制御するためのボタンを配置する

次に音楽の再生に関わる操作を 3D 空間上で行えるように、再生/停止ボタンを配置することにします :play_pause:
Babylon.js には 3D 空間上で GUI を作成するための ライブラリ が用意されているため、
それを活用することで簡単に 3D空間上にクリック可能なボタンを配置出来ます。

それでは早速 3D空間上にボタンを配置し、
クリックすることで音楽の再生/停止が切り替えられるようにします :arrow_down:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>

    <!-- Babylon.js の 3D GUI プラグインを CDN 経由で取り込む -->
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>
    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // Tone.js の Player を使用して、用意した mp3 の再生を準備する
        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).toMaster();

        // 画面上に表示するボタンをクリックした際のコールバック関数
        // 既に Player が mp3 を読み込み済みの時に、
        // 音楽が既に再生されていれば停止、停止されていたら再生を行う
        const onPlayPauseButtonClicked = () => {
            if (player.loaded) {
                if (player.state == 'started') {
                    player.stop()
                } else {
                    player.start()
                }
            }
        }

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            // Babylon.js の GUI ライブラリを使用して、ユーザがクリック可能なボタンを作成する
            // ボタンの名前は PlayPauseButton としている
            const manager = new BABYLON.GUI.GUI3DManager(scene);
            const playPauseButton = new BABYLON.GUI.HolographicButton('PlayPauseButton');
            manager.addControl(playPauseButton);

            // playPauseButton ボタンのポジションを y: -0.2m, z: -5m に配置する
            playPauseButton.position.y = -0.2;
            playPauseButton.position.z = -5;

            // Y軸座標で 180度回転させておく (後に設定するテキストが正面から見られるようにするため)
            playPauseButton.mesh.rotate(BABYLON.Axis.Y, Math.PI, BABYLON.Space.WORLD);

            // playPauseButton ボタンのテキストに Play/Pause というテキストを設定する
            playPauseButton.text = "Play/Pause";
            playPauseButton.onPointerUpObservable.add(onPlayPauseButtonClicked);

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);
            scene.render();

            time += 0.01;
        });

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

index.html を書き換えたら、再び http://localhost:3000 にアクセスしてみます :earth_africa:

スクリーンショット 2019-07-13 19.51.55.png

画面中央に Play/Pause というテキストが設定されたボタンが出現し、
クリックすることで音楽の再生/停止が切り替われば成功です :exclamation: :clap:

音楽の波形解析データを利用してクラブっぽい演出を VR空間に取り入れる

Tone.js では再生中の音源の波形解析をしてデータを取得することが可能です。
波形解析したデータを元に Babylon.js 内の世界に変化をもたらすことで、
より音楽にマッチしたカッコいい空間を演出することが可能になります :dancers:

1. Tone.js を使って再生中の音楽の波形データを取得する

Tone.js にはデフォで波形解析を行うための各種クラスが用意されているため、
簡単に波形解析を行うことが可能です :thumbsup:

実際に再生している mp3 ファイルの波形解析してデータを取得してみます :arrow_down:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>

    <!-- Babylon.js の 3D GUI プラグインを CDN 経由で取り込む -->
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>
    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // 音楽の入力信号を RMS で取得出来るようにする
        const meter = new Tone.Meter('level');

        // 音楽の波形をフーリエ変換して音の高さごとのスペクトラムを取得する
        // 音の高低の強さが getValue 関数を使用すると配列として取得出来るイメージ
        // 引数の 64 は分解能 (高低の分割数) を指す
        const fft = new Tone.Analyser('fft', 64);

        // ↓ のような音楽の波形を取得する
        // https://info.shimamura.co.jp/digital/knowledge/2014/03/19260/2
        // 引数の 256 は分割数を (波形の分割数) を指す
        const waveform = new Tone.Analyser('waveform', 256);

        // 1秒ごとに再生中の音楽の波形データをログで出力する
        setInterval(() => {
            if (player.state == 'started')
                console.log(meter.getValue(), fft.getValue(), waveform.getValue());
        }, 1 * 1000) // 1s

        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).connect(meter).fan(fft, waveform).toMaster();

        const onPlayPauseButtonClicked = () => {
            if (player.loaded) {
                if (player.state == 'started') {
                    player.stop()
                } else {
                    player.start()
                }
            }
        }

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            const manager = new BABYLON.GUI.GUI3DManager(scene);
            const playPauseButton = new BABYLON.GUI.HolographicButton('playPauseButton');
            manager.addControl(playPauseButton);

            playPauseButton.position.y = -0.2;
            playPauseButton.position.z = -5;

            playPauseButton.mesh.rotate(BABYLON.Axis.Y, Math.PI, BABYLON.Space.WORLD);

            playPauseButton.text = "Play/Pause";
            playPauseButton.onPointerUpObservable.add(onPlayPauseButtonClicked);

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);
            scene.render();

            time += 0.01;
        });

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

index.html を書き換えたら、http://localhost:3000 にアクセスします。

console.log で波形データを出力しているため、開発者を開き出力について確認出来るようにします :white_check_mark:
スクリーンショット 2019-07-13 22.53.25.png

謎の小数が大量に表示されていれば成功です :thumbsup:
これらの値は音楽の再生状況に応じて刻々と変化していきます :wavy_dash:

解析データを元にオブジェクトやシェーダーの値を変化させることで、
音楽にマッチした効果を VR 空間上に取り入れる事が可能となります :notes:

2. Tone.js で波形解析したデータを元に VR空間が盛り上がる仕掛けを作る

波形解析は出来たので、あとはフィーリングで VR クラブの中が盛り上がりそうな仕掛けを盛り込んでいきましょう :exclamation: :fireworks:

1. 音の強さに応じて立方体を躍動させる

まずは、音の強さに応じて立方体を躍動させてみたいと思います :black_square_button:
先程の index.html 内にあった Tone.Meter を使用して実現します :arrow_down:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>
    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // 音楽の入力信号を RMS で取得出来るようにする
        const meter = new Tone.Meter('level');

        // 音楽の波形をフーリエ変換して音の高さごとのスペクトラムを取得する
        // 音の高低の強さが getValue 関数を使用すると配列として取得出来るイメージ
        // 引数の 64 は分解能 (高低の分割数) を指す
        const fft = new Tone.Analyser('fft', 64);

        // ↓ のような音楽の波形を取得する
        // https://info.shimamura.co.jp/digital/knowledge/2014/03/19260/2
        // 引数の 256 は分割数を (波形の分割数) を指す
        const waveform = new Tone.Analyser('waveform', 256);

        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).connect(meter).fan(fft, waveform).toMaster();

        const onPlayPauseButtonClicked = () => {
            if (player.loaded) {
                if (player.state == 'started') {
                    player.stop()
                } else {
                    player.start()
                }
            }
        }

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('Floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            const manager = new BABYLON.GUI.GUI3DManager(scene);
            const playPauseButton = new BABYLON.GUI.HolographicButton('PlayPauseButton');
            manager.addControl(playPauseButton);

            playPauseButton.position.y = -0.2;
            playPauseButton.position.z = -5;

            playPauseButton.mesh.rotate(BABYLON.Axis.Y, Math.PI, BABYLON.Space.WORLD);

            playPauseButton.text = "Play/Pause";
            playPauseButton.onPointerUpObservable.add(onPlayPauseButtonClicked);

            // Tone.Meter の値を元に躍動する立方体 MeterBox を作成する
            // 初期位置は原点から 10m 頭上の場所に設置
            const meterBox = BABYLON.MeshBuilder.CreateBox('MeterBox', { size: 1 }, scene);
            meterBox.position = new BABYLON.Vector3(0, 10, 0);

            // MeterBox をカッコよく見せるための各種カラー設定を行うための Material 作成
            const meterBoxMaterial = new BABYLON.StandardMaterial('MeterBoxMaterial', scene);

            // MeterBox の表面色の設定
            meterBoxMaterial.diffuseColor = new BABYLON.Color3(1, 0, 0);

            // 特定の方向から来る光を MeterBox が反射するための設定
            meterBoxMaterial.specularColor = new BABYLON.Color3(0.5, -0.5, 0.7);

            // MeterBox 自身が発光する光の設定
            meterBoxMaterial.emissiveColor = new BABYLON.Color3(0.6, 0.7, 0.8);

            // 全体的な MeterBox の明るさ設定
            meterBoxMaterial.ambientColor = new BABYLON.Color3(1, 1, 1);

            meterBox.material = meterBoxMaterial;

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')

        // Tone.Meter の値に応じて変化する MeterBox の Mesh 取得
        const meterBox = scene.getMeshByName('MeterBox')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);
            scene.render();

            // Tone.Meter で取得した値を元に MeterBox のスケールを最大で 15倍にする
            const rms = meter.getValue() * 14 + 1;
            meterBox.scaling = new BABYLON.Vector3(rms, rms, rms);

            // 常に X軸方向と Y軸方向に回転させておく設定
            meterBox.rotation.x += 0.01;
            meterBox.rotation.y += 0.01;

            time += 0.01;
        });

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

http://localhost:3000 にアクセスしてみます :arrow_down:

cubee.gif

頭上で躍動している立方体の存在が確認出来ていれば OK です :thumbsup:

2. 音の高低に応じて地面を躍動させる

次に Tone.Analyser('fft', 64) で取得した音の高低スペクトラムを使用して、
地面を音の高低に応じて動的に躍動させるようにしてみます :thumbsup:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>
    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // 音楽の入力信号を RMS で取得出来るようにする
        const meter = new Tone.Meter('level');

        // 音楽の波形をフーリエ変換して音の高さごとのスペクトラムを取得する
        // 音の高低の強さが getValue 関数を使用すると配列として取得出来るイメージ
        // 引数の 64 は分解能 (高低の分割数) を指す
        const fft = new Tone.Analyser('fft', 64);

        // ↓ のような音楽の波形を取得する
        // https://info.shimamura.co.jp/digital/knowledge/2014/03/19260/2
        // 引数の 256 は分割数を (波形の分割数) を指す
        const waveform = new Tone.Analyser('waveform', 256);

        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).connect(meter).fan(fft, waveform).toMaster();

        const onPlayPauseButtonClicked = () => {
            if (player.loaded) {
                if (player.state == 'started') {
                    player.stop()
                } else {
                    player.start()
                }
            }
        }

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('Floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            const manager = new BABYLON.GUI.GUI3DManager(scene);
            const playPauseButton = new BABYLON.GUI.HolographicButton('PlayPauseButton');
            manager.addControl(playPauseButton);

            playPauseButton.position.y = -0.2;
            playPauseButton.position.z = -5;

            playPauseButton.mesh.rotate(BABYLON.Axis.Y, Math.PI, BABYLON.Space.WORLD);

            playPauseButton.text = "Play/Pause";
            playPauseButton.onPointerUpObservable.add(onPlayPauseButtonClicked);

            const meterBox = BABYLON.MeshBuilder.CreateBox('MeterBox', { size: 1 }, scene);
            meterBox.position = new BABYLON.Vector3(0, 10, 0);

            const meterBoxMaterial = new BABYLON.StandardMaterial('MeterBoxMaterial', scene);

            meterBoxMaterial.diffuseColor = new BABYLON.Color3(1, 0, 0);
            meterBoxMaterial.specularColor = new BABYLON.Color3(0.5, -0.5, 0.7);
            meterBoxMaterial.emissiveColor = new BABYLON.Color3(0.6, 0.7, 0.8);
            meterBoxMaterial.ambientColor = new BABYLON.Color3(1, 1, 1);

            meterBox.material = meterBoxMaterial;

            // 表面が赤色のマテリアルを作成する
            const redMaterial = new BABYLON.StandardMaterial(scene);
            redMaterial.diffuseColor = new BABYLON.Color3(1.0, 0, 0);

            // 表面が緑色のマテリアルを作成する
            const greenMaterial = new BABYLON.StandardMaterial(scene);
            greenMaterial.diffuseColor = new BABYLON.Color3(0, 1.0, 0);

            // 表面が青色のマテリアルを作成する
            const blueMaterial = new BABYLON.StandardMaterial(scene);
            blueMaterial.diffuseColor = new BABYLON.Color3(0, 0, 1.0);

            // Tone.Analyser('fft', 64) で分解能が 64 のため、64 個のメッシュで床を作成する
            const iteration = 8; // 8 * 8 = 64 (fft.size);
            for (var i = 0; i < iteration; i++) {
                for (var j = 0; j < iteration; j++) {
                    // VR クラブのサイズを取得 (x, y, z 同サイズの立方体のため適当に x のサイズを取得)
                    const boxSize = box.getBoundingInfo().boundingBox.extendSize.x;

                    // VR クラブのサイズを分割して 64 個のメッシュが敷き詰められるように、
                    // 各メッシュのポジションの x と z の値を算出する
                    const splitSize = boxSize / iteration;
                    const splitX = splitSize * i * 2 - boxSize + 1;
                    const splitZ = splitSize * j * 2 - boxSize + 1;

                    // 今回はシリンダーを敷き詰めて床にする
                    // 各シリンダーは算出した x と z の位置に配置していく
                    const groundCylinder = BABYLON.MeshBuilder.CreateCylinder('GroundCylinder', { size: 1 }, scene);
                    groundCylinder.position = new BABYLON.Vector3(splitX, -8, splitZ);
                    groundCylinder.scaling = new BABYLON.Vector3(2, 2, 2);

                    // 何番目に作成したシリンダーかを判定して、
                    // 赤緑青の三色で各シリンダーの色を規則的に設定する
                    const no = (i * iteration + j) % 3;
                    switch (no) {
                        case 0: groundCylinder.material = redMaterial; break;
                        case 1: groundCylinder.material = greenMaterial; break;
                        case 2: groundCylinder.material = blueMaterial; break;
                    }
                }
            }

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')
        const meterBox = scene.getMeshByName('MeterBox')
        const groundCylinders = scene.getMeshesByID('GroundCylinder')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);
            scene.render();

            const rms = meter.getValue() * 14 + 1;
            meterBox.scaling = new BABYLON.Vector3(rms, rms, rms);

            meterBox.rotation.x += 0.01;
            meterBox.rotation.y += 0.01;

            // 音楽が再生されていれば音の高低の強さに応じて
            // 各シリンダー(床) を上下に躍動させる
            if (player.state == 'started') {
                const fftValues = fft.getValue();
                for (var i = 0; i < fft.size; i++) {
                    groundCylinders[i].position.y = fftValues[i] * 0.05;
                }
            }

            time += 0.01;
        });

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

http://localhost:3000 にアクセスしてみます :arrow_down:

ground.gif

:arrow_up: のように音楽に合わせて床が躍動していれば成功です :clap: :tada:

Tone.js で取得した音楽の波形を元にパーティクルを発生させる

Babylon.js には Solid Particle System (SPS) という仕組みが備わっており、
Mesh をパーティクルとして扱うことが可能です :globe_with_meridians:

今回は SPS を使用して、よりパーティー感のある演出を入れていきます :sparkles:
具体的に言うと SPS を使用して波形の形を表現してみたいと思います :wavy_dash:

babylon-js/public/index.html
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8" />
    <title>Babylon Club</title>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.18/Tone.min.js"></script>

    <style>
        html,
        body {
            overflow: hidden;
            width: 100%;

            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>

<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);

        // 音楽の入力信号を RMS で取得出来るようにする
        const meter = new Tone.Meter('level');

        // 音楽の波形をフーリエ変換して音の高さごとのスペクトラムを取得する
        // 音の高低の強さが getValue 関数を使用すると配列として取得出来るイメージ
        // 引数の 64 は分解能 (高低の分割数) を指す
        const fft = new Tone.Analyser('fft', 64);

        // ↓ のような音楽の波形を取得する
        // https://info.shimamura.co.jp/digital/knowledge/2014/03/19260/2
        // 引数の 256 は分割数を (波形の分割数) を指す
        const waveform = new Tone.Analyser('waveform', 256);

        const player = new Tone.Player({
            url: '/audio/music.mp3',
            loop: true,
        }).connect(meter).fan(fft, waveform).toMaster();

        const onPlayPauseButtonClicked = () => {
            if (player.loaded) {
                if (player.state == 'started') {
                    player.stop()
                } else {
                    player.start()
                }
            }
        }

        const createScene = () => {
            const scene = new BABYLON.Scene(engine);

            const camera = new BABYLON.FreeCamera('MainCamera', new BABYLON.Vector3(0, 0, 0), scene);
            camera.setTarget(BABYLON.Vector3.Zero());
            camera.attachControl(canvas, true);

            const box = BABYLON.MeshBuilder.CreateBox('Box', { size: 20, sideOrientation: BABYLON.Mesh.BACKSIDE }, scene);
            box.position = BABYLON.Vector3.Zero();

            const shaderMaterial = new BABYLON.ShaderMaterial('Floor', scene, './shaders/floor');
            box.material = shaderMaterial;

            const light = new BABYLON.HemisphericLight('AmbientLight', new BABYLON.Vector3(0, 0, 0), scene);

            const manager = new BABYLON.GUI.GUI3DManager(scene);
            const playPauseButton = new BABYLON.GUI.HolographicButton('PlayPauseButton');
            manager.addControl(playPauseButton);

            playPauseButton.position.y = -0.2;
            playPauseButton.position.z = -5;

            playPauseButton.mesh.rotate(BABYLON.Axis.Y, Math.PI, BABYLON.Space.WORLD);

            playPauseButton.text = "Play/Pause";
            playPauseButton.onPointerUpObservable.add(onPlayPauseButtonClicked);

            const meterBox = BABYLON.MeshBuilder.CreateBox('MeterBox', { size: 1 }, scene);
            meterBox.position = new BABYLON.Vector3(0, 7, 0);

            const meterBoxMaterial = new BABYLON.StandardMaterial('MeterBoxMaterial', scene);

            meterBoxMaterial.diffuseColor = new BABYLON.Color3(1, 0, 0);
            meterBoxMaterial.specularColor = new BABYLON.Color3(0.5, -0.5, 0.7);
            meterBoxMaterial.emissiveColor = new BABYLON.Color3(0.6, 0.7, 0.8);
            meterBoxMaterial.ambientColor = new BABYLON.Color3(1, 1, 1);

            meterBox.material = meterBoxMaterial;

            const redMaterial = new BABYLON.StandardMaterial(scene);
            redMaterial.diffuseColor = new BABYLON.Color3(1.0, 0, 0);

            const greenMaterial = new BABYLON.StandardMaterial(scene);
            greenMaterial.diffuseColor = new BABYLON.Color3(0, 1.0, 0);

            const blueMaterial = new BABYLON.StandardMaterial(scene);
            blueMaterial.diffuseColor = new BABYLON.Color3(0, 0, 1.0);

            const boxSize = box.getBoundingInfo().boundingBox.extendSize.x;

            const iteration = 8; // 8 * 8 = 64 (fft.size);
            for (var i = 0; i < iteration; i++) {
                for (var j = 0; j < iteration; j++) {
                    const splitSize = boxSize / iteration;
                    const splitX = splitSize * i * 2 - boxSize + 1;
                    const splitZ = splitSize * j * 2 - boxSize + 1;

                    const groundCylinder = BABYLON.MeshBuilder.CreateCylinder('GroundCylinder', { size: 1 }, scene);
                    groundCylinder.position = new BABYLON.Vector3(splitX, -10, splitZ);
                    groundCylinder.scaling = new BABYLON.Vector3(2, 2, 2);

                    const no = (i * iteration + j) % 3;
                    switch (no) {
                        case 0: groundCylinder.material = redMaterial; break;
                        case 1: groundCylinder.material = greenMaterial; break;
                        case 2: groundCylinder.material = blueMaterial; break;
                    }
                }
            }

            return scene;
        }

        const scene = createScene();

        const vrHelper = scene.createDefaultVRExperience();
        vrHelper.enableInteractions();

        var _rotationAngle = 0;
        vrHelper.onControllerMeshLoaded.add((webVRController) => {
            var isHorizontalRotate = false;
            const angle = Math.PI / 4;
            webVRController.onPadValuesChangedObservable.add((stateObject) => {
                if (webVRController.hand === 'left') {
                    var matrix = BABYLON.Matrix.RotationAxis(BABYLON.Axis.Y, angle * _rotationAngle);
                    var move = new BABYLON.Vector3(stateObject.x, 0, -stateObject.y);
                    var addPos = BABYLON.Vector3.TransformCoordinates(move, matrix);
                    vrHelper.position = vrHelper.position.add(addPos)
                } else {
                    if (isHorizontalRotate && Math.abs(stateObject.x) > 0.8) {
                        isHorizontalRotate = false;
                        stateObject.x > 0 ? _rotationAngle++ : _rotationAngle--;

                        var target = BABYLON.Quaternion.FromRotationMatrix(BABYLON.Matrix.RotationY(angle * _rotationAngle));
                        vrHelper.currentVRCamera.rotationQuaternion = target
                    } else if (Math.abs(stateObject.x) < 0.8) {
                        isHorizontalRotate = true
                    }
                }
            });
        });

        const box = scene.getMeshByName('Box')
        const meterBox = scene.getMeshByName('MeterBox')
        const groundCylinders = scene.getMeshesByID('GroundCylinder')

        var time = 0.0;
        engine.runRenderLoop(() => {
            box.material.setFloat('time', time);

            const rms = meter.getValue() * 14 + 1;
            meterBox.scaling = new BABYLON.Vector3(rms, rms, rms);

            meterBox.rotation.x += 0.01;
            meterBox.rotation.y += 0.01;

            if (player.state == 'started') {
                const fftValues = fft.getValue();

                for (var i = 0; i < fft.size; i++) {
                    groundCylinders[i].position.y = fftValues[i] * 0.05;
                }
            }

            scene.render();
            time += 0.01;
        });

        // Solid Particle System (SPS) の使用を宣言する
        const SPS = new BABYLON.SolidParticleSystem('SPS', scene);

        // ビルボードを有効にする (負荷軽減のため)
        // http://nn-hokuson.hatenablog.com/entry/2017/03/24/211211
        SPS.billboard = true;

        // パーティクル自体は回転させず、カラーもテクスチャのアップデートしない (負荷軽減のため)
        SPS.computeParticleRotation = false;
        SPS.computeParticleColor = false;
        SPS.computeParticleTexture = false;

        // 波形を可視化するために使用する平面 WaveformPlain をサイズ 0.1m で作成する
        const waveformPlane = BABYLON.Mesh.CreatePlane('WaveformPlain', 0.1, scene)

        const boxSize = box.getBoundingInfo().boundingBox.extendSize.x;

        // SPS で平面 WaveformPlain を波形解析の分解能 (256個) 扱えるようにする
        // 各平面は VR クラブ内の横幅いっぱいになるように配置される
        // 波形の様子が見やすくするように平面群はカメラの正面に配置して、
        // 縦幅を 3倍にして、色を配置場所に応じて変化させている
        SPS.addShape(waveformPlane, waveform.size, {
            positionFunction: function (particle, i, s) {
                particle.position.x = (s / waveform.size) * boxSize * 2 - boxSize;
                particle.position.z = -8;
                particle.scaling.y = 3.0

                particle.color = new BABYLON.Color4(
                    particle.position.y / boxSize + 0.5,
                    particle.position.z / boxSize + 0.5,
                    particle.position.x / boxSize + 0.5,
                    1.0
                );
            }
        });

        // WaveformPlain はもう使用しないため破棄する
        waveformPlane.dispose();

        // SPS に追加した平面群を扱う Mesh を作成する
        const spsMesh = SPS.buildMesh();

        // SPS のパーティクルアップデート前に
        // パーティクルを変化させるのに使用する波形データを
        // waveformValues 変数に保持しておく
        // (updateParticle で waveform.getValue() を大量に実行したくないため)
        var waveformValues;
        SPS.beforeUpdateParticles = function () {
            waveformValues = waveform.getValue();
        }

        // SPS のパーティクルを全てアップデートする
        SPS.updateParticle = function (particle) {
            // パーティクルのグローバルインデックスは idx という変数で取得可能
            const waveformValue = waveformValues[particle.idx]
            particle.position.x = (particle.idx / waveform.size) * boxSize * 2 - boxSize;

            // 波を表現するためにパーティクルの位置 Y を波形に合わせて上下させる
            particle.position.y = waveformValue * boxSize;

            particle.color = new BABYLON.Color4(
                particle.position.y / boxSize + 0.5,
                particle.position.z / boxSize + 0.5,
                particle.position.x / boxSize + 0.5,
                1.0
            );
        }

        // シーンのレンダリング前に SPS で管理している
        // パーティクルをアップデートする (SPS.updateParticle が呼ばれる)
        scene.registerBeforeRender(() => {
            SPS.setParticles();
        })

        window.addEventListener('resize', function () {
            engine.resize();
        });
    </script>
</body>

</html>

http://localhost:3000 にアクセスしてみます :arrow_down:

end.gif

音楽を再生した時に目の前の平面の行列が波形っぽく動いていれば成功です :clap:
ある程度音楽に合わせた演出が出来てきたので、実際に Quest で見てみましょう :sunglasses:

Quest で動作確認する

それでは 前回と同様の手順 で現状の VR クラブ内の様子を Quest で確認します :white_check_mark:

endd.gif

無事に Quest 内でも見られました :exclamation: :clap:

多少パフォーマンスにも気を使いながら改修してきたので、
Quest でもちゃんと FPS は安定して出てるように感じました :thumbsup:

Quest で見た感じ躍動する床のシリンダーがそれなりに迫力ありましたが、
目の前の波形や頭上の躍動する立方体はそれっぽい感があるだけで
あまり迫力や臨場感は感じませんでした :boom:

おわりに

今回は Tone.js を使って実際に音楽にマッチした演出を導入してみました :musical_note:

本当は波形解析したデータを利用して、更にダイナミックな演出を取り入れる予定だったのですが、
めちゃくちゃ本質とは関係ないところで時間を食ってしまいそうだったため断念しました :boom: :upside_down:

次回は折角 Tone.js を使用しているので音楽にリバーブ掛けてみたり、
ユーザが何らかのアクションを起こすと音楽に介入できる仕組みを作る予定です :bangbang:

参考リンク

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

Unityの状態管理フレームワークを作ってみた

「Unityで状態管理するのしんどくね??」
ってなったので状態管理をするフレームワークを作ってみました.

作った経緯

最近結構Webフロントエンドの開発をメインにやっているのですが, 先日SPAJAM東北予選にて久々にUnityを使ったのですが 「状態管理しんどくない??」 「UIに値反映するのダルすぎなんだけど??」 っと見事にWebフロントエンドのフレームワーク慣れしてしまった体が拒否反応を起こしてしまったので後日,状態管理フレームワークを作ってみました.

似たようなものに「Unidux」っというReduxをUnityで実装しているものもあるのですが,個人的に他ライブラリへの依存が嫌だったの自分でも作ってみました.

参考にしたもの

Webフロントエンドの状態管理アーキテクチャ・フレームワークというと 「Flux」や「Redux」などがありますが,今回は「The Elm Architecture」をリスペクトして実装しました.

出来たもの

その名も UniTEA です.

使い方

おそらく一番簡単なカウンターアプリの実装を見ながら使い方を紹介していきます.

0. インストール方法

Unity Package Manager を利用しているのでプロジェクトルート以下にある Packages/manifest.json

{
  "dependencies" : {
    ...
    "com.uzimaru0000.unitea": "https://github.com/uzimaru0000/UniTEA.git",
    ...
  }
}

と追記してエディタに行くとインストールされます.

1. Modelとなる構造体を作る

アプリケーションの状態となるModelを定義します.
今回はカウンターアプリなので以下のようなものでいいでしょう

public struct Model
{
  public int count;
}

注意してほしいのは構造体で定義してください.

2. Modelの変更を伝えるMessageを作る

Modelの変更をフレームワーク側に伝えるMessageを定義します.
今回は

  • カウントアップ
  • カウントダウン

の2つでいいと思います.

public enum Msg
{
    Increase,
    Decrease
}

enumで定義してください.

3. Messageをラップしたclassを作る

Messageをラップしたclassを作成します.
このとき,このclassには IMessenger<Msg> というインターフェースを実装してください.
Msg GetMessage() というメソッドを実装すれば大丈夫です.返り値にはで定義したMessageを返してください.

using UniTEA;

public class IncreaseMsg : IMessenger<Msg>
{
    public Msg GetMessage() => Msg.Increase;
}

public class DecreaseMsg : IMessenger<Msg>
{
    public Msg GetMessage() => Msg.Decrease;
}

4. Updaterを作る

ModelをUpdateするためのメソッドを定義したclassを作成します.
IUpdater<Model, Msg> というインターフェースを実装してください.
このインターフェイスは (Model, Cmd<Msg>) Update(IMessenger<Msg> msg, Model model) というメソッドを定義します.C# 7から実装された Tuple です. Cmd<Msg> は非同期処理を表す型です.今回は Cmd<Msg>.noneという値を使います.

using UniTEA;

public class Updater : IUpdater<Model, Msg>
{
    public (Model, Cmd<Msg>) Update(IMessenger<Msg> msg, Model model)
    {
        switch(msg)
        {
            case IncreaseMsg _:
                return (new Model
                {
                    count = model.count + 1
                }, Cmd<Msg>.none);

            case DecreaseMsg _:
                return (new Model
                {
                    count = model.count - 1
                }, Cmd<Msg>.none);

            default:
                return (model, Cmd<Msg>.none);
        }
    }
}

状態が変わるときは新しいModelを生成しましょう.

5. Viewを更新するRendererを作る

ModelからView(UIなど)の見た目を変更するclassを作ります.
IRenderer<Model> というインターフェースを実装してください.
このインターフェイスは, void Render(Model model) というメソッドを実装します.

using UnityEngine;
using UnityEngine.UI;
using UniTEA;

public class Renderer : MonoBehaviour, IRenderer<Model>
{
    [SerializeField]
    Text display;

    public void Render(Model model)
    {
        display.text = model.count.ToString();
    }
}

今回はTextコンポーネントModelcountの値を入れているだけですね.

6. イベントを発火されるためのclassを作る

イベント発火用のDispatcherクラスを作ります. このclass自体は特に実装するインターフェイスは無いので任意のMonoBehaviourクラス等でイベントを設定したり,publicメソッドにしてuGUIのインスペクタから設定してもいいです.また,あとで出てくる UniTEA クラスを管理するクラスをインスペクタから設定する・シングルトンにする・DIライブラリを使う等で参照出来るようにしてください.

using UnityEngine;
using UnityEngine.UI;
using UniTEA;

public class Dispatcher : MonoBehaviour
{
    [SerializeField]
    Manager manager;

    [SerializeField]
    Button increaseButton;

    [SerializeField]
    Button decreaseButton;

    void Start()
    {
        increaseButton.onClick.AddListener(() => manager.Dispatch(new IncreaseMsg()));
        decreaseButton.onClick.AddListener(() => manager.Dispatch(new DecreaseMsg()));
    }
}

7. UniTEAクラスのマネージャークラスを作る

フレームワークのCoreであるUniTEAクラスを管理するマネージャークラスを作ります.
管理と言っても初期化とDispatchメソッドを公開するだけです.

using UniTEA;
using UnityEngine;

public class Manager : MonoBehaviour
{
    UniTEA<Model, Msg> _instance;
    UniTEA<Model, Msg> instance
    {
        get {
            if (_instance == null)
            {
                _instance = new UniTEA<Model, Msg>(
                                    () => (new Model(), Cmd<Msg>.none),
                                    new Updater(),
                                    renderer);
            }

            return _instance;
        }
    }

    [SerializeField]
    new Renderer renderer;

    public void Dispatch(IMessenger<Msg> msg)
    {
        instance.Dispatch(msg);
    }
}

8. Unityのオブジェクトにattachする

Renderer, Dispatcher, Manager を任意のGameObjectにattachしてインスペクターから依存のあるものを注入していきます.

これで以下のような動作をするカウンターができます!!

詳しい説明

Messageをラップする理由

Messageに値を含ませるためです.
例えばInputFieldに入力した値をMessageに含ませるとき,enumをMessageに使うと値をUpdateに伝えることができません.
なので以下のようなコードでMessageをラップしたクラスに値を含ませてあげます.

public enum Msg
{
    Input
}

public class InputMsg : IMessenger<Msg>
{
    public string value;

    public Msg GetMessage() => Msg.Input;

    public InputMsg(string value)
    {
        this.value = value;
    }
}

これでInputMsgに入力された値を含ませることができます.

ただ,値を含めるためのMessageを作る際に上のようなコードを書かなければいけないので UniTEA.Utils namespaceの中に OneValueMessage<T, U> というクラスがあるので

public class InputMsg : OnValueMessage<Msg, string>
{
    public override GetMessage() => Msg.Input;
    public InputMsg(string value): base(value) {}
}

っとすれば最初のコードと同じになります.

Cmd<Msg> って?

Cmd<Msg>型は非同期処理を表す型です.Task型をラップしている型になります.
作成するにはコンストラクタに Task<IMessenger<Msg>>を渡してあげます.

他にも質問があったらコメントでお願いします :pray:

最後に

最初の方でも述べましたがこのライブラリ自体は他のライブラリへの依存はないです.ですが,CmdにTaskを渡すために「UniTask」を使ったり,Dispatcherの部分に「UniRx」をつかったり,DIをするために「Zenject」を使うのはいいと思います.むしろそのほうがいいかもしれないです.

また,コアの部分の開発にそんなに時間がかかってないので「ここをもっとこうしたらいいと思う」「この部分はなんでこうなの?」っと言ったissue, PullRequest お待ちしてます!!

https://github.com/uzimaru0000/UniTEA

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

【Unity(C#)】オブジェクトに当たると途切れるレーザーの実装方法

レーザー

RayLineRendererで実装します。

Rayがオブジェクトに当たったかどうかの判定を行い、LineRendererの終点の位置を変えることで
レーザーの途切れを表現します。

そのためにはRayLineRendererの始点、終点を合わせておく必要があります。

RayとLineRendererの位置を合わせる
 void OnRay()
    {      
        float lazerDistance = 10f;
        Vector3 direction = hand.transform.forward * lazerDistance;
        Vector3 pos = hand.transform.position;

        RaycastHit hit;
        Ray ray = new Ray(pos, hand.transform.forward);
        lineRenderer.SetPosition(0,hand.transform.position);

        if (Physics.Raycast(ray, out hit, lazerDistance))
        {
            lineRenderer.SetPosition(1, pos + direction);
        }

        Debug.DrawRay(ray.origin, pos + direction, Color.red, 0.1f);
    }

画像のようにぴったりと合わせることができました。(赤い線=Ray,青い線=LineRenderer)
ray_Line.PNG

貫通してしまう

先程のコードのままでは画像のようにオブジェクトを貫通してしまいます。
Penetration.PNG

この問題を解決するうえで、RayLineRendererをぴったりと合わせたことが鍵となります。

Rayはオブジェクトとの衝突判定を行うことができます。
また、オブジェクトと衝突した座標を取得することもできます。

つまり、LineRendererの終点にRayが衝突した座標を当てはめれば
オブジェクトに当たった箇所でレーザー(LineRenderer)が途切れているように見せることができる
ということです。

コードに落とし込むとこうです。

        if (Physics.Raycast(ray, out hit, lazerDistance))
        {
            hitPos = hit.point; //オブジェクトとの衝突座標を取得
            lineRenderer.SetPosition(1, hitPos); //LineRendererの終点に当てはめる
        }

デモ

OculusQuestでビルドしたデモになります。
Ray_LineRenderer.gif

遮蔽物でレーザーが途切れているように見えます。ばっちりです。

デモにおいて、始点も少しずらしているので
始点のずらし方に関しても最終的なコードと合わせて記述していきます。

最終的なコード

using UnityEngine;

[RequireComponent(typeof(LineRenderer))]
public class Lazer : MonoBehaviour
{
    [SerializeField]
    GameObject hand;

    LineRenderer lineRenderer;
    Vector3 hitPos;
    Vector3 tmpPos;

    float lazerDistance = 10f;
    float lazerStartPointDistance = 0.15f;
    float lineWidth = 0.01f;

     void Reset()
    {
        lineRenderer = this.gameObject.GetComponent<LineRenderer>();
        lineRenderer.startWidth = lineWidth;
    }

    void Start()
    {
        lineRenderer = this.gameObject.GetComponent<LineRenderer>();
        lineRenderer.startWidth = lineWidth;
    }


    void Update()
    {
        OnRay();      
    }

    void OnRay()
    {      
        Vector3 direction = hand.transform.forward * lazerDistance;
        Vector3 rayStartPosition = hand.transform.forward*lazerStartPointDistance;
        Vector3 pos = hand.transform.position;
        RaycastHit hit;
        Ray ray = new Ray(pos+rayStartPosition, hand.transform.forward);

        lineRenderer.SetPosition(0,pos + rayStartPosition);

        if (Physics.Raycast(ray, out hit, lazerDistance))
        {
            hitPos = hit.point;
            lineRenderer.SetPosition(1, hitPos);
        }
        else
        {      
            lineRenderer.SetPosition(1, pos + direction);
        }

        Debug.DrawRay(ray.origin, ray.direction * 100, Color.red, 0.1f);

    }
}

始点をずらす

始点をずらしているのは下記の箇所です。

    Vector3 rayStartPosition = hand.transform.forward*lazerStartPointDistance;
    Ray ray = new Ray(pos+rayStartPosition, hand.transform.forward);
    Vector3 pos = hand.transform.position;
    lineRenderer.SetPosition(0,pos + rayStartPosition);

Shaderで始点をずらしているようにみせる1こともできますが、
今回は始点そのものの座標を調節してます。

手にコライダーが付いている状態で手のTransformを参照してRayを出した場合、
手のPivot、コライダーの大きさによっては
Rayが手のコライダーに衝突してしまって判定が取れない場合があります。
そもそものRayの始点をずらすことで意図しない衝突を回避できます。

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

低価格でも高クォリティな最強アセット13選!

この記事は「Unityアセット真夏のアドベントカレンダー2019」8月15日の記事になります。

Unity アセット真夏のアドベントカレンダー 2019 Summer!

私が現在持っているアセットの中で5~10ドル程のおすすめを紹介します!

目次

1.Modern UI Pack
2.Sun Flares
3.Materialize FX
4.Simple Physics Toolkit
5.Animal pack deluxe
6.Wet Road Materials
7.AdvancedMissile
8.Realistic Grass and Bush Pack2
9.Zero Gravity Part One
10.FPS Online Shooter Example
11.100 Post Processing Styles
12.Town Constructor Pack
13.Simple Mesh Cutter

1.Modern UI Pack

Modern UI Pack
カテゴリ:GUI
価格:$5

モダンでオサレなアニメーション付きUIパックです。

ボタン、アイコン、スイッチ、スライダー、ドロップダウン、テキストエリアなど多くの種類があります。
さらにHierarchy右クリックから簡単に作れます。
量がすごい!!!!
モダン1.PNG
デフォルトにはないようなスライダー
モダン2.PNG
とってもオシャレなアイコン付き!
モダン3.PNG

2.Sun Flares

Sun Flares
カテゴリ:テクスチャ&マテリアル
価格:4.99

太陽を見たときに見える丸い光のテクスチャです。
あれって光芒って言うらしいです。

昼バージョンと夕方バージョンがあります。
つけるだけでとてもそれっぽくなるので是非ご活用ください!

光らせたいオブジェクトにLens Flareコンポーネントをセットします。
フレア1.PNG

FlareにアセットのLensFlareをセットします。
フレア2.PNG

エディター上でも確認できます。
フレア4.PNG

夕方バージョン
フレア3.PNG

3.Materialize FX

Materialize FX
カテゴリ:シェーダー
価格:$9

崩壊や生成に使えるシェーダーです。魔法っぽいのとSFっぽい奴の2種類があります。

さらにノードベースのシェーダー作成アセットのAmplify Shader Editor に対応してるとのこと。

※注意 このアセットはシェーダーモデル3.0以上に対応しているGPUが必要です。

SFっぽく出現したり
魔法2.gif

振り下ろした瞬間に出現する武器など、様々な表現ができます。
魔法3.gif

4.Simple Physics Toolkit

Simple Physics Toolkit
カテゴリ:物理エンジン
価格:$11.99

様々な物理エンジンのスクリプト詰め合わせのアセットです。
磁石、爆発、推進エンジン、物体の発射、風、バウンドプレート、浮力、無重力、オブジェクトの破壊、インタラクティブアイテムの10種類です。

爆発
物理1.gif

磁石
物理2.gif


物理3.gif

5.Animal pack deluxe

Animal pack deluxe
カテゴリ:3D>キャラクター>動物
価格:$5

26種類の動物を追加するアセットです。テクスチャはPBR対応で、さらにすべての動物に待機、走る、歩く、攻撃、食べる、死ぬのアニメーション付き!
このクォリティで$5はとても安いと思います。

サメやワニ、牛
動物1.PNG
ヤギやクマ、ブタ
動物2.PNG
アヒル、ニワトリ、ウサギ、サケ
動物3.PNG
ヘビやネズミ、サソリ
動物4.PNG
カタツムリ、カエル、カニなどなど
動物5.PNG

ちなみにこのアセット作者さんは他の動物や恐竜、水生生物のアセットも5ドルで販売していま!

Animal pack deluxe v2

Underwater life deluxe

Dinosaur Pack 1.0

6.Wet Road Materials

Wet Road Materials
カテゴリ:2D>テクスチャ&マテリアル>道路
価格:$4.99

濡れた道路を追加するアセットです。
2048pxの高解像度テクスチャでとてもリアルな濡れた道路が表現可能!

マテリアルのパラメータを変えるだけで簡単に道路の質感を変えることが可能です。
道路1.PNG

もっと水たまりを増やしたり。
道路2.PNG

完全に濡れた道路も作れたりします。
道路3.PNG

7.AdvancedMissile

AdvancedMissile
カテゴリ:ツール
価格:$5

日本人の方が作成したミサイルを追加するアセットです。
通常のミサイルの他に、誘導ミサイルや
カクカクに動くSFっぽい挙動までパラメータを調整することで簡単にできます!
日本語ドキュメントが付いているのもとてもありがたいです!

通常の無誘導ミサイルや
ミサイル1.PNG

誘導ミサイル
ミサイル2.PNG

ちょっと変わった誘導の仕方など
ミサイル3.PNG

パラメーターをいじくるだけで簡単に実現可能です!
ミサイル4.PNG

8.Realistic Grass and Bush Pack2

Realistic Grass and Bush Pack2
カテゴリ:3D>植物>草木
価格:$4.99

このアセットは低ポリでリアルな草花を生やすことができます。

三種類の草
草1.PNG

三種類の花のセットになっています。
草2.PNG

ツリーとしてテレインで使うこともできます。
草3.PNG

また、このアセット作者さんは他にも多くの植物のアセットを$5や$10で公開していますので、ぜひチェックしてみてください!

Bamboo Tree Pack

Realistic Foliage Pack1

High Quality Grass and Bush Pack

Realistic Grass and Bush Pack3
などなど
他にもいっぱい公開されているので是非チェックしてみてください!

9.Zero Gravity Part One

Zero Gravity Part One
カテゴリ:3D>環境>SF
価格:$10.15

PBR対応の宇宙船の内部のアセットです。
壁や窓、小物など合計59個のプレハブが入っています。
SFっぽい表現をしたい時は是非!

宇宙船1.PNG

宇宙船2.PNG

宇宙船3.PNG

10.FPS Online Shooter Example

FPS Online Shooter Example
カテゴリ:テンプレート>パック
価格:$4.99

Photonを使用して作られた簡単なオンラインFPSゲームの完成プロジェクトです。
ほかの完成プロジェクトに比べたらシンプルですが、スクリプトを見て学ぶには充分な教材だと思います。
オンラインマルチプレイゲームを作って見たい方はぜひ買って学んで見てはどうでしょうか!

FPS1.jpg

FPS2.jpg

FPS3.jpg

11.100 Post Processing Styles

100 Post Processing Styles
カテゴリ:ビジュアルエフェクト>シェーダー>カメラエフェクト
価格:$4.99

PostProcessingを使ってみたい!
でもどうすればいいのかわからない!
そんな時に使えるのがこのアセット。
GUIを開いて気になったものをクリックするだけ!

エディタ上でプレビューを見ながら気になったものをクリックするだけ!
ポスト1.PNG

ポスト3.PNG

ポスト2.PNG

アップグレードバージョンの1000+ Fast Post Processingもあります。

1000+ Fast Post Processing

12.Town Constructor Pack

Town Constructor Pack
カテゴリ:3D>環境>都市
価格:$4.99

ビルや街の小物を追加する巨大アセットです。
100以上の建築用モジュールパーツ、50以上の道路の小物、11の背景用の低解像度建築物などが含まれています。

街1.PNG

街2.PNG

街3.PNG

なんと夜バージョン付き!
街4.PNG

13.Simple Mesh Cutter

Simple Mesh Cutter
カテゴリ:ツール>エフェクト
価格:$10

ゲーム内でメッシュを切断したい!
それが簡単にできてしまうアセットです。
アニメーションにも対応していて、切断後もボーンを使うことが出来るため、切断後死亡モーションを入れるといった使い方もできそうです。

切断したメッシュをさらに切断することも可能
メッシュ1.PNG

切断されたメッシュは別々のオブジェクトになりながらもアニメーションをさせることができます
メッシュ2.PNG

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