simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

AIに身体性を与えるためのマイクロワールドの構築11:モデルベース駆動

モデルとしているデータ世界に更新を加えると反応して変化するビュー側世界を観測できるようにしました。

f:id:simplestar_tech:20171224195439g:plain

地味ですが、ブロックを積み上げるデータ世界の出来事が、私たちが認識できる形で表示されています。
モデルを更新するとビューが反応する、当たり前ですが、とても重要なことです。

来年からは、細かいブロックのルールを決めて、世界が育っていく様を観察できるようにしていきます。

先週は3連休を取ったのですが40℃近い熱を出して寝込んでしまって…まったく作業できませんでした。
つらかった…ではまた次の休日にお会いしましょう!

以下、実装メモです…どうやって作っているか興味ある方以外は読む必要はありません。

構築5の頃から、観測者を Unity 側に置いていましたが、これが間違いであることにやっと気づきました。

以下の処理は本当は Model 側が行わなければならなかったのです。

有効視界内に新たに入るチャンクを、自分に近いチャンクごとのキューに加えて、未読み込みのチャンクについて、チャンクファイルデータの読み込みとメッシュの構築を走らせます。
開放チャンクキューに入っているチャンクを次のアクティブチャンクの切り替え時に取り出して、その時に有効視界範囲外のチャンクになっていたら、チャンクデータを開放します。

外部から Observer が何チャンクにいるのか、これを指定する必要があります。

まずは Unity 座標系で Observer の位置からチャンクを特定する処理を UnityWorld 側に作ります。

既に作っていました。

namespace SimpleStar
{
    public class UnityWorld : SimpleMonoBehaviour
    {
        public static Chunk GetChunk(Vector3 position)
        {
            return World.Instance.ChunkArray[_RemapX(Mathf.FloorToInt(position.x / (UnityChunk.WidthOffset * Chunk.Width)))][_RemapZ(Mathf.FloorToInt(position.z / (UnityChunk.DepthOffset * Chunk.Depth)))];
        }
    }
}

この Observer がいるチャンクをモデル側の World に伝えます。

World.Instance.SetObserverChunk(UnityWorld.GetChunk(this.transform.position));

このときに、モデル側で必要なチャンクの読み込みが走るものとします。

しかし、読み込みは非常に時間を要する処理なので、非同期実行で読み込む必要があります。
これは外側から UniRX の機能を使って次のように呼ぶことで解決します。

            Vector3 observerPosition = _observer.transform.position;
            Observable.Start(() => {
                World.Instance.SetObserverChunk(GetChunk(observerPosition));
            }).Subscribe(_ => {
            });

そして、これまでビュー側で調べていた部分を、モデル側から更新通知を受け取るように修正します。
ここで、モデル側では 0~255 チャンクしかありませんが、ビュー側は -5~5 チャンクで表示するなど、observer の位置によって
チャンクの出現位置が異なることが明らかになりました。

そこで、ビューはその時のチャンクがどこに配置されるべきなのか計算する必要があります。
この計算を具体的にすると次の通りです。

        private Vector3 GetChunkObjectPosition(Chunk chunk, out int offsetX, out int offsetZ)
        {
            Vector3 observerPosition = _observer.transform.position;
            Chunk zeroChunk = GetChunk(observerPosition);

            offsetX = chunk.Longitude - zeroChunk.Longitude;
            if (World.Instance.LongitudeSize / 2 < Mathf.Abs(offsetX))
            {
                if (0 < offsetX)
                {
                    offsetX = (chunk.Longitude - World.Instance.LongitudeSize) - zeroChunk.Longitude;
                }
                else if (0 > offsetX)
                {
                    offsetX = chunk.Longitude - (zeroChunk.Longitude - World.Instance.LongitudeSize);
                }
            }

            offsetZ = chunk.Latitude - zeroChunk.Latitude;
            if (World.Instance.LatitudeSize / 2 < Mathf.Abs(offsetZ))
            {
                if (0 < offsetZ)
                {
                    offsetZ = (chunk.Latitude - World.Instance.LatitudeSize) - zeroChunk.Latitude;
                }
                else if (0 > offsetZ)
                {
                    offsetZ = chunk.Latitude - (zeroChunk.Latitude - World.Instance.LatitudeSize);
                }
            }

            float widthOffset = UnityChunk.WidthOffset * Chunk.Width;
            float depthOffset = UnityChunk.DepthOffset * Chunk.Depth;

            int observerChunkPositionX = Mathf.FloorToInt(observerPosition.x / widthOffset);
            int observerchunkPositionZ = Mathf.FloorToInt(observerPosition.z / depthOffset);

            Vector3 chunkObjectPosition = new Vector3(observerChunkPositionX + offsetX * widthOffset, 0, observerchunkPositionZ + offsetZ * depthOffset);
            return chunkObjectPosition;
        }

これで 0~255 なのに -5~5 の位置にオブジェクトが配置され…

f:id:simplestar_tech:20171203233552j:plain

そして、領域外のチャンクは対象から外さなくてはなりません。
一番外周のチャンクについて削除します。
これで移動し続けてもずっとチャンクが残るような心配はなくなりました。

あと、関係ないようですが Windows Mixed Reality に対応したゲームとするため MixedRealityToolkit-Unity を導入しました。
github.com

Input の取り方が変わります。

シーン設定にて Add the Input Manager prefab を行い、その Input Manager オブジェクトに
XboxControllerInputSource コンポーネントを追加します。
加えて今まで Input から直接値を取っていたコードを次のように修正します。

using HoloToolkit.Unity.InputModule;

namespace SimpleStar
{
    public class UnityAgent : XboxControllerHandlerBase
    {
        public override void OnXboxAxisUpdate(XboxControllerEventData eventData)
        {
                // read inputs
                float h = eventData.XboxLeftStickHorizontalAxis;
                float v = eventData.XboxLeftStickVerticalAxis;
                bool crouch = OnButton_Pressed(XboxControllerMappingTypes.XboxA, eventData);

追記:HoloToolkit が勝手に変更加えたらしく、最新版では次のコードになってました

public override void OnXboxInputUpdate(XboxControllerEventData eventData)

さてさて、いよいよモデルベース駆動で重要な部分を組んでいきます。

今モデル側で変更が入ったとき、ビュー側が反応するように作られています。
例えば定期的に World 側を更新するような次のコードを書いてみます。

        public void Test()
        {
            Chunk debugChunk = _chunkData[0][0];

            for (int i = 0; i < Chunk.Width; i++)
            {
                for (int j = 0; j < Chunk.Depth; j++)
                {
                    for (int k = 0; k < Chunk.Height; k++)
                    {
                        int blockId = debugChunk.BlockData[i][j][k];
                        int nextBlockId = GetNextBlockData(debugChunk, out debugChunk, i, j, k - 1);
                        if (0 != nextBlockId && 0 == blockId)
                        {
                            debugChunk.BlockData[i][j][k] = 1;
                            break;
                        }
                    }
                }
            }
            for (int l = 0; l < Chunk.LayerCount; l++)
            {
                debugChunk.LayerUpdated[l] = true;
            }

            if (null != _viewWorld)
            {
                _viewWorld.UpdateChunkMesh(debugChunk);
            }
        }

すると、このように定期的に世界が更新されるようになりました!

f:id:simplestar_tech:20171224194732g:plain

モデルを更新するとビューが反応する。
やっと基礎的なことが確認できるようになってきました。