ぐーるらいふ

底辺。

【Unity】uGUI Scroll List で Recycle したい (初心者向け)【再利用】

お疲れ様です。ぐーるです。
また一ヶ月ぐらい空いてしまいました。

「あ、そういえば一個思いついた」
と下書きにメモだけしてて、保存して
放置してた記事があったのを二重に思い出しました。

今回の単語は

unity UGUI Scroll List Recycle

です。

marginとか追記するの忘れた…時間見つけて追記しとこう。自分用のメモ。
あと最上段で引っ張ると判定に入ってIndexOutOfException吐いちゃうかもいかんいかん

  • > 追記しました。(2019/07/30)


サンプルプロジェクトを用意しましたので、
手っ取り早く見たい方はこちらを!

github.com

unityでスクロールリストをまず作ってみよう

よくある縦リストをunityで作る場合はこんな手順になります。

  1. ScrollRectを作る
  2. ViewPort(Mask)をよしなに作る
  3. ScrollBarを作る
  4. VerticalLayoutGroupとContentSizeFitterを作る
  5. LayoutElementを持ったprefabなどを複数追加するとリスト表示出来る

(ScrollViewをベースに作るのがおすすめです)

f:id:ghoul_life:20190729234203p:plain

要素を使いまわそう

まず、要素を使い回すには、以下のものを消します。

  • VeritcalLayoutGroup
  • ContentSizeFitter

えっ?て思うかも知れませんが、ここを自作する形になります。
大丈夫、難しくないです。

f:id:ghoul_life:20190730003734g:plain

ソースコードをまるっと

以下のソースを作成します。

/// <summary>
/// 要素を再利用するレイアウトグループ(Vertical)
/// </summary>
public class RecycleLayoutGroup : MonoBehaviour
{
    
    [SerializeField] RectTransform _itemElementPrefab; // リスト内の1要素
    [SerializeField] int _instantateItemCount = 9; // 何個作っておくか
    [SerializeField] float _margin = 10; // 隙間

    private int SAMPLE_ITEM_COUNT = 100; // サンプルで作成する数

    // 生成したアイテムはLinkedListで管理する
    public LinkedList<RectTransform> itemList = new LinkedList<RectTransform>();

    // cacheして処理の高速化を図る
    private RectTransform _rectTransform;
    private float _itemSizeheight = -1;

    // field
    protected float _diffPreFramePositionY = 0; // 一つ前の位置
    protected int currentItemNo = 0; // 現在の一番上の位置

    void Start()
    {
        _itemElementPrefab.gameObject.SetActive(false);

        for (int i = 0; i < _instantateItemCount; i++)
        {
            var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform;
            item.SetParent(transform, false);
            item.name = "Recycle Item " + i.ToString();
            item.anchoredPosition = new Vector2(0, -ItemHeight * i);
            itemList.AddLast(item);

            item.gameObject.SetActive(true);

            OnUpdateItem(i, item.gameObject);
        }


        // 先に全アイテム分だけscrollRectを広げておくのだ
        var delta = rectTransform.sizeDelta;
        delta.y = ItemHeight * SAMPLE_ITEM_COUNT;
        rectTransform.sizeDelta = delta;
    }

    void Update()
    {
        if (itemList.First == null)
        {
            return;
        }

        // 下
        while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2)
        {
            _diffPreFramePositionY -= ItemHeight;

            var item = itemList.First.Value;
            itemList.RemoveFirst();
            itemList.AddLast(item);

            var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo;
            item.anchoredPosition = new Vector2(0, -pos);

            OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject);

            currentItemNo++;
        }

        // 上
        while (anchoredPositionY - _diffPreFramePositionY > 0)
        {
            _diffPreFramePositionY += ItemHeight;

            if(currentItemNo > 0)
            {
                var item = itemList.Last.Value;
                itemList.RemoveLast();
                itemList.AddFirst(item);

                currentItemNo--;

                var pos = ItemHeight * currentItemNo;
                item.anchoredPosition = new Vector2(0, -pos);
                OnUpdateItem(currentItemNo, item.gameObject);
            }   
        }
    }

    private void OnUpdateItem(int itemIndex, GameObject gameObject)
    {
        if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT)
        {
            gameObject.SetActive(false);
        }
        else
        {
            gameObject.SetActive(true);

            var listElement = gameObject.GetComponentInChildren<ItemElement>();
            listElement.SetMessage("message == " + itemIndex);
        }
    }


    //-----------------------------------------------------------------------
    // getter

    private float anchoredPositionY
    {
        get
        {
            return -rectTransform.anchoredPosition.y;
        }
    }


    public float ItemHeight
    {
        get
        {
            if (_itemElementPrefab != null && _itemSizeheight == -1)
            {
                _itemSizeheight = _itemElementPrefab.sizeDelta.y + _margin;
            }
            return _itemSizeheight;
        }
    }

    protected RectTransform rectTransform
    {
        get
        {
            if (_rectTransform == null) _rectTransform = GetComponent<RectTransform>();
            return _rectTransform;
        }
    }
}

これをVerticalLayoutGroupの代わりに使えばOKです。

f:id:ghoul_life:20190730004034p:plain

終わり!なんて言うとブーイングの嵐なので、ちょっと解説をば。

Start

    void Start()
    {
        _itemElementPrefab.gameObject.SetActive(false);

        for (int i = 0; i < _instantateItemCount; i++)
        {
            var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform;
            item.SetParent(transform, false);
            item.name = "Recycle Item " + i.ToString();
            item.anchoredPosition = new Vector2(0, -ItemHeight * i);
            itemList.AddLast(item);

            item.gameObject.SetActive(true);

            OnUpdateItem(i, item.gameObject);
        }


        // 先に全アイテム分だけscrollRectを広げておくのだ
        var delta = rectTransform.sizeDelta;
        delta.y = ItemHeight * SAMPLE_ITEM_COUNT;
        rectTransform.sizeDelta = delta;
    }

ここでは、使い回す前提で指定個数しかprefabをinstantiateしないでおきます。
で、その際のanchorPositionを各アイテムの高さをずらして配置します。
今までであれば、VerticalLayoutGroupがやってくれていたことを自作するイメージです。

指定個数のprefabを作り終わったら、
このリスト全体の大きさを表示したい全ての項目分広げておきます。
(ここがポイントです。)

Update

スクロール処理はScrollRectがまかないます。
ScrollRectがcontentに指定しているRectTransformを動かします。
このupdateで動いた値をチェックして、逐一情報を更新します。
下に動かすか、上に動かすかで、リストの付与先(First or Last)が違うのに注意してください。

   void Update()
    {
        if (itemList.First == null)
        {
            return;
        }

        // 下
        while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2)
        {
            _diffPreFramePositionY -= ItemHeight;

            var item = itemList.First.Value;
            itemList.RemoveFirst();
            itemList.AddLast(item);

            var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo;
            item.anchoredPosition = new Vector2(0, -pos);

            OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject);

            currentItemNo++;
        }

        // 上
        while (anchoredPositionY - _diffPreFramePositionY > 0)
        {
            _diffPreFramePositionY += ItemHeight;

            if(currentItemNo > 0)
            {
                var item = itemList.Last.Value;
                itemList.RemoveLast();
                itemList.AddFirst(item);

                currentItemNo--;

                var pos = ItemHeight * currentItemNo;
                item.anchoredPosition = new Vector2(0, -pos);
                OnUpdateItem(currentItemNo, item.gameObject);
            }   
        }
    }

OnUpdateItem

ここでカラムの更新を行います。
最初に作った指定個数より少ない場合に注意。

    private void OnUpdateItem(int itemIndex, GameObject gameObject)
    {
        if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT)
        {
            gameObject.SetActive(false);
        }
        else
        {
            gameObject.SetActive(true);

            var listElement = gameObject.GetComponentInChildren<ItemElement>();
            listElement.SetMessage("message == " + itemIndex);
        }
    }

結果

簡単に要素を使い回すスクロール処理を作ることが出来ました。
今回の例では縦でしたが、xにすればもちろん横でも出来ます。

簡単なのに効果大です。
是非使ってみて下さい。

注意

「リストに入れるprefabの種類がいくつかあるんだけど…?」
こうなってくるとリサイクルがちょっと複雑になりそうです。
あくまでここでは単一のリストの例ってことで。

お疲れ様でした。
次はまた当分先になりそう!
サークル活動、ゲーム作りの方も頑張ります。

【Unity】unity1week「あつめる」 GOLD RUSHの実装について 【unity1week】

unity1week「あつめる」が終わりました

お疲れ様です。ぐーるです。

終わりましたunity1week。
今回も難産でした。
お題は「あつめる」。
ちょろっと作ったカードダンジョンRPGを利用して
ゴールドを集めるゲームにしようと思い立ちました。

f:id:ghoul_life:20190708211936g:plain

Twitter上でも非常に多くの方が注目してくれて
気恥ずかしい限りでしたが、非常に励みになりました。

本当にありがとうございます。
今回も毎回お馴染みの実装について紹介したいと思います。
なるべくコンパクトにまとめたいのですが、毎回長文です…。

設計について

正直あんまり話すこと無い…w
前回のやつと比べるとすごい簡単。全部uGUIだし。

前回のなんてObject Pool , Preload System , Recycler
とメモリ管理の嵐でInstantiateを鬼の形相で排除してました。

ghoul-life.hatenablog.com

これに比べたら全然言うことがない。

設計といえるほど工夫した点は特に無いんですが、
カードはもちろん継承して汎用的に処理をまとめて
どっからでも処理できるようにしてました。

UtilやExtension,NCMB,Effect,Soundなどゲームを補助するシステム
は省いてるけど基盤はこんな感じです。

f:id:ghoul_life:20190708212110p:plain

(基本的にメモリに乗っかっててほしいので、1sceneです。
3Dじゃないし、リソースそんなに多くないのでめちゃくちゃ軽量だしね)

EnemyEngine?

この関連図を見ると異様なものが一個ある。そうEnemyEngineです。
これは敵キャラのカードを作った後、そのカードのステータスを決定するシステムです。
フロアによって出現する敵を制御したり、パラメータをセットしたり、画像リソースを持ってたりする。

これ、本当に製品にするなら、全てのカードに作るべきだと思った。

「GoldCard」を作る時は必ず「GoldEngine」を経由する とか

カードを生成する時、パラメータが状況によって変わってくる。
武器やゴールドは値しかないので、その値入れるだけなのだが、
その処理を外に出しておくと見通しが良くなって拡張性が高くなっていく
今回は面倒でEnemy以外はそのまま書いてしまった。反省。

ちゃんとやるなら全てのカードはEngine化する

画面について

左半分カードフィールド側

f:id:ghoul_life:20190708212551p:plain

正直ちょっと失敗だったかも。
カード含めて全てuGUIでやってました。
が、カードの上に攻撃パーティクルを表示したくて
それを出すのにSortingLayerとか使って涙ぐましい努力してたりします。

SpriteRenderer + TextMeshProにしちゃって、3Dオブジェクトとして扱えばもっとラクに色々出来たかもしれない。
また、フロアが進むと3x3 , 5x5 , 7x7 ...とどんどんフィールドが広くなっていくので、
メモリ管理にどうなのかとちょっと不安ではありました。

今はいいですが20x20とかやろうとすると
同一カードのグルーピングをしてバッチング処理を考えた実装にしないと影響が出るかも知れません。

右半分ステータス側

また、値周りはUniRXのReactiveProperty使って値変えるだけで即座に反映されるようにしてました。
例えばゲーム一周してタイトルに戻った時に値を初期化するのですが、
その時にnewし直すと当たり前にsubscribeが外れるので、上手く回そう。
(newと初期値投入は別にするとかね。やっぱりInitializerクラスを作って別でやるべきだったかなぁと。
初期化処理は分散してますが、大量に処理があります。)

プログラムについて

特筆すべき所は…ありません!w
とか言うと本当に何にもなくなっちゃうので、むりやり捻出。
(コード的に難しい事は何にもしてないのでちょっと恥ずかしい)

f:id:ghoul_life:20190708212715p:plain

ゲームログを表示している箇所はキューイングして複数のログを受け取ることが出来て、
一個ずつ遅延評価してます。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System.Text;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace CardDungeon
{
    public class TextWriter : MonoBehaviour
    {

        [SerializeField] private Text _text;

        // 全部フィールド化して少しでもメモリ管理をラクにしてあげたい
        // 涙ぐましい努力
        private int _length;
        private string _textString;
        private StringBuilder _stringBuilder;
        private bool _isAnimation = false;
        private const float ONE_TIME = 0.01f;

        private List<string> _messageQue = new List<string>();

        public void Initialize()
        {
            _isAnimation = false;
            _text.text = "";
            _length = 0;
            _textString = "";
            _stringBuilder = null;
            _messageQue = new List<string>();
        }

        public void Show(string txt)
        {
            _messageQue.Add(txt);
        }

        private void Update()
        {
            if (!_isAnimation && _messageQue != null && _messageQue.Count > 0)
            {
                StartCoroutine(textWriter(_messageQue[0]));
            }
        }

        IEnumerator textWriter(string text)
        {
            _isAnimation = true;
            _length = 0;
            _stringBuilder = new StringBuilder();
            _text.text = _stringBuilder.ToString();

            yield return null;

            while (_length < text.Length)
            {
                _textString = text.Substring(_length, 1);
                _stringBuilder.Append(_textString);
                _text.text = _stringBuilder.ToString();
                _length++;


                yield return new WaitForSeconds(ONE_TIME);
            }

            _text.text = text;

            yield return new WaitForSeconds(0.25f);

            _isAnimation = false;
            _messageQue.RemoveAt(0);

            yield return true;
        }
    }
}

またPopupする処理は得意のプーリング。

f:id:ghoul_life:20190708212852p:plain

namespace CardDungeon
{
    public class PopupManager : SingletonMonoBehavior<PopupManager>
    {
        [SerializeField] Dialog _dialog;
        [SerializeField] Dialog _yesnoDialog;
        [SerializeField] RectTransform _popupCanvas;
        [SerializeField] PopText _popTextPrefab;

        private List<PopText> _popList;

        private readonly int POP_TEXT_CNT = 10;

        private bool _isEnable;

        public bool IsEnable { get => _isEnable; set => _isEnable = value; }

        public void Initialize()
        {
            if (_popList != null)
            {
                foreach (var p in _popList)
                {
                    Destroy(p);
                }
                _popList.Clear();
            }

            // 予め生成しておいて非表示する
            _popList = new List<PopText>();
            for (var i = 0; i < POP_TEXT_CNT; i++)
            {
                var p = Instantiate(_popTextPrefab, _popupCanvas);
                p.Initialize();

                _popList.Add(p);
            }

            _dialog.Initialized();
            _yesnoDialog.Initialized();
        }

        // 以下略
        public void ShowPopup(string valueString, RectTransform rect, Vector3 adjustPos)
        {

            if (!_isEnable) return;

            if (_popList != null)
            {
                // 使用中でないポップアップを使用する
                var p = _popList.FirstOrDefault(pop => !pop.IsUse);
                if (p != null)
                {
                    p.Show(valueString, rect, adjustPos);
                }
            }

        }

    }
}

簡単なコードだけどゲームには効果抜群です。
unity1weekとか出してるやつはほとんどやってそうですけど。

金土日の追い込みでやったこと

ここが一番つらく、楽しい時間です。
4時間睡眠 x 2です。(気絶するように寝る)

基本的なシステムは出来上がっていたが、
御粗末すぎるので、それを以下に製品に近づけるかの作業でした。

  • オンラインランキング
  • 敵エンジン
  • グラフィックを仮素材から正式版に(UIまで出来ず)
  • バグ取り(結局取り切れず。まれに同じ場所にカードが作られてしまう)
  • タイトル、チュートリアル、ゲームオーバー画面などのUI全部
  • その他細かい調整など

これは寝れない…。
寝たら死ぬぞ!と思いながらストロングゼロ呑んでました。
拾った画像なんですが、凄くお気に入りです。

f:id:ghoul_life:20190708213329j:plain

グラフィック周りは本当にうつらうつらと…。

終わって

今回もすごいゲームが多いです。
ほんとみんな発想の鬼だなと。

とある漫画家の方がインタビューで言ってましたが、
「目についたものでネームを切ってみる練習をしている」
と。
普段から「これをゲームにしたらどうだろう?」と考えながら
生活しているような人は発想力が鍛えられているのではないでしょうか。
自分もそうありたいものです。

ゲーム紹介ページにも書きましたが、沢山の課題が残ってます。
それでも遊んでくださった方がいて凄く嬉しいです。
本当にありがとうございます。

次回はもう少しぶっ飛んだ感じのやつ作りたいなと思ってます。

unityroom.com

【Unity】 文字列に含まれる絵文字を判別する

お世話になっております。ぐーるです。
また久しぶりになってしまいました。

ずっと開発はしてて、RPGとカードゲームを2本同時に作成しているのですが、
このRPG作るのが凄く楽しいのです。

シナリオ、システム、グラフィック、キャラクター
全部自分で用意するのですが、
キャラクターの絵を描く、描いたキャラクターをいきいきと会話させる、
といった事だけでとても楽しい。

「こんな会話させよう」「こうしたらこう展開出来るな」
といったシーンを考えて実装してるだけで楽しいです。
(現在進捗率70%)

妄想癖が功を奏するなんて事もあるのだな、と思います。

閑話休題、今日はunityで絵文字の判別をする方法を共有しようかなと。

unityで絵文字って使えるの?

基本的に使えません。
使おうと思うと特別なライブラリや対応を入れる必要があります。

baba-s.hatenablog.com

じゃあ対応する必要なくない?

使いたい、という要望があったり
外部サービスと連携していたりすると
対応する必要が出てきます。

判別方法

  1. 文字列からUnicode配列に変換
  2. Unicode配列からUTF32に変換
  3. UTF32で絵文字判定を行う

という手順で判別が可能です。
(コードは後ほど。先に絵文字とはというお話を)

Unicodeの絵文字って

www.unicode.org

Unicodeでは絵文字は上記ルールに沿って実現されています。
Unicode一つで表示出来るものもあれば、複数にまたがって表示しているものもあります。
これを一つ一つ判別する必要があります。

判別する方法として

の2つがあります。

正規表現パターンの方がオススメですが、
絵文字以外の文字を含めてしまう恐れがあります。
完全一致であれば、含めてしまう恐れが少なくなりますが、
追加があった場合、逐一入力する必要があります。

正規表現
iOSで扱われるUnicode 6.0絵文字の判定をする正規表現 · GitHub


完全一致例
Unity-UI-emoji/info.txt at master · mcraiha/Unity-UI-emoji · GitHub

実装

完全一致で実装する場合のコード例を共有します。
文字コードをまるっとソースに入れちゃってますが、
これはブログ用の実装で、プロジェクトにするならTextAssetsなど
の方が管理はしやすいでしょう。
また、絵文字コード表は全ては網羅出来ていないと思われますので、ご了承ください。

コード例)

    // inputからEmojiを取り除いた文を返却する
    public static string RemoveEmojiString(string inputString)
    {
        int i = 0;
        string firstString = null;
        string secondString = null;
        string threeString = null;
        string fourString = null;
        StringBuilder sb = new StringBuilder();
        var uint32Size = sizeof(UInt32);

        // unicode byte配列に文字列を変換
        byte[] unicodeBytes = Encoding.Unicode.GetBytes(inputString);

        // unicode配列からUTF32配列に変換
        var utf32Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF32, unicodeBytes);
        // 配列の長さ
        int length = utf32Bytes.Length / uint32Size;

        while (i < length)
        {
            // 1文字目をチェック
            firstString = BitConverter.ToUInt32(utf32Bytes, i * uint32Size).ToString("X4");

            // 2文字目までつなげてチェック
            if (i < (length - 1))
            {
                secondString = firstString + "-" + BitConverter.ToUInt32(utf32Bytes, (i + 1) * uint32Size).ToString("X4");
            }

            // 4文字目までつなげてチェック
            if (i < (length - 3))
            {
                threeString = BitConverter.ToUInt32(utf32Bytes, (i + 2) * uint32Size).ToString("X4");
                fourString = secondString + "-" + threeString + "-" + BitConverter.ToUInt32(utf32Bytes, (i + 3) * uint32Size).ToString("X4");
            }

            // 後ろから文字コード表と合わせてチェックして一致してたら絵文字と判断して読み飛ばす
            if (EMOJI_CODES.Any(e => e.Equals(fourString)))
            {
                i += 4;
            }
            else if (EMOJI_CODES.Any(e => e.Equals(secondString)))
            {
                i += 2;
            }
            else if (EMOJI_CODES.Any(e => e.Equals(firstString)))
            {
                i += 1;
            }
            else
            {
                // 正しい文字だけ投入
                sb.Append(Char.ConvertFromUtf32(BitConverter.ToInt32(utf32Bytes, i * uint32Size)));
                i += 1;
            }
        }

        return sb.ToString();
    }

    // 絵文字コード一覧表
    public static readonly string[] EMOJI_CODES =
    {
        "1F004",
        // 略 ~~~~~~~~
     };

文字コード一覧表をそのまま載せるとあまりにソースが長くなるので別ファイルにしました。

Unicode Emoji Code List · GitHub

あとがき

以上で絵文字の判別が出来ます。
コード見ると「はいはい、まあそうだよね」って感じですよね。

軽く調べるとサロゲートペアだけ判別すればいいよ、なんて
乱暴なコードもあったりして、ちょっと気になるなと思った次第です。

次はUnity1Weekの記事になりそうです。
うーん、もうちょっと更新したい。

【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