simplestarの技術ブログ

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

AIに身体性を与えるためのマイクロワールドの構築16:風・土そして水

世界を構成する四大元素は風・土・水・火でした。
現在は風と土だけで世界が構成されていますので、水を作り出す必要があります。

イメージとしてはこんな感じ
f:id:simplestar_tech:20180114171718j:plain

水は液体なので、固体である土と違ってより低い風ブロックの隙間へ移動していくものです。

以前より作ってきたバックグラウンド処理にて、チャンクごとにすべてのブロックを処理し、もし水ブロックだったら周囲のブロックを確認して、直下が風ブロックだったら交換、直下が土ブロックだったら側面の風ブロックを確認して、もし風ブロックを見つけたら、その風ブロックと交換する仕組みを入れてみます。

期待では水に囲まれた中に空気ブロックがあると、これがバブルのように水面へ上昇してくると思います。
さっそく水ブロックの表現を作っていきましょう。

水を半透明の青いマテリアルとし、透過先に土ブロックが見えないといけませんので、水ブロックのメッシュの向こう側にもう一つ土メッシュを用意しなければなりません。
処理負荷はこの際無視して、半透明なブロックを考慮した2つ目のメッシュ作成の構造を考えていきます。

。。。と同時に、今のメッシュが一つの仕組みで水面の表現ができないか考えて、次のようなアイディアが浮かび上がってきました。
隣接するブロックが水なら土ブロックのメッシュを作成する。

とりあえず水ブロックを定義して、従来のシステムにおいて半透明なマテリアルを与えるとどのように見えるのか確認してみました。

f:id:simplestar_tech:20180114172041j:plain

要するに、この問題をどうにかしようと考えて、次のコードを閃いたわけです。
風にも水にも触れていないブロックは描画しないが、それ以外は描画するというロジックです。

int blockID = Chunk.GetBlockId(_chunk.BlockData[x][z][y]);
if (AirMaterialId != blockID)
{
    int n2 = World.Instance.GetNextBlockData(_chunk, out nextChunk, x + 1, z, y);
    int n5 = World.Instance.GetNextBlockData(_chunk, out nextChunk, x - 1, z, y);

    int n1 = World.Instance.GetNextBlockData(_chunk, out nextChunk, !bZOffset ? x : x + 1, z + 1, y);
    int n3 = World.Instance.GetNextBlockData(_chunk, out nextChunk, !bZOffset ? x : x + 1, z - 1, y);
    int n4 = World.Instance.GetNextBlockData(_chunk, out nextChunk, !bZOffset ? x - 1 : x, z - 1, y);
    int n6 = World.Instance.GetNextBlockData(_chunk, out nextChunk, !bZOffset ? x - 1 : x, z + 1, y);
    int t1 = World.Instance.GetNextBlockData(_chunk, out nextChunk, x, z, y + 1);
    int s1 = World.Instance.GetNextBlockData(_chunk, out nextChunk, x, z, y - 1);

    int subMeshIndex = materialIndices[blockID];
    if (WaterMaterialId == blockID && AirMaterialId == t1)
    {
        subMeshIndex = materialIndices[blockID + 1];
    }

    if (AirMaterialId != n1 && AirMaterialId != n2 && AirMaterialId != n3 && AirMaterialId != n4 && AirMaterialId != n5 && AirMaterialId != n6 && AirMaterialId != t1 && AirMaterialId != s1)
    {
        // not touch wind
        if (WaterMaterialId != n1 && WaterMaterialId != n2 && WaterMaterialId != n3 && WaterMaterialId != n4 && WaterMaterialId != n5 && WaterMaterialId != n6 && WaterMaterialId != t1 && WaterMaterialId != s1)
        {
            // not touch water
            continue;
        }
    }

この結果、次のレンダリング結果を得ました。

f:id:simplestar_tech:20180114172841j:plain

たまたま発見したのですが、水面オブジェクトだけ上面をレンダリングするようにすると、深いところのブロックが暗く表示されることを確認しました。
水中から上を見るとうまくありませんが、水面より上にカメラがあるときは良い絵が作れます。

では見た目が完成したので、ブロックのプラグインを考えていきましょう。

インタフェースは仮で次のようなものを用意してみました。

public interface IHexWorldUtility
{
    BlockData GetBlockData(int widthIndex, int depthIndex, int heightIndex);
    void SetBlockData(int widthIndex, int depthIndex, int heightIndex, BlockData blockData);
}

public interface IBlockLogic
{
    int BlockId();
    void Execute(int widthIndex, int depthIndex, int heightIndex, IHexWorldUtility pluginUtility);
}

public struct BlockData
{
    public ushort blockId; // 12bit (0~4095)
    public byte impuritiesId; // 4bit (0~15)
    public byte impuritiesRatio; // 4bit (0~15)
    public byte stress; // 6bit (0~63)
    public byte heat; // 6bit (0~63)

単純に水を土に変えるブロックロジックを作ってみました。

結果イメージ
f:id:simplestar_tech:20180114214525j:plain

今はメッシュの更新のタイミングがオブザーバーが移動したときだけなので、モデル側が更新されてもビュー側は更新されません。

チャンクの処理が一通り終わり、データに更新があるかどうかを確認して、もし更新があれば、ビューにチャンクのメッシュ更新を行うよう要請する仕組みを作ってみました。

結果イメージ
f:id:simplestar_tech:20180114214605j:plain

あとは正しくブロックのロジックを作るだけですね。
まずは単純なロジックを書いてみます。
水が風に上面で触れていたなら、干上がるように風になるというロジックです。

    public class WaterLogic : IBlockLogic
    {
        public int BlockId()
        {
            return 2; // @debug しかるべきリストから参照するべき
        }

        public void Work(int widthIndex, int depthIndex, int heightIndex, IHexWorldUtility worldUtility)
        {
            BlockData blockData = worldUtility?.GetBlockData(widthIndex, depthIndex, heightIndex + 1) ?? BlockData.Identity;
            if (0 == blockData.blockId)
            {
                blockData.blockId = 0;
                worldUtility?.SetBlockData(widthIndex, depthIndex, heightIndex, blockData);
            }
        }
    }

実行結果は次の動画で確認できます。

www.youtube.com

では、冒頭で書きました、低いところを目指す水のロジック書きます。
以下のコードになりました。

#if !USE_PLUGINS
using System;
using System.Collections.Generic;

namespace SimpleHexWorld
{
    public class WaterLogic : IBlockLogic
    {
        public int BlockId()
        {
            return 2; // @debug しかるべきリストから参照するべき
        }

        public void Work(int widthIndex, int depthIndex, int heightIndex, IHexWorldUtility worldUtility)
        {
            double randomValue = _random.NextDouble();

            if (0.1f < randomValue)
            {
                // return;
            }

            BlockData blockData = worldUtility?.GetBlockData(widthIndex, depthIndex, heightIndex - 1) ?? BlockData.Identity;
            if (0 == blockData.blockId)
            {
                blockData.blockId = 0;
                worldUtility?.SetBlockData(widthIndex, depthIndex, heightIndex, blockData);

                blockData.blockId = 2;
                worldUtility?.SetBlockData(widthIndex, depthIndex, heightIndex - 1, blockData);
            }
            else
            {
                List<int> sideIndices = new List<int>();
                for (int sideIndex = 0; sideIndex < 6; sideIndex++)
                {
                    int sideWidthIndex = widthIndex;
                    int sideDepthIndex = depthIndex;
                    worldUtility?.GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);

                    BlockData sideBlockData = worldUtility?.GetBlockData(sideWidthIndex, sideDepthIndex, heightIndex) ?? BlockData.Identity;
                    if (0 == sideBlockData.blockId)
                    {
                        sideIndices.Add(sideIndex);
                    }
                }
                if (0 < sideIndices.Count)
                {
                    int randIndex = _random.Next(0, sideIndices.Count);
                    int sideWidthIndex = widthIndex;
                    int sideDepthIndex = depthIndex;
                    worldUtility?.GetSideBlockIndex(widthIndex, depthIndex, sideIndices[randIndex], out sideWidthIndex, out sideDepthIndex);
                    blockData.blockId = 0;
                    worldUtility?.SetBlockData(widthIndex, depthIndex, heightIndex, blockData);

                    blockData.blockId = 2;
                    worldUtility?.SetBlockData(sideWidthIndex, sideDepthIndex, heightIndex, blockData);
                }
            }
        }

        private Random _random = new Random();
    }
}
#endif

実行結果は次の通り

www.youtube.com

フレームレートは60と落ちませんが、チャンク処理のループが遅いので非常にゆっくりと水ブロックは更新されていきます。
カメラ周辺のチャンクは昔話の竜宮城の外のように、周囲よりもずっと時間の進みが早い方が遊んでいて反応があって面白いかもしれません。
半径5チャンクぐらいなら、半径ごとにチャンク処理スレッドを割り当ててもいいかも

動きに不満があるとしたら、まずバブルが上昇することはありませんでした。
理由として、height 0からheight max まで順番に入れ替え処理がチャンク単位で行われるため、一度に空気ブロックは水面まで上昇してしまうことが挙げられます。
あと、側面への移動は空いている箇所へのランダム選択ですが、ここは直前のブロックの進んだ向きに最も近い方向へ進むべきだと思います。
慣性システムのために移動方向に周囲6,上下2の8つのdirectionとして3bitどこかblokから割いても良いかもしれませんね。
そのほか stress の計算を先に済ませて、最も圧をうけているブロックから力を受けて遠ざかる方向へ移動するのも自然かもしれません。

f:id:simplestar_tech:20180115083850g:plain