simplestarの技術ブログ

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

オープンワールドにおけるキャラクタAI(NPC)のあり方

ゲーム内のキャラクタを制御するAIを作る場合、周りから収集した情報を使って意思決定をし、実際に行動に移すまでの処理の流れを設計する必要がある。

…という考え方から、すでに間違った方向へ思考が働いていた!という話

f:id:simplestar_tech:20171013083708j:plain

画像の出典 www.jokeroo.com
がリンク切れてしてしまったので、それを引用しているページ

従来型のこの手法は、収集した情報から意思決定をする処理を、すべて人間がデザインしなければなりません。
オープンワールドにおける状況や選択肢は膨大な組み合わせとなりますので、当然人間にはデザインしきれない量となります。
するとゲーム内のキャラクタAIは状況に合わせることができず、単調な反応だけを返す、内部のロジックが丸見えの、残念な振る舞いをするキャラクタになってしまうのです。

では、単純に動的に行動を決定する仕組みを考えていけばよいのでしょうか?
これも答えは NO です。

そうした場合、未来予知という極めて難解なタスクをこなさなければならなくなり
学術界の最先端では有効な解法としてモンテカルロ木探索アルゴリズムが提案されていますが
オープンワールドのような選択肢があいまいなゲーム内では、適用できないケースが多く、パフォーマンス面からも実用的な解法とは言えません。
囲碁や将棋は完全情報の限られたパターン数の局面と、膨大な棋譜が存在するため、うまく機能することが多いだけなのです。

さて、つまり現在のオープンワールド(どこまでも世界が広がっているような)ゲームは未来予知、つまり高精度な予測シミュレーションが行えないがために、動的に正しい行動の選択ができないのです。
問題がはっきり見えてきました、ということは、もし高精度な未来予測が行えるオープンワールドのゲームができたなら、解決へ向かうということです。

これからのオープンワールドのキャラクタAI(NPC)のあり方として私が考えるのは
高精度な未来予測が行えるオープンワールドシステムを用意し、その上で、NPCは動的に正しい(報酬を受け取れる)行動を選択する、というものです。
従来は人間がゲームデザインの段階で未来予測をして、様々な状況の判定処理を記述し、限られた条件において最善の行動をするようにAIを記述してきましたが(ビヘイビアツリーやステートマシンによるルールベース手法)
私は新規に、未来予測が行えるワールドシステムをデザインし、その未来予測の結果として高い報酬を得られる行動を選択する仕組みを考えだしました。(オープンワールドモンテカルロ木探索)

ただ、私が考える手法を実現するには、オープンワールドを部分空間で区切ってシミュレートできるという、まったく新しいデザインで一から作り上げる必要があるため、これまで考えた人はいても実装の時間的制約の問題から実現した人はいません。
ちょっと試してみる、のハードルが高くてできない研究なのです。

…ということで、引き続きオープンワールド(マイクローワールド)を作っていきます。

六角柱を敷き詰めたマイクロワールドの構築(メッシュの結合とマルチマテリアル処理)

AIに身体性を与えるためのマイクロワールドの構築3です。

マルチマテリアル処理と、オクルージョンカリングについて記載します。
まずは、パフォーマンスにどれだけ差が出るか、一番簡単で頭の悪い実装を行ってみたいと思います。

以前パーリンノイズで、キューブオブジェクトを配置していくデモを見ましたね。
simplestar-tech.hatenablog.com

こちらのコードを参考に六角柱オブジェクトを配置していくデモシーンを作ってみましょう。

ざっと 180 x 40 個の六角柱を表示したところ 20fps で SetPass call が 14100 回という値でした。
f:id:simplestar_tech:20171009170254j:plain

では、カリングなしで、とりあえず全部同じ GameObject となるように修正してみます。
つまり、巨大な頂点バッファとインデックスバッファとUV座標バッファを構築して、描画するという手段を取ってみます。

先ほどと同じように 180 x 40 個の六角柱を表示したところ 77fps で SetPass call が 19 回という値でした。
パフォーマンスモニターで Draw に要した時間は 0.53 ms でした。
f:id:simplestar_tech:20171009222457j:plain

つまり、描画速度は 94倍アップしました。
さて、試してみてわかったことに、残念ながら全頂点を一つのメッシュに格納できなかったため
180 x 4 個の六角柱ずつ、計10 個のオブジェクトで描画することにしました。

マテリアルが一色なのは、サブメッシュカウントを1にしているためです。
続いて、深度によって色分けされるように追加実装してみます。

f:id:simplestar_tech:20171009230544j:plain

できました。

サブメッシュ数を増やすことで、SetPass call が 67 回と増えてしまいましたが
パフォーマンスモニターで Draw に要した時間を調べたところ 0.64 ms と、そこまでパフォーマンスは落ちませんでした。

対象 Unity シーンは HexRandomMap です。
GitHub にアップしましたので、実装が気になる方はこちらを見てみてください。

github.com

マルチマテリアル処理まで、確認しました。
引き続きオクルージョンカリングについて記載していきますよ!

続く…

2017/10/15 続きです。

まずオクルージョン処理に入る前にいくつか障害があります。
どういう障害なのかもわからないので、一つ一つ具体的にしていく作業から始めます。

なんとなく気づいたこととして
指定した幅と高さが感覚と合わない問題がありました。

これを頑張って解決しました。
以下の絵は 8x8 を指定したときのメッシュ結合結果です。

f:id:simplestar_tech:20171015164340j:plain

これで感覚に沿うようになりました。

X軸に六角形が 8 つ並び、Z軸方向は密に配置されるように交互にオフセットが入るようになっています。

f:id:simplestar_tech:20171015165124j:plain

頂点数と三角形数は、特にカリングが入っていないのでこのような数字となっています。

続いて、ワールドを構成する情報をもとにビジュアライズしていないという問題があります。
こちらも頑張って解決しまして

次のように3次元配列にブロックのあるなしを指定することで

        _mapData[0][0][0] = 1;
        _mapData[0][0][1] = 1;
        _mapData[0][0][2] = 0;
        _mapData[0][0][3] = 1;
        _mapData[1][0][0] = 1;
        _mapData[0][1][0] = 1;

f:id:simplestar_tech:20171015230428j:plain

3次元空間のブロックの積み重ねをメッシュ結合ともとに、ビジュアライズできるようにしました。

そして残るは、接合している表示されることのない三角形や、完全に埋もれてしまう頂点をメッシュから除去する仕組みが入っていない問題があります。

例えばこれ、8x8x8の結合メッシュですが
f:id:simplestar_tech:20171015231715j:plain

こんなに頂点数や三角形数はいらないはずなんです。

こちらを頑張って解決することにしました。

f:id:simplestar_tech:20171016232843j:plain

頑張ると、こんな感じで頂点数や三角形数が減りました。
これ以上減らすことは不可能なので、頑張るのはここまでにします。

試しに1000万ブロックを敷き詰めたマイクロワールドを描画してみました。
f:id:simplestar_tech:20171016231940j:plain
プロファイラーで確認したところ、81.8FPS の Wait for FPS が 91% の 11.79ms でした。
つまり、描画に要している時間は 0.92 ms ほどでした。
f:id:simplestar_tech:20171016233727j:plain
ブロック一つ一つにゲームオブジェクトを割り当てた最初の簡単なアプローチと比べて
大幅にパフォーマンスアップしました。

あとはコード整理ですが、ひとまず六角柱を敷き詰めたマイクロワールドの構築がイメージに近づいてきた
(マルチマテリアル処理と、オクルージョンカリングについて記載した)ので、次の記事に進みます。

GitHub DesktopでUnityプロジェクトのバージョン管理(初心者向け)

ゲーム作りをまじめに始めることにしたので、ソースコードのバージョン管理をしていきます。
選択肢はいろいろありますが、慣れ親しんだツールを用いたいので、GitHubを使います。

どういうものかはいろいろなサイトがやさしくまとめてくれていますので
github とは - Google 検索
ここでは、実際に行った作業ログだけ残します。

Windows 環境なので、まずは Git for Windows をダウンロードしてインストール
git-for-windows.github.io

これで、どのフォルダでも GitBush なるコンソール画面にて git コマンドを打てるようになります。
右クリックのコンテキストメニューから GitBush を選べるようになっているはず。
試しに git init を打つと、git リポジトリとして初期化してくれるでしょう。

さて、次に GUI ツールとして GitHub Desktop を導入します。
desktop.github.com
Windows用のダウンロード、インストールを終えましたら
GitHub アカウントを記入して開始します。

まだ GitHub アカウントを取得していない場合は、こちらで Sign up を完了させてください。
github.com

簡単な使い方だけ示しておきます。

ローカルファイルをバージョン管理に追加

まずは、適当に Unity プロジェクトを作ります。
Assets フォルダが置かれているパスをカレントに GitBush を起動して git init します。
これでローカルリポジトリの完成です。
加えて、リモート管理用に無視リストを配置しておきましょう。
.gitignore テキストファイルを作り
下記のテキスト内容を書き込んでおきます。

/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/Assets/AssetStoreTools*

# Visual Studio 2015 cache directory
/.vs/

# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb

# Unity3D generated meta files
*.pidb.meta

# Unity3D Generated File On Crash Reports
sysinfo.txt

# Builds
*.apk
*.unitypackage
空のリモートリポジトリの作成

次の GitHub のページに行き、リモートリポジトリを作成します。
https://github.com/

注意点として、まったくの空っぽのリポジトリとします。
これで、リモートリポジトリのアドレスだけが用意されました。

ローカルとリモートの統合処理

空のリモートリポジトリを作成したときに、すでにそのリポジトリに案内が書かれていると思いますが
ローカルでまずはコミットまで行ってしまいましょう。

GitHub Desktop を起動して、File メニューより、Add Local Repository より、さっき git init したフォルダを選択します。
.gitignore テキストファイルが有効に機能していれば Library などの重たいファイルはリモート管理対象から外されているはずです。
ここまで確認できたら、GitHub Desktop の master へ commit ボタンを押してファイルをローカルリポジトリにコミットします。

続いて、空のリモートリポジトリのページに書かれている remote add コマンドを git init を行った時と同じプロンプトに書き込んで実行します。
git remote add origin https://github.com/*****/*******.git

あとは、次の push コマンドを実行することで、リモートに完全に情報がアップロードされます。(初回だけアップ先リポジトリGitHub アカウント情報を記入するポップアップダイアログが出る)
git push -u origin master

これで Unity プロジェクトは、皆さんと共有できる形で GitHub にアップロードされました。

github.com

おまけとして、README.md のマークダウン方式の記入方法を示します。

# RandomMapMaker

Main image  

![title](doc/main_image.jpg "alt")  

Unity 2017.1.1f1(64bit) Project  

# Scenes

それぞれの Unity シーンの説明  

- RandomMap  
![title](doc/main_image.jpg "alt")  

パーリンノイズを用いてキューブを敷き詰めるマップを生成  
Unityで async/await を使ったスレッド操作スクリプトをテスト  
(別スレッドにて 10000 ループを終えたのち、マップオブジェクトをx方向へ大きく移動させる)  

- HexPrism  
![title2](doc/hex_prism.jpg "alt2")  

頂点情報、インデックスリスト、UV値、テクスチャマテリアルをすべてスクリプトから生成してメッシュモデルを構築・表示するシーン  
(全頂点情報はモデリングツールを使わずに、頭の中で考えながら手書きにより作り出されたものです)  

覚えづらいのは改行のために半角スペースが2連続行末に必要ということと
画像を挿入する書式→ ![画像id](イメージファイル相対パス "マウスオーバー文字")
おぼえづらい!ので、思い出せなくなったらこのページに来て、コピペする!


以上、初めての Unity GitHub Desktop でバージョン管理でした!

Unity:テクスチャ付き六角柱メッシュをスクリプトだけで構築しました

AIに身体性を与えるためのマイクロワールドの構築2です。

まずは三角形を、頂点データを構築することによって、表示してみたいと思います。

参考にしたページはこちら
www.shibuya24.info

ただ三角形を表示するのも芸がないので、六角柱を作ってみました。

f:id:simplestar_tech:20171007220344j:plain

作成コードは次の通り

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

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class CreateMeshScript : MonoBehaviour {

    [SerializeField]
    private Material _mat;

    // Use this for initialization
    void Start () {

        var mesh = new Mesh();
        float root3 = Mathf.Sqrt(3f);

        Vector3[] positions = new Vector3[] {
            new Vector3 (0f, 1f, 0f),
            new Vector3 (0f, 1f, 2f),
            new Vector3 (root3, 1f, 1f),
            new Vector3 (root3, 1f, -1f),
            new Vector3 (0f, 1f, -2f),
            new Vector3 (-root3, 1f, -1f),
            new Vector3 (-root3, 1f, 1f),

            new Vector3 (0f, -1f, 0f),
            new Vector3 (0f, -1f, 2f),
            new Vector3 (root3, -1f, 1f),
            new Vector3 (root3, -1f, -1f),
            new Vector3 (0f, -1f, -2f),
            new Vector3 (-root3, -1f, -1f),
            new Vector3 (-root3, -1f, 1f),
        };
        mesh.vertices = new Vector3[] {
            // 天板
            positions[0], positions[ 1], positions[ 2], positions[ 0], positions[ 2], positions[ 3], positions[ 0], positions[ 3], positions[ 4], positions[ 0], positions[ 4], positions[ 5], positions[ 0], positions[ 5], positions[ 6], positions[ 0], positions[ 6], positions[ 1], 
            // 底板
            positions[7], positions[ 9], positions[ 8], positions[ 7], positions[10], positions[ 9], positions[ 7], positions[11], positions[10], positions[ 7], positions[12], positions[11], positions[ 7], positions[13], positions[12], positions[ 7], positions[ 8], positions[13], 

            // 側面
            positions[1], positions[ 8], positions[ 2], 
            positions[2], positions[ 9], positions[ 3], 
            positions[3], positions[10], positions[ 4], 
            positions[4], positions[11], positions[ 5], 
            positions[5], positions[12], positions[ 6],
            positions[6], positions[13], positions[ 1], 
            // 側面2
            positions[1], positions[13], positions[ 8],
            positions[2], positions[ 8], positions[ 9],
            positions[3], positions[ 9], positions[10],
            positions[4], positions[10], positions[11],
            positions[5], positions[11], positions[12],
            positions[6], positions[12], positions[13]           
        };

        int[] triangles = new int[mesh.vertices.Length];
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            triangles[i] = i;
        }
        mesh.triangles = triangles;
        mesh.RecalculateNormals();

        var filter = GetComponent<MeshFilter>();
        filter.sharedMesh = mesh;

        var renderer = GetComponent<MeshRenderer>();
        renderer.material = _mat;

    }

    // Update is called once per frame
    void Update () {
		
	}
}

続いて、次のテクスチャを貼ります。
f:id:simplestar_tech:20171008153753p:plain

ということで貼りました。

f:id:simplestar_tech:20171008153828j:plain

うまく貼られているようですね。

この UV 設定まで行うコードを次に示します。

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

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class CreateMeshScript : MonoBehaviour {

    [SerializeField]
    private Material _mat;

    // Use this for initialization
    void Start() {

        var mesh = new Mesh();
        float root3 = Mathf.Sqrt(3f);

        Vector3[] positions = new Vector3[] {
            new Vector3 (0f, 1f, 0f),
            new Vector3 (0f, 1f, 2f),
            new Vector3 (root3, 1f, 1f),
            new Vector3 (root3, 1f, -1f),
            new Vector3 (0f, 1f, -2f),
            new Vector3 (-root3, 1f, -1f),
            new Vector3 (-root3, 1f, 1f),

            new Vector3 (0f, -1f, 0f),
            new Vector3 (0f, -1f, 2f),
            new Vector3 (root3, -1f, 1f),
            new Vector3 (root3, -1f, -1f),
            new Vector3 (0f, -1f, -2f),
            new Vector3 (-root3, -1f, -1f),
            new Vector3 (-root3, -1f, 1f),
        };
        mesh.vertices = new Vector3[] {
            // 天板
            positions[ 0], positions[ 1], positions[ 2], // 1 
            positions[ 0], positions[ 2], positions[ 3], // 2
            positions[ 0], positions[ 3], positions[ 4], // 3
            positions[ 0], positions[ 4], positions[ 5], // 4
            positions[ 0], positions[ 5], positions[ 6], // 5
            positions[ 0], positions[ 6], positions[ 1], // 6 
            // 底板
            positions[ 7], positions[ 9], positions[ 8], // 1
            positions[ 7], positions[10], positions[ 9], // 2
            positions[ 7], positions[11], positions[10], // 3
            positions[ 7], positions[12], positions[11], // 4
            positions[ 7], positions[13], positions[12], // 5
            positions[ 7], positions[ 8], positions[13], // 6 

            // 側面
            positions[1], positions[ 8], positions[ 2], // 1天
            positions[2], positions[ 8], positions[ 9], // 1底

            positions[2], positions[ 9], positions[ 3], // 2天
            positions[3], positions[ 9], positions[10], // 2底

            positions[3], positions[10], positions[ 4], // 3天
            positions[4], positions[10], positions[11], // 3底

            positions[4], positions[11], positions[ 5], // 4天
            positions[5], positions[11], positions[12], // 4底

            positions[5], positions[12], positions[ 6], // 5天
            positions[6], positions[12], positions[13], // 5底

            positions[6], positions[13], positions[ 1], // 6天
            positions[1], positions[13], positions[ 8], // 6底
 
        };

        int[] triangles = new int[mesh.vertices.Length];
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            triangles[i] = i;
        }
        mesh.triangles = triangles;

        Vector2[] uvSources = new Vector2[]
        {
            // 天板
            new Vector2 (0.25f, 0.25f),
            new Vector2 (0.25f, 0.50f),
            new Vector2 (0.50f, 0.375f),
            new Vector2 (0.50f, 0.125f),
            new Vector2 (0.25f, 0.0f),
            new Vector2 (0.00f, 0.125f),
            new Vector2 (0.00f, 0.375f),
            // 底板
            new Vector2 (0.75f, 0.25f),
            new Vector2 (0.75f, 0.50f),
            new Vector2 (0.75f, 0.0f),
            new Vector2 (1.00f, 0.125f),
            new Vector2 (1.00f, 0.375f),
            // 側面x6
            new Vector2 (0.25f, 1.00f),
            new Vector2 (0.25f, 0.75f),
            new Vector2 (0.00f, 1.00f),
            new Vector2 (0.50f, 1.00f),
            new Vector2 (0.50f, 0.75f),
            new Vector2 (0.75f, 1.00f),
            new Vector2 (0.75f, 0.75f),
            new Vector2 (0.00f, 0.75f),
            new Vector2 (0.50f, 0.50f),
            new Vector2 (0.00f, 0.50f),
        };

        Vector2[] uvs = new Vector2[] {
            // 天板
            uvSources[0], uvSources[1], uvSources[2], // 1
            uvSources[0], uvSources[2], uvSources[3], // 2
            uvSources[0], uvSources[3], uvSources[4], // 3
            uvSources[0], uvSources[4], uvSources[5], // 4
            uvSources[0], uvSources[5], uvSources[6], // 5
            uvSources[0], uvSources[6], uvSources[1], // 6

            // 底板
            uvSources[7], uvSources[2], uvSources[8],   // 1
            uvSources[7], uvSources[3], uvSources[2],   // 2
            uvSources[7], uvSources[9], uvSources[3],   // 3
            uvSources[7], uvSources[10], uvSources[9],  // 4
            uvSources[7], uvSources[11], uvSources[10], // 5
            uvSources[7], uvSources[8], uvSources[11],  // 6

            // 側面
            uvSources[12], uvSources[13], uvSources[14], // 1天
            uvSources[14], uvSources[13], uvSources[19], // 1底

            // 2
            uvSources[15], uvSources[16], uvSources[12], // 2天
            uvSources[12], uvSources[16], uvSources[13], // 2底
            // 3
            uvSources[17], uvSources[18], uvSources[15], // 3天
            uvSources[15], uvSources[18], uvSources[16], // 3底
            // 4
            uvSources[13], uvSources[1], uvSources[19], // 4天
            uvSources[19], uvSources[1], uvSources[21], // 4底
            // 5
            uvSources[16], uvSources[20], uvSources[13], // 5天
            uvSources[13], uvSources[20], uvSources[1],  // 5底
            // 6
            uvSources[18], uvSources[8], uvSources[16], // 6天
            uvSources[16], uvSources[8], uvSources[20], // 6底
            
        };
        mesh.uv = uvs;

        mesh.RecalculateNormals();

        var filter = GetComponent<MeshFilter>();
        filter.sharedMesh = mesh;

        var renderer = GetComponent<MeshRenderer>();
        renderer.material = _mat;

    }

    // Update is called once per frame
    void Update () {
		
	}
}

このコードの見どころは、バッファに追加する頂点情報を六角柱の各面ごとに切り分けられている点です。
つまり、マップ生成を自動化したときに、表示する必要のない面の情報をそぎ落としながら、一つのメッシュとして…

あれ、複数のマテリアルが設定されている時は、メッシュを分けないといけないのかな…
次は、複数マテリアルを利用するメッシュの自動生成について調べていきます。

ちょっと調べた感じ…
なるほど、

Mesh.subMeshCount = 2;             // 2つのSubMeshを格納できるように指定.
Mesh.SetTriangles(triangles0, 0);  // 0番目のSubMeshを格納.
Mesh.SetTriangles(triangles1, 1);  // 1番目のSubMeshを格納.

という感じにして、materials にマテリアルを設定するのか

画像ファイルを複数選択して一括で解像度、サイズを変更する

ブログに画像を貼り付けたり、ゲームや画像処理の入力データとして数百枚の画像ファイルをそろえたりするときに、一括してファイル選択状態から解像度を変更したい時があります。
みなさんも困ったことありますよね?
一枚一枚画像編集ソフトで開いて、編集メニューから解像度指定を行って、別名を付けて保存とか…やってられません!

そこで、選択して、右クリックのコンテキストメニューからリサイズto低解像度で、ドン!と作業が完了するツールが存在しますので使ってみました。

完璧に動作しました。
ありがとうございます!

使ったツールはこちら

www.bricelam.net

ダウンロードしたインストーラを実行すると、すぐに右クリックのコンテキストメニューから Resize Pictures が選べるようになります。
パフォーマンスも極めて高かったです。

忘れないうちに、便利ツールとしてメモします。

マイクロワールドの試作1

今回は、こちらの記事の続きです。
simplestar-tech.hatenablog.com
具体的にマイクロワールドを作っていきます。

世界を構成している要素

構想で登場したものは以下の三つでした

  • ワールド(ブロックによって敷き詰められた世界)
  • ブロック(内部に複数のオブジェクト)
  • エージェント(風ブロックの中を移動する、ブロックや内部のオブジェクトとメッセージのやり取り)

まずはワールドについて構想してきたことを列挙します。

  • 平面充足は正三角形、正三角柱を敷き詰めた3次元空間
  • 三角柱は四元素のいずれかに分類される(火、風、土、水)
  • 火ブロックは面接続されたブロックが土ブロックだった場合、火の耐性に応じてアクションを誘発するブロック間の火隣接メッセージを伝える(受け取ったブロックが燃える、溶ける、高温になる、など)
  • 風ブロックは面接続されたブロックが同じ風ブロックだった場合、風隣接メッセージを伝える(熱、ガス、臭い、メッセージをバケツリレー的に伝播させるなど)
  • 土ブロックは基本的に重力メッセージによって落下したがる、周囲に接着するブロックか、直下に落下しないブロックがあれば、落下はしない。内包しているオブジェクトから周囲オブジェクトへ伝えたいメッセージがあれば伝える。(におい、音、熱など)
  • 水ブロックは基本的に低い場所へ流動したがる、周囲に土ブロックと直下に土ブロックがれば固定され、そのいずれかに風ブロックがあれば、その方向へ流動する。水位という属性があり、隣接する水ブロックの水位が低い方へも流動し、自身の水位を移動した分だけ減らす。

すでにブロックについて書いてきましたが、そのブロックとオブジェクトとメッセージについて動的な部分を詳しく見ていきます。

例えば、水ブロックですけど、水位パラメータは水ブロックが持つのでしょうか?
そのほか、水ブロックは内部に液体オブジェクトを持ち、そのオブジェクトに粘性や熱といったパラメータがあるのでしょうか?
複数や単体のブロックで構成される、海、湖、池、沼、バケツの水、溶岩だまりといった概念や
それを構成している、海水、真水、溶岩、ヘドロといった概念を
ブロックとして持たせるのか、オブジェクトとして持つのか、すべてメッセージとするのか
設計していく必要があります。

エージェントについて考えると

こうしたワールドの一部を切り取って、未来予測ともいえるシミュレーションを行い、アクションと結果の関係から行動の利益を導き出せるようにする必要があります。
ワールドは世界共通の現象だったとしても、エージェントはその観測した空間をブラックボードに一度コピーして、時間軸を操作してシミュレーションを行い、時に途中までのシミュレーションに戻って
何度も選択できるアクションをやり直し、最適な解を導き出す努力ができなくてはいけません。
これを可能にする設計にする必要もあります。

うーん、いざ試作するといっても、また構想に入ってしまいました。

水ブロックは、水としてふるまうための情報を、液体オブジェクトから引き出すということになる感じでしょうか?
その液体オブジェクトには必ず、流動の振る舞いを決定するパラメータをすべて持っていなくてはなりませんよね。
ある液体オブジェクトという基底クラスがあり、そこには液体特有のパラメータ(粘性、熱、揮発性)があるとします、海水、真水、溶岩、ヘドロはそれらを継承して作られたものとすれば、コード量は少なくて済みますね。
また、そうした派生オブジェクトに、メッセージを流し込んだときに、例えば溶岩に海水が面接続したときに、それぞれがリアクションを取ることになり、派生したコードの振る舞いが実行されるという設計です。
溶岩のリアクションは接続されたブロックに奪われた熱の分だけ、凝固へ向かい、ブロックを土に変えて、そのブロックの構成オブジェクトが岩石や金属、鉱石にすげ変わったり、海水のリアクションは受け取った熱の分だけ蒸発して、隣接する空気ブロックへ水蒸気を流したり、自ブロックが空気ブロックに変わって、水位とは異なる構成成分としてミネラルである塩オブジェクトが空気ブロックの中に残り、その成分はその成分量を保持したまま落下する、たい積すると塩の土ブロックになるなど。
さらに、圧力・応力というものが土ブロックにはかかり、最大応力をオーバーすると土ブロックが破断し、それに接着していたブロックが落下可能なら落下するなど

重要なことは、そのワールドを構築する際の基本的なメカニズムを正しく抽象化して、それを抽象クラスとして設計に盛り込むことです。

また、新しい概念が登場してきたので、メモします。
水位に似た、構成割合というものです。
確かに液体が熱せられて蒸発したときに、100%の土ブロックになるのは質量保存に反しすぎですね。
物質オブジェクトについても個体、液体、気体という状態変化をするという、基底の振る舞いというものが見えてきました。

マイクロワールドを動き回るエージェントには、こうした化学要素のシミュレーションも行えてほしいわけで、そのためにはワールドにそうしたシミュレーションが入る必要があります。
また、土ブロックも圧力・応力を与えると、一定の閾値で破壊、破断するという点も、建築において重要になってきます。

エージェントには、作物を育てたり、建築して自然の驚異に立ち向かったり、お金を使った経済活動を行ったり、集団で力を合わせたりしてほしいところだけど
そういった細かい部分は、ワールドの土台が固まってきてからにします。

では、ここまで構造してきたものを少しずつ形にしてみようと思います。

世界を構成している要素の実装

リソースは有限ですので、地上というものがあるのならば、地下深くまで掘り進めると限界深度となる、それ以上掘り進められない岩盤ブロックが現れるものとし
天高く積み上げていっても、一番下の土ブロックが圧壊または溶岩と化して、積み上げたブロックが沈み込み、それ以上高いところには行けないものとします。
いつか、空を飛ぶ機械をエージェントが発明して飛んだとしても、限界高度までたどり着いたら、それ以上上昇できない風ブロックがあり、風ブロック以外が面接続すると破壊されることとします。

これらが示すのは3次元空間でいうところの高さのバッファ長であり、まずはテストのためにすぐに限界が来る狭い空間として、海抜を深度0とするなら、地底に-10、天空に10ブロックとします。
まずは、深度-10に落下も破壊もない土ブロック(岩盤)を敷き詰めます。
世界が巨大な球体とするなら、地平は循環するため、例えばx方向にまっすぐ20進んだら、元のブロックに到達するという循環があるものとします。
なので、地平の果ては存在せず、海が断崖絶壁を滝のように流れ落ちるような果ては存在しないものとします。(エージェントがそうした迷信を信じるのはあり)

これでブロックにはすべて一意のインデックスが振られることになり、隣接するブロックへのインデックスも類推することができるようになります。
例えば、座標 x, y, z が 0, 0, 0 のブロックにとって、0, 0, 1 のブロックは1m上のブロックであると類推でき、同様に直下のブロックは 0, 0, -1 と類推できます。
自身のブロックの直下が風ブロックだったら、液体オブジェクトを与えられる水位だけ水ブロックが落下させるとしたとき、メッセージとしてそうした現象が成り立つようなことが伝播します。
上下は簡単ですが、周囲のブロックはどのようにインデックスが打たれるべきでしょうか?平面充足が三角形であるため、この点は簡易に決めることができません。
一辺を1unitとする正三角形なら、横方向はブロックの重心を結ぶ長さも1unitになります、縦方向は{ \sqrt{3} }unitになりますね。
これでセーブデータなどの場所の座標から、特定のブロックへのアクセスが可能ということになります。

重要な認識としては、ブロックの位置は不変で、面接続しているブロックも切り替わることはありません。
移動するのはあくまでブロックを構成しているオブジェクトであって、それらが様々なふるまいによって気化して周囲に伝播したり、支えを失って落下したり、融解して周囲に伝播したりします。
ブロックには具体的なインデックスが振られていなくても、ブロック同士で互いに面接続しているブロックへのアドレスを保持していれば、アクセスに支障はありません。

ブロックには面接続している、合計五つのブロックへのアドレスを保持する必要があることがわかってきました。
ここまでのことを具体化すると、次のクラスが記述されました。

public class Block
{
    int _position_east;
    int _position_north;
    int _position_depth;

    public Block _northEastBlock;
    public Block _southBlock;
    public Block _northWestBlock;
    public Block _upBlock;
    public Block _downBlock;
}

public class SoilBlock : Block
{

}

public class WaterBlock : Block
{

}

public class AirBlock : Block
{

}

public class FireBlock : Block
{

}

これをワールド depth 20 x east 20 x north 20 blocks の 8,000 blocks の世界に配置するように
まずは、ハードディスクに初期化した世界情報を記述し、これを読み込んでメモリ上に再構成するサンプルを作り
そのメモリ上のワールド情報を Unity でビジュアライズする部分を書いてみましょう。

うーん、その後時間をおいて考えてみたのですが
すべてのブロックを大気ブロックとして、水位、密度、割合、圧力、熱という表現にオブジェクトがその割合だけ存在するというのはどうでしょうか?
つまり、空気さえもオブジェクトとなり、ブロックを何パーセント占めているかという表現になります。
例えば、海水ブロックに大気が接していたら、徐々に大気に水蒸気が伝播していき、その分海水が入っていたブロックの水位が蒸発した分だけ下がり続け、上昇した塩分濃度がほかの接している海水ブロックに伝播して同じ塩分濃度になるといった具合です。
そうすると、常にブロックは位置も変わらず、種類も変わらず、含んでいるオブジェクトの伝播の振る舞いに従って自身や周囲のブロックの構成成分を変容させていくだけとなります。

しかし、Minecraft と Unity で作ってみたっていう動画を探してみたのですが、例がわんさか出てきますね。
オブジェクト数が数千、数万と出ていそうなビューなのにフレームレートがあまり落ちないような動画とかもあるし、いったいどういうマシンで遊んでいるんだろう?
それと、みんなどれだけ Unity で Minecraft 作りたいんだって感じです。
AIを作りたいかは別として、同じようなことを考える人はいっぱいいるんですね。

また、記事が長くなってきたので、次の AI カテゴリーの記事へ続きを書きます。