ぐーるらいふ

底辺。

【Unity】Unity2019.1で初級者でも本当に簡単にECSを実現する

ECSって難しそう

  • ECSってのがあるらしい。
  • Unityで大量のオブジェクトを表示しても軽いらしい。
  • だがまだpreview版なので、仕様変更が激しいらしい。

という噂だけ聞いていて、

「ほーんでもまだゲームに使うには早そうだぬ」

と煎餅をバリバリ食べて放置していたんですが、
重い腰を上げてやってみたら凄い簡単に出来ちゃいました。

なので、記事にして記憶を残しておこうと思います。

ECSの解説…はちょっと置いといて

ECSってなに?ComponentSystemって?といった解説記事は
沢山の先駆者が残してくれていて、自分も存分に参考にさせて頂いてます。
ちゃんと知りたい!といった方は是非読んでみてください。

tsubakit1.hateblo.jp

qiita.com

www.f-sp.com


などなど、本当にいつもお世話になっております。

まず動かしてみよう

自分は、

まず動かしてみてから、動作を見つつ用語を紐づけて理解していく

スタイルでやってみようと思いました。

まず、単純に一つのcubeをECSを利用して表示するコードを紹介します。

導入

Unity 2019.1.1f1 を使用。
このサンプルでは新規プロジェクトから行ってますが、
途中のプロジェクトでも必要なpackageをインストールすれば問題なくECS使えます。

unityが開いたらPackageManagerを開きます。

f:id:ghoul_life:20190511140847p:plain

Advancedからpreview packageを表示させます。

f:id:ghoul_life:20190511141550p:plain

以下のpackageをinstallします。

  • Burst
  • Entities
  • Hybrid Renderer
  • Mathematics
  • Jobs (後々必要になると思いますが、本当に最小なら無くても大丈夫です)

f:id:ghoul_life:20190511141453p:plain

これで事前準備は完了です。

実装

早速コードを書いていきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Rendering;
using Unity.Collections;

public class ECSBootstrap : MonoBehaviour
{
    [SerializeField] private Mesh _mesh;
    [SerializeField] private Material _material;

    private const int ECS_CREATE_COUNT = 1;

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

    private void InitializeECS()
    {
        var entityManager = World.Active.EntityManager;

        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh)
            ,typeof(LocalToWorld)

            // Transform
            ,typeof(Translation)
        );

        NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp);

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();
    }
}

コードこれだけでOKです。

meshは標準で入っているcubeを、materialは適当にcreateして使用します。shaderはstandardで大丈夫です。

f:id:ghoul_life:20190511144744p:plain

実行してみましょう。

f:id:ghoul_life:20190511144932p:plain

はい、出ました。ECS簡単ですね。
画像では、Entity Debuggerというものも一緒に載せています。
これはECSで作られたEntityを確認することが出来るwindowsです。
Window > Analysisの所にあります。

f:id:ghoul_life:20190511145233p:plain

ECSを利用してオブジェクトを生成するとInspectorに表示されないため、
どんな状態になっているか把握できないことがあります。
そういった時にこれを見ると、Entityが実際に作られているか確認することが出来ます。
この例で行くと、「Entity 0」が作られているのが確認できます。

Entityって?

EntityとはGameObjectに相当する構造体の事です。
ECSでは1Entityを1つの実体として処理します。

簡単に解説

Managerを取得

EntityManagerはECSのEntityを管理するクラスです。
ECSを利用するためにまずそのManagerを取得します。

        // 現在有効なEntityManagerを取得
        var entityManager = World.Active.EntityManager;

どのようなEntityを生成するか指定

続いてどのようなEntityを生成するか?を指定します。
cubeを表示するだけならLocalToWorld , Translationは必要ないのでは?
と思うかもしれませんが、描画する位置情報を処理するarcheTypeも追加しないと
カメラ上には出てきませんので、追加しています。
(Entity的には無くてもちゃんと作られます)

        // どのようなEntityを生成するか指定
        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh) // Meshを描画する
            ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する

            // Transform
            ,typeof(Translation) // 座標を指定してEntityに反映させる
        );

Entityをいくつ作るかを指定

次にそのEntityはいくつ作るか?を指定します。

        // いくつEntityを生成するかを指定。ここでは1つだが、この値を変えればその個数分作られる
        private const int ECS_CREATE_COUNT = 1;
        NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp);
        entityManager.CreateEntity(entityArchetype, entityArray);

Entityの初期設定

そして作ったEntityの初期設定を行います。

        // 各Entityに対する初期設定を行います。
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            
            // 各EntityのRenderMeshに使用したいmeshとmaterialを渡します。
            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose(); // ちゃんと破棄する

こんな流れになります。

初期位置を変更してみよう

つまり、初期設定で位置を指定すれば位置を変えることが出来そうだ。
やってみましょう。

まず個数を10にします。

private const int ECS_CREATE_COUNT = 10;  // 10に変更

続いて、以下のように初期化処理にTranslationを操作する処理を追加します。

       using Unity.Mathematics; // float3はUnity.Mathematics.float3なので

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) }); // 追加

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();

実行してみましょう。

f:id:ghoul_life:20190512155018p:plain

Entityの個数が10に増え、位置がランダムになっています。
このようにentityManager.SetComponentDataで初期値を操作する事が可能です。

独自の処理を追加する

こっからです。
もちろん予め用意されたものだけではゲームなんて作れません。
独自の振る舞いを追加したいに決まっています。

初期値で定めた速度で上下に移動する処理

を追加したいと思います。

オリジナルのComponentDataを作成する

移動速度を持つComponentDataを作成します。

using Unity.Entities;

// classではなく、structなのに注意して下さい。
public struct MoveSpeedComponent : IComponentData
{
    public float _moveSpeed;
}

Entityを移動させる処理を作成する

Entityをリアルタイムで操作する処理はComponentSystemが行っています。
そのComponentSystemを独自に実装する事で振る舞いを追加することが出来ます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

[AlwaysUpdateSystem]
public class MoveSystem : ComponentSystem
{
    protected override void OnUpdate()
    {

        Entities.ForEach((ref Translation translation, ref MoveSpeedComponent moveSpeedComponent) =>
        {
            translation.Value.y += moveSpeedComponent._moveSpeed;

            // 上下反転
            if (translation.Value.y > 5f)
            {
                moveSpeedComponent._moveSpeed = -math.abs(moveSpeedComponent._moveSpeed);
            }
            if (translation.Value.y < -5f)
            {
                moveSpeedComponent._moveSpeed = +math.abs(moveSpeedComponent._moveSpeed);
            }
        });
    }
}

生成した各EntityをForEachで回して、そのEntityが持っているComponentDataの参照を取得し処理を行うように記述します。

[AlwaysUpdateSystem]

これが無いと
f:id:ghoul_life:20190512161424p:plain
このようにComponentSystemが動いていない状態になります。
独自にWorldを作成している場合はこの限りでは無いと思うのですが、この例ではWorldは一旦置いています。

archeTypeにMoveSpeedComponentを追加

作成したMoveSpeedComponentをEntityに追加します。

        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh) // Meshを描画する
            ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する

            // Transform
            ,typeof(Translation) // 座標を指定してEntityに反映させる

            // Custom
            ,typeof(MoveSpeedComponent) // Entityの移動速度を指定する
        );

Entityの初期化処理に移動速度を設定

移動速度をランダム値で初期化して、Entityごとに移動速度をバラつかせます。

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) });
            entityManager.SetComponentData(entity, new MoveSpeedComponent { _moveSpeed = UnityEngine.Random.Range(-0.5f, 0.5f) }); // 追加

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();

これで独自処理追加完了です。実行してみましょう。

f:id:ghoul_life:20190512162209g:plain

Entityがランダムな速度で上下移動しているのが確認できました。

プラスワン

個数を10000にしてみましょう。

f:id:ghoul_life:20190512164208p:plain

余りに大量なため、画面が埋まっていますが、
FPSに影響が無く動作する事が確認できます。

GameObjectがTransformで行っている処理であれば
Unity.Transforms.Scale , Unity.Transforms.Rotation
なども使えますし、
色を変化させたければ、RenderMeshのmaterialからcolorを変化させたりもできます。
(materialはSharedなので、全部一緒に変わりますが、shaderを使えばもっと複雑に出来そうではあります。)

        Entities.ForEach((Entity e) => {

            var renderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
            var material = renderMesh.material;
            // ~~~~
            renderMesh.material = material;
       });

こんな感じでRenderMeshにアクセスすることはできます。

とっかかりとして

ECSをまず動かす。という観点でやってみました。

ここから

  • どんな仕組みで動いているんだろう
  • Worldってなんだろう
  • NativeArrayって何なんだろう
  • なんでComponentDataは構造体しか使えないんだろう
  • もっと複雑な処理をECSで実現したい

といったさらに深く学習していくとっかかりとなってくれたら幸いです。

反省

4月記事書いてない!最低でも一月一記事ぐらい投稿したかった。
次はもう少しゲームらしい記事を上げたい所です。

別件でストーリーのあるゲームを作成していまして

  • ストーリーのプロット完成
  • カットシーンのラフは完成(画像などは仮だが動作や文章は出来ている)
  • 立ち絵、顔画像 (いまここ作成中)

となっています。
エターなりそうですが、出来るだけ頑張りたいと思ってます…。

【Unity】第11回unity1week「つながる」オブジェクトプールについて

またunity1weekが開催されましたね。
これで11回目になります。すべて参加してまして、
「じょじょにUnityに慣れてきてるんだなぁ、ちょっとずつだけど出来る事が広がっているな」
と自分なりに感じる結果となっています。

f:id:ghoul_life:20190319214902p:plain:w150
(今回作ったアイコン。文字はあれですが、ユニティちゃんはちょっと力作です。)

サイドバーに今までunity1weekで作ったアプリを表示してますので、
良かったら…やっぱ見ないでいいです。

初期のものはコードも酷いし、unityわかってないし、絵も下手くそですごく恥ずかしいですが
恥を忍んで、公開したままにしてます。成長の証って事で…。

お題について

お題もそうなんですが、実は毎回自分に対して
「今回は○○について勉強すること」
という題目を決めて参加してます。

今回は「オブジェクトプーリング」「負荷対策」について
主に取り上げてみようかなと漠然と考えてました。

事件がおこる

なんだこれ。けもみみおーこくすげーな。って事件です。
この辺りではまだ勉強段階で、
「こうやったらこんなこと出来るのかなぁ」
という推測を元にスクラップ&ビルドを繰り返してました。

で、これをゲームにしようと思いました。

つながる要素

前回の最大の反省点は「難しすぎた事」でした。
僕は格闘ゲームとかもプレイしてるのですが、
基本的に「連打をしない」ということが染み付いていまして、
どんなゲームでもリズムよく(タイミングよくポンポンポンと)
押すのが普通になってしまっていました。

ぱふもどきさんの放送を見てたのですが、
凄い連打してたり、位置取りとかあんまり気にしてなかったりと
「ああ、これが普通の人だ。自分が間違っていた…」
とちょっとショックを受けました。

その反省を活かそうと、今回はなるべく複雑な操作、
システムは止めようと漠然と考えていて
コンボが繋がるとスコアがどんどん伸びていく感じがわかりやすいだろう
とシンプルな要素に落ち着かせました。

破壊について

ボロノイ図」とかでググってみてね。
voronoiのアルゴリズムを利用して作ってます。
自分なりにやっちゃってるので、なんちゃってボロノイかもしれませんので割愛。

最初のランダム点配置は Random.insideUnitSphere でやっちゃってます。
これオンラインで同期する場合はRandom.seedの共有も必要になるんだろうな…。

f:id:ghoul_life:20190320230648p:plain

オブジェクトプールと破片について

一ブロックにつき、10~20の破片にランダムで分解されるような仕様にしています。
この各破片は一つ一つがGameObjectになってます。
Rigidbodyで物理演算の影響も受けないとならないため、別々なオブジェクトにしないとなりませんでした。
そして以前から告知している通り、随時動的に計算して作っています。
単純に毎回Instantiateするとプチフリーズの嵐になってしまうので以下のようにプールを作りました。

PartsPool.cs

public void InitializePool()
{
    if (IsPopulated && parts.Length == maxPoolSize) return;

    // Clear old shard pool if one exists.
    if (parts != null)
    {
        foreach (var part in parts)
	{
	    if (part == null) continue;
	    DestroyImmediate(part.gameObject);
	}
    }

    parts = new Part[maxPoolSize];

    for (int i = 0; i < maxPoolSize; i++)
    {
	parts[i] = new GameObject("Part").AddComponent<Part>();
	parts[i].transform.parent = transform;
    }
}

このプールは破片プールです。
各破片はここにあるGameObjectを使いまわします。
800個から1000個ほど生成しておいていました。

Part.cs

void Initialize()
{
    if (GetComponent<Renderer>() == null) gameObject.AddComponent<MeshRenderer>();
    if (GetComponent<Rigidbody>() == null) gameObject.AddComponent<Rigidbody>();

    meshFilter = GetComponent<MeshFilter>() == null ? gameObject.AddComponent<MeshFilter>() : GetComponent<MeshFilter>();
    meshCollider = GetComponent<MeshCollider>() == null ? gameObject.AddComponent<MeshCollider>() : GetComponent<MeshCollider>();
            
    meshCollider.convex = true;
    meshFilter.sharedMesh = new Mesh();
    IsUse = false;
    gameObject.SetActive(false);
}

破片はゲーム内でInstatiateせず、Inspector上で800個用意して、Activeを切っておきます。
また各種必要なコンポーネントを変数に持っておきアクセスを容易にしときます。

Part.cs

public void Use(GameObject parent, Vector3[] newVertices, Vector3[] newNormals, int[] newTriangles, Vector2[] newUVs)
{
    part.name = "Fractured Parts";
    var mesh = new Mesh();
    part.Mesh = mesh;
    part.Mesh.vertices = newVertices;
    part.Mesh.normals = newNormals;
    part.Mesh.uv = newUVs;
    part.Mesh.triangles = newTriangles;
    part.meshCollider.sharedMesh = part.Mesh;

    part.gameObject.SetActive(true);
    part.IsUse = true;

    part.GetComponent<Renderer>().material = parent.GetComponent<Renderer>().material;

    if (parent.GetComponent<Rigidbody>())
    {
        part.GetComponent<Rigidbody>().mass = parent.GetComponent<Rigidbody>().mass;
        part.GetComponent<Rigidbody>().velocity = parent.GetComponent<Rigidbody>().velocity;
    }
}


使う時にActive化して、位置、大きさ、Meshをセットしてあげます。
また、この破片はparentとなる元オブジェクトが管理しており、
設定した時間後に削除されます。この削除は非アクティブ状態にして、Poolに戻るだけにしておきます。

PartsPool.cs

public static void BackPool(GameObject obj , float waitTime)
{
        var part = obj.GetComponent<Part>();
        if (part != null)
        {
            pool.StartCoroutine(pool._BackPool(part , waitTime));
        }
}

private System.Collections.IEnumerator _BackPool(Part part, float waitTime)
{
        yield return new WaitForSeconds(waitTime);

        part.IsUse = false;
        part.gameObject.SetActive(false);
        part.transform.parent = transform;
}

(一定時間後に削除する、というのをCoroutineで簡易的に実装してます。)

こうすることによって、poolingを使い回すことが可能です。
これでゲーム中はInstatiateとDestroyを使わずに進めることが可能です。
(IsUseがfalseのものをプールから取ってくればOKですね)

Effect , PopupScoreなども同様にInGame中にはInstantiateせず、Active切り替えで使いまわしてます。

CameraPlay

assetstore.unity.com

これ知らなかったんですが、こんな凄いAssetがあるとは…。
有料なんですが、カメラ演出は効果が大きいので、コストに見合う効果が出ます。

画面を揺らす、集中線を出すといったことが1行でかけます。
便利すぎる。最初はやりまくってたんですが、気持ち悪くならない程度にちょっと抑えました。

AudioManager

同時に複数のオブジェクトを破壊するので、音も大量に一度に鳴ります。
AudioSourceは最終的に10個用意していて、その中で使ってないAudioSourceを探して使うようにしてます。
15でも良かったかも。

NCMB

バカやって変な不具合出してた。別記事にまとめたので、そちらをどうぞ
自分のせいです。これもうちょっと作る必要あるなぁ…。通信中表示とかまだ甘い。

ghoul-life.hatenablog.com

その他色々

最後はほんとに不具合直すのと、違和感を削る作業(調整とも言う)に忙殺されますね。

ゲームプレイ > ここ変だな、これ欲しいな > 直す、作る

これを繰り返しまくる。土日はほとんどこれ。
ここでポップアップやらスコア動かしたり操作説明付けたりなど。
この辺りが今まで甘かったのだけど、少しずつ計算に入れて作業を進めることが
出来るようになってきたなと思う。

お絵かき

前回はバリバリ描きまくりで疲れましたが、
今回はほとんど要らなかったのでアイコンしか描けず。
文字は酷いので外したverを。

f:id:ghoul_life:20190320234606p:plain:w150

割とそれっぽく描けたかな…と思います。リリースギリギリに滑り込み。
元ネタは「何でも言うことを聞いてくれるアカネちゃん」です。

反省点

  • ステージとかもう一つぐらい作れば良かった
  • ボムアイテム作りたかった(どうしてもエフェクトが間に合わず…Blenderに慣れないと…)
  • 敵とか居たらもっと良かったかも。むしろ街中で怪獣とユニティちゃんが戦うゲームにするのはどうだろうか
  • 甘城ブリリアントパークが面白すぎた
  • SEKIROが楽しみすぎてそれどころじゃなかった
  • WebGLの日本語入力これか。時間があったらやってみよう

tsubakit1.hateblo.jp

次回

もちろん参加します。
今回はお絵描きがほとんど出来なかったので、
次はノベル要素強めとかやりたいなと思ってます。
xNode使ってエディタ拡張の勉強かなー。

【Unity】 NCMB 405 エラー Method not Allowed 【オンラインランキング】

お疲れ様です。ぐーるです。unity1weekお疲れ様でした。
その記事を書いているのですが、その前に一個メモしておきたいことが。
お恥ずかしいミスなのですが…。

NCMBって何?

クラウド上に用意された機能をAPIで使用するだけでサーバ開発、運用不要で
バックエンドサービスを利用することが出来るサービスのことをmBaasと言いまして、
niftyが提供しているmBaasを

nifty cloud mobile backend == NCMB

と呼んでいるようです。まぁ楽にサーバ機能を使うことが出来るものです。
unity SDKも用意されていて、簡単に使うことが出来ます。
使い方はググると公式もありますし、沢山記事もあるので割愛します。

mbaas.nifcloud.com

mbaas.nifcloud.com

Method not allowed????

NCMBを利用してデータをテーブルに追加する時に以下のようなコードを書きます。

    public void TestSend(string objectId , string name , int score)
    {
        NCMBObject obj = new NCMBObject("TestTable");
        obj.ObjectId = objectId;
        obj["name"] = name;
        obj["score"] = score;

        obj.SaveAsync((NCMBException e) =>
        {
            if (e == null)
            {
                Debug.Log("success");
            }
            else
            {
                //エラー処理
                Debug.LogError(e);
            }
        });
    }

このSaveAsyncで以下のエラーが出てしまいました。

【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxxxxx/TestTable/
【type】:PUT
【content】:{"name":"test user name","score":8307}

【StatusCode】:405
【Error】:NCMB.NCMBException: Method not allowed.
【ResponseData】:

これなんだろう?と。

原因

NCMBを利用してデータを入れる際は以下のように判定されます。

obj.ObjectId = objectId; <- ここ

obj.ObjectId [ null ] == 新規レコードを追加 
obj.ObjectId [ (存在しているObjectId) ] == 該当レコードを更新

となります。
ちなみに、新規レコード追加した際にobj.ObjectIdに値が入って返ってくるため、
その値を利用して次回以降は更新することが出来ます。

ここが以下の値だとエラーになります

obj.ObjectId [ (存在していないObjectId) ] == 404エラー No data available.

【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxxxx/TestTable/aaaaa
【type】:PUT
【content】:{"name":"name","score":2595}
UnityEngine.Debug:Log(Object)

【StatusCode】:404
【Error】:NCMB.NCMBException: No data available.
【ResponseData】:
UnityEngine.Debug:Log(Object)
obj.ObjectId [ "" ] == 405エラー Method not allowed.

【url】:https://mb.api.cloud.nifty.com/xxxxxxxxxxxx/TestTable/
【type】:PUT
【content】:{"name":"test user name","score":8307}

【StatusCode】:405
【Error】:NCMB.NCMBException: Method not allowed.
【ResponseData】:

となります。つまりObjectIdが""だと、エラーになってしまう…と。

反省

NCMBを利用する時に、簡単に考えると
「ObjectIdはPrayerPrefsにでも保存しとくか」
と考えてこんなコードを書いてしまいました。

obj.ObjectId = PlayerPrefs.getString("NCMB_ObjectId");

これで""になってしまってたのが原因…。はずかしー。

ちゃんとデフォルト値を入れとけって事ですね。反省。

obj.ObjectId = PlayerPrefs.GetString("NCMB_ObjectId" , null);

【Unity】xNodeの使い方 初心者向け

お疲れ様です。ぐーるです。
ビルの件の高速化の記事を進めているのですが、めっちゃ長いので、気長にちまちま加筆してます。

というわけで気分転換でxNodeについて書きます。
普段真っ黒いコンソールしか見てない自分としては
ビジュアルスクリプティングというのに興味がありまして、
Playmakerとか触ってみたいなと思ってたんですが、有料なのかぁと二の足を踏んでいました。

先日勉強会でxNodeってのがあるという話を聞きまして、早速触ってみました。
初心者向けですごーく初歩的な内容ですがメモとしてまとめておこうと思います。

xNodeの導入

xNodeはAssetStoreにもありますが、githubで公開されています。
github.com

cloneしても良いですが、unitypackageが公開されているので、それを利用しました。
https://github.com/Siccity/xNode/releases

xNode_1.6.unitypackage

をDLして、unityのプロジェクトで

Assets > Import Package > Custom Package

でimportします。

xNodeの最もシンプルな構成

シンプルに考えるとxNodeは二つのクラスしかありません。

f:id:ghoul_life:20190304173735p:plain

NodeとNodeGraphです。

XNode.Node

これは単一のノードを指します。
f:id:ghoul_life:20190304184225p:plain
値を持ったり、値によって振る舞いを変えたりといった事が出来ます。

XNode.NodeGraph

これはNodeを管理するScriptableObjectを作れるクラスです。

f:id:ghoul_life:20190304185452p:plain

このようにNodeを複数グループ管理するイメージです。
内部ではnodesというリストを持っていて、ここで各ノードを保持しています。

早速作ってみる

今回はNovelゲームを題材にして、そのNovelゲームの会話処理をノード化し、
会話の流れをビジュアル的に捉えやすくするイメージで作ってみます。

NodeGraphの作成

まずxNodeのウィンドウを出すため、NodeGraphを作成します。

using UnityEngine;
using XNode;

[CreateAssetMenu(fileName = "NovelGraph", menuName = "Node Graph/NovelGraph")]
public class NovelGraph : NodeGraph { 
}

menuNameなどは適宜変えてください。
間違いでもなんでもなく、本当にこれだけでOKです。
最小限で良ければ何にも要らないです。

Nodeの作成

ノードを作成します。

f:id:ghoul_life:20190304190706p:plain

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

public class NovelNode : Node {

    public Sprite _novelSprite;

    [TextArea(3, 5)]
    public List<string> _messageList;

    [Input] public NovelNode _beforeNode;
    [Output] public NovelNode _nextNode;
}

表示したい立ち絵スプライト画像と文字列リストを持ちます。
ノード同士を連結させるために前と後ろの参照を持っておきます。
[Input]というAttributeを付与するとノード連結を受け入れることが出来、
[Output]なら逆にノード連結を出力することが出来ます。

[Output] -> [Input]

の関連になります。
f:id:ghoul_life:20190304193526p:plain
コードはこれで準備完了です。

NodeGraphとノードエディタ

f:id:ghoul_life:20190304214330p:plain
(他も見えますが、気にせず)

Project > Create > Node Graph > NovelGraph

でScriptableObjectを生成し、それをダブルクリックします。
f:id:ghoul_life:20190304215104p:plain
xNodeエディタウィンドウが開くので、右クリックでNodeを追加出来ます。
f:id:ghoul_life:20190304215326p:plain

後は好きな値を入れます。
[Input][Output]を繋げる時は直感的にドラッグすればOKです。
f:id:ghoul_life:20190304215912p:plain

NodeGraphの使い方

使う側は簡単です。

public class NovelGraphController : MonoBehaviour
{
    [SerializeField] NovelGraph _novelGraph;
    private int _nodeIndex = 0;
    // ~~
    // nodesに入っているのでそこからアクセス出来ます
    var novelNode = _novelGraph.nodes[_nodeIndex] as NovelNode;

}

こんな感じで自由にアクセス出来ます。
NovelGraphには上で作成したScriptableObjectを紐づければOKです。

ノベルゲーム風に使ってみた

こんな感じでデータを入れて

f:id:ghoul_life:20190304172553p:plain

こんな感じに会話が進む感じにしてみました。

f:id:ghoul_life:20190304222134g:plain

TextFaderというライブラリをちょっとお借りしてカスタムして使用してます。
会話の流れがビジュアル的に捉えやすいですね。
会話の入れ替えやデータのミスの把握などが直感的に行えそうです。

baba-s.hatenablog.com

【Unity】ビル群をランダム生成する

お疲れ様です。ぐーるです。
また間が空いてしまった…。

unity1weekがもうすぐあるので、Unity思い出さないとなぁ~と触っています。

ビル群を自動生成する

こんなにRT&いいねされたのは初。

正直大したことはしていなくて、とっても恐縮。
ランダムでscale決めて、空いている所に配置しているだけです。

早速コード

BuildingMapCreater

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

// ビル一棟ごとのデータ(位置と大きさだけ)
public class BuildingMapObjectData
{
    public int id;
    public Vector3Int position;
    public Vector3Int scale;
}

public class BuildingMapCreater
{

    // もっとマップを広くしたい場合は大きくすればOK
    private int MAP_SIZE_W = 100;
    private int MAP_SIZE_H = 100;

    private int[][] _maps;
    private static BuildingMapCreater _instance;

    private BuildingMapCreater() { }

    public static BuildingMapCreater Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BuildingMapCreater();
            }
            return _instance;
        }
    }

    // ビル群のマップを自動で生成する
    public List<BuildingMapObjectData> CreateMap()
    {
        var result = new List<BuildingMapObjectData>();

        
        var i = 0;
        var j = 0;
	var id = 1;
	BuildingMapObjectData mo = null;

	// 初期化(-1)埋め
        _maps= new int[MAP_SIZE_H][];
        for (i = 0; i < MAP_SIZE_H; i++)
        {
            _maps[i] = new int[MAP_SIZE_W];
            for (j = 0; j < MAP_SIZE_W; j++)
            {
                _maps[i][j] = -1;
            }
        }

        i = 0;
        j = 0;
        while (true)
        {
            mo = new BuildingMapObjectData();
            mo.id = id++;

            mo.position = Vector3Int.zero;

	    // ビルの大きさをランダムで適当に
            mo.scale.x = Random.Range(1, 20);
            mo.scale.y = Random.Range(1, 20);
            mo.scale.z = Random.Range(1, 20);

            if (!ExecBuild(mo))
            {
                break;
            }

            result.Add(mo);
        }

        return result;
    }

    // 範囲内に他のビルが重なっていないかチェックする
    private bool ExecBuild(BuildingMapObjectData mo)
    {
        while (true)
        {
            if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2))
            {
                PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2);
                PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z);
                return true;
            }
            else
            {
                if (mo.position.x < MAP_SIZE_W)
                {
                    mo.position.x += 1;
                }
                else if (mo.position.z < MAP_SIZE_H)
                {
                    mo.position.z += 1;
                    mo.position.x = 0;
                }
                else
                {
                    return false;
                }
            }
        }

    }

    // 指定範囲を塗りつぶす
    private void PaintMapRect(int id, int x, int y, int w, int h)
    {
        for (var yy = y; yy < y + h; yy++)
        {
            for (var xx = x; xx < x + w; xx++)
            {
                PaintMap(id, xx, yy);
            }
        }
    }

    // 指定範囲が空いているか調べる
    private bool IsEmptyMapRect(int x, int y, int w, int h)
    {
        for (var yy = y; yy < y + h; yy++)
        {
            for (var xx = x; xx < x + w; xx++)
            {
                if (!IsEmptyMap(xx, yy))
                {
                    return false;
                }
            }
        }
        return true;
    }

    // 指定位置をIDで塗る
    private void PaintMap(int id, int xx, int yy)
    {
        if (yy >= 0 && yy < _maps.Length &&
        xx >= 0 && xx < _maps[yy].Length)
        {
            _maps[yy][xx] = id;
        }
    }

    // 指定位置が空いているかチェック
    private bool IsEmptyMap(int xx, int yy)
    {
        if (yy >= 0 && yy < _maps.Length &&
        xx >= 0 && xx < _maps[yy].Length)
        {
            if (_maps[yy][xx] > 0)
            {
                return false;
            }
            return true;
        }
        return false;
    }

    // デバッグ用出力
    private void DebugOutput()
    {
        for (var i = 0; i < MAP_SIZE_H; i++)
        {
            string output = "";
            for (var j = 0; j < MAP_SIZE_W; j++)
            {
                output += "[" + _maps[i][j] + "]";
            }
            Debug.Log(output);
        }
    }
}

Stage

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

public class Stage : MonoBehaviour
{

    [SerializeField] Building _buildingPrefab;
    private List<Building> _mapObjects;

    public void CreateMap()
    {
        StartCoroutine(_CreateMap());
    }

	
    private IEnumerator _CreateMap()
    {
        if (_mapObjects != null)
        {
            foreach (var m in _mapObjects)
            {
                Destroy(m.gameObject);
            }
            _mapObjects.Clear();
            _mapObjects = null;
        }
        _mapObjects = new List<Building>();
        var mapObjectDatas = BuildingMapCreater.Instance.CreateMap();
        foreach (var mod in mapObjectDatas)
        {
            var fs = Instantiate(_buildingPrefab, this.transform);
            fs.SettingBuildingMapObjectData(mod);
            _mapObjects.Add(fs);
            yield return new WaitForEndOfFrame();
        }
    }
}

Building

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

public class Building : MonoBehaviour
{
    [SerializeField] int _id;
    private bool _isInitialize = false;
    private Vector3 _targetPosition = Vector3.zero;
    private float INIT_TIME = 10.0f;

    // Update is called once per frame
    void Update()
    {
        if (_isInitialize)
        {
            if (Vector3.Distance(this.transform.position, _targetPosition) > 0.01f)
            {
                this.transform.position = Vector3.MoveTowards(this.transform.position, _targetPosition, INIT_TIME * Time.deltaTime);
            }
            else
            {
                this.transform.position = _targetPosition;
                _isInitialize = false;
            }
        }
    }

    public void SettingBuildingMapObjectData(BuildingMapObjectData mapObjectData)
    {
	// dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整
        var x = mapObjectData.position.x + (mapObjectData.scale.x / 2);
        var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる
        var z = mapObjectData.position.z + (mapObjectData.scale.z / 2);
        _id= mapObjectData.id;
        _targetPosition = new Vector3(x, y, z);

	// 最初沈ませておいて、下から迫り上がるような演出をする
        this.transform.position = new Vector3(_targetPosition.x, _targetPosition.y - mapObjectData.scale.y, _targetPosition.z);
        this.transform.localScale = mapObjectData.scale;

	_isInitialize = true;
    }
}

ビル群を生成するイメージ

まず、ランダムでビルという名のただのcubeの大きさを適当に決めます。
その後配置するのですが、この時

  • ビル同士が被らない
  • ビルとビルの間に1マス以上の隙間を空ける

この2つを実現させます。

高さは被らないため、除外して考えるので、単純に2次元配列で管理します。

0,0の位置から大きさ分の四角形が入る位置を探します。
この時、-1 , + 2をして道路分を空けるようにします。
(この辺の値を大きくするとさらに道路を広く出来ます。
また、この列や行はビル配置禁止!といった値を決めておくと大通りを作ることが出来ます)

if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2))
{
    PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2);
    PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z);
    return true;
}

入らなければ一マス動かす、で入る位置を探します。

入ったらその位置にIDを書き込んでおき、すでにここにはビルがありますよ、
という情報を残しておきます。

f:id:ghoul_life:20190227194818p:plain

これを繰り返して、MAP内に入り切らなくなったら終了です。

余談

記事書いてて思ったのですが、HITしたらIDからビル情報取ってきて、その大きさ足したほうが良かった

生成したビル情報使う

ビル情報のスケールはそのまま入れますが、positionはpivotの関係上、計算が必要です。

// dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整
var x = mapObjectData.position.x + (mapObjectData.scale.x / 2);
var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる
var z = mapObjectData.position.z + (mapObjectData.scale.z / 2);

プロジェクトとして

  1. 空のGameObjectを生成して、StageスクリプトをAdd Component
  2. CubeをInspectorに生成して、BuildingスクリプトをAdd Componentして、Prefab化
  3. 作ったCubeをStageに紐づけ
  4. 実行して、Stage.CreateMap()をコールすればOK

おまけ

なんとなくビルが下からニュッと生える感じにしたかったので、
目標位置と開始位置を変えて、目標位置に到達するまで移動するようにしてます。
上から降ってくる感じとかEasingとか付けるともっと賑やかになります。
(DoTweenとか使うともっと簡単ですね)

f:id:ghoul_life:20190227180855g:plain

最後に

蓋を開けてみたらとんでもなく初歩的なスクリプト
ほんとこんなんでバズって良いんだろうかという気になってます。
実はみんなFracture側の記事を期待してたりして。

【Unity】Android Nativeプラグイン開発 最小構成でなるべくわかりやすくまとめた

Androidでネイティブプラグイン

Androidでネイティブプラグイン開発を行う時の作業手順をまとめてメモしておきます。

環境

使用した環境は以下になります。

そして、最小構成で作成します。
aarを使わず、より不要な物をそぎ落としたjarで組み込みます。

なるべく処理に不要な物は排除し、本当に処理を行うのに必要なものは
何なのか?という事に着目してまとめてます。
なるべく、わかりやすく。Step By Stepで。

Android Studioを使ってUnityプラグインを作ろう

手順1 プロジェクトを作成

Android Studioを立ち上げ、新規プロジェクトを「Add No Activity」で作成します。

f:id:ghoul_life:20190126020326p:plain:w300
f:id:ghoul_life:20190126020559p:plain:w300

手順2 ライブラリモジュールを作成

プロジェクトが出来たら、ライブラリモジュールを作成します。

Android Studioの上部メニューバーからFile > New > New Module
そして、「Android Library」を選択して作成します。

f:id:ghoul_life:20190126020926p:plain
f:id:ghoul_life:20190126021255p:plain:w300
f:id:ghoul_life:20190126021041p:plain:w300

手順3 必要ないファイルの削除

ライブラリモジュール以外は必要ないため、
作成したプロジェクトフォルダを開き、最初に作られたappを削除します。

f:id:ghoul_life:20190126021732p:plain:w300

そしてsetting.gradleからもappを消します。

include ':app' ':unitypluginsamplelibrary'
->
include ':unitypluginsamplelibrary'

f:id:ghoul_life:20190126021919p:plain:w300

手順4 Unityクラスライブラリの追加

AndroidでNative連携Pluginを作る時、AndroidからUnityの機能を呼びたい事があります。
そういった機能をAndroidから使えるように、Unityが用意しているAndroid用クラスライブラリをPlugin側に組み込む必要があります。
例えば、Unity HubでUnity2018.3.2f1の場合はここにあります。

C:\Program Files\Unity\Hub\Editor\2018.3.2f1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes

これをモジュールのlibsの中にコピーして配置します。

f:id:ghoul_life:20190126022258p:plain:w300

手順5 ビルドスクリプトの修正

ビルドスクリプト(モジュールの直下にあるbuild.gradle)を以下のように整理修正します。
(compileSdkVersionなどは各環境に合わせて下さい)

apply plugin: 'com.android.library'

android {
    compileSdkVersion 27

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    compileOnly fileTree(dir: 'libs', include: 'classes.jar')
}

// buildしたjarをカレントに持ってきてリネーム
task createJarFile(type: Copy) {
    // 環境によって出力先がよく変わるみたい?intermediates-jarsとかもあるらしい
    from('build/intermediates/packaged-classes/release/')
    into('.')
    include('classes.jar')
    rename('classes.jar', 'UnityPluginSample.jar')
}
createJarFile.dependsOn(build)

新規追加したタスク「createJarFile」はUnityに組み込むjarをビルド後に抜き出し、リネームするタスクです。こういうのを作ると楽になります。

実はこの辺りでちょっと困ったことがあったので、その件についてまとめました。

ghoul-life.hatenablog.com

aarを使わないこの記事の方針では実はここまでしなくても良かったりします。
興味があれば一読してみてください。

手順6 configurationの設定

Android StudioからcreateJarFileタスクを実行出来るように構成を設定します。
Android StudioのEdit ConfigurationsからGradleを選択して、以下を参考に設定してください。

+ボタン > Add New Configuration > Gradle
Name : 自由に
Gradle Project : プロジェクトフォルダを選択
Tasks : createJarFileと入力

f:id:ghoul_life:20190126023556p:plain:w300
f:id:ghoul_life:20190126023708p:plain:w300

手順7 テスト用ファイルの削除

本来は必要であるべきと思われるのですが、最小構成なので、ここでは省きます。
モジュール作成時に自動で追加されるTestコードを削除します。

f:id:ghoul_life:20190126024030p:plain:w300

手順8 実際に使用するコードを記述する

ここまで来てようやっとコードがかけます。長い。
ソースファイルを新規追加し、以下のようにコードを記述します。
手順4でライブラリを正しくlibsの下に配置していて、手順5でgradleの設定が出来ていれば
com.unity3d.playerパッケージが使えるようになるはずで、エラーが出ないと思われます。
(エラーが出るときはその辺りを見直すとよい)

package com.example.unitypluginsamplelibrary;

import com.unity3d.player.UnityPlayer;
import java.util.Random;

public class HelloAndroidNativePlugin {

    public static void Execute()
    {
        Random r = new Random();
        UnityPlayer.UnitySendMessage("AndroidNativeManager" , "FromAndroid" , "Hello Unity Android Plugin. Rand." + r.nextInt());
    }
}

手順9 ビルドしてJarを作成する

Build VariantsをReleaseにして、ConfigureをcreateJarFileに合わせ、RunすればOKです。
BUILD SUCCESSと出れば、正常にプラグインが作成出来ています。

f:id:ghoul_life:20190126024718p:plain:w300

これでAndroid側の作業は完了です。

UnityでAndroid Pluginを組み込む

こちらはぐっと簡単です。

手順1 作成したプラグインを配置

Assets/Plugins/Android

以下に配置します。上の手順ならjarファイルをそこにポンと置けばOKです。

f:id:ghoul_life:20190126025305p:plain

手順2 プラグインの機能を使用するスクリプトを作成

組み込んだプラグインの機能を使うスクリプトを記述します。
AndroidJavaClassでクラスを指定し、Callで呼びたい関数を指定すればOKです。

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

public class AndroidNativeManager : MonoBehaviour
{
    [SerializeField] Text _androidMessageText;

    public static readonly string ANDROID_NATIVE_PLUGIN_CLASS = "com.example.unitypluginsamplelibrary.HelloAndroidNativePlugin";

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

    public void CallAndroidPlugin()
    {
        using (AndroidJavaClass androidJavaClass = new AndroidJavaClass(ANDROID_NATIVE_PLUGIN_CLASS))
        {
            androidJavaClass.CallStatic("Execute");
        }
    }

    public void ResetMessage()
    {
        _androidMessageText.text = "";
    }

    public void FromAndroid(string message)
    {
        _androidMessageText.text = message;
    }
}

apkを作成し、実機で動かしてみる

実際にapkをビルドして、それを実機で動かしてテストします。
自分はAndroid Emuratorで実行しました。

f:id:ghoul_life:20190126030157g:plain

Unity -> Android -> Unity
と問題なく処理が動いているのを確認できました。

【Unity】Android Native連携Pluginを開発してたら has been replaced with 'variant.getPackageLibraryProvider()'.

Androidのネイティブ連携開発

UnityでAndroidのNative連携Pluginの開発をしていたらこんなエラーに出くわした。

WARNING: API 'variantOutput.getPackageLibrary()' is obsolete and has been replaced with 'variant.getPackageLibraryProvider()'.
It will be removed at the end of 2019.
For more information, see https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variantOutput.getPackageLibrary(), use -Pandroid.debug.obsoleteApi=true on the command line to display a stack trace.
Affected Modules: helloplugin

エラー内容を見れば、
「variantOutput.getPackageLibrary()は2019年には消えるから、variant.getPackageLibraryProvider()に
置き換えて下さい」
といった具合なのですが、ちょっと対応に戸惑ったのでメモしておこう。

発生した状況

Android Studio 3.3を利用して素直にプロジェクトを作るとデフォルトで

com.android.tools.build:gradle:3.3.0

を利用することになり、そしたら遭遇した。

前提として

AndroidでNative連携Pluginを作る時、AndroidからUnityの機能を呼びたい事がよくある。
そういった機能を使う時に、Unityが用意しているAndroid用クラスライブラリをPlugin側に
組み込む必要がある。
例えば、Unity HubでUnity2018.3.2f1を利用している場合はここにある。

C:\Program Files\Unity\Hub\Editor\2018.3.2f1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes

Macとかならこの辺りを参考に
docs.unity3d.com

原因

build.gradleに記載してあるこれが原因。

android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

これは、AndroidでUnityの機能を使うためにlibs/classes.jarに配置して使用するが、
Unity側に含めると重複エラーになってしまうので、aarに固める時には省きたい。
その省く処理を行っている。

間違った対応メモ

android.variants.all{ variant ->
    variant.packageLibararyProvider.each { output ->
        output.configure{
            exclude “libs/classes.jar”
        }
    }
}

ストレートに直すとこうだ。
エラーは出なくなるが、これでは上手くいかず、classses.jarがPluginに入ってしまった。

そもそもPackageLibraryProviderに代わっているため、流れてくる内容が違う。
中身はTaskProviderでDebug , Releaseになってるような感じだった(ちょっと詳しくはわからないけど)
なので、そこでexcludeとかやっても特に効果が無い。

解決について

ただやりたいことは、

ビルド時にlibs/classes.jarを省きたい

これだけなんだ。

つまりこうすればいい

dependencies{
    compileOnly fileTree(dir:”libs” , file: “classes.jar”)  
    .....
}

(もちろんpackageLibrary~の部分は全部消していい)

前まではprovidedだったが、これも無くなって今はcompileOnlyになった。
これで固める時にclasses.jarを省くことが出来る。