simplestarの技術ブログ

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

AIに身体性を与えるためのマイクロワールドの構築5(構築といいつつまた構想)

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

ビジュアライゼーションのパフォーマンス確認も一区切りのところまで来ました。
なんとか数百万ブロックまでなら30FPS出そうです。
ということで、モデルの方を詰め始めましょう!

これまで生きてきて、心から尊敬する人物が二人いるのですが、その一人の方に相談したところ
Minecraft のようにチャンク構造で作るのが良いのではないかとのことでした。
世界を2Dの平面として定義して、その平面の一部領域を切り取り、チャンクを定義します。

世界は seed 値によって決まり、無限に広がる世界のほんの一部だけを構築して、チャンク単位の読み込みを行いビジュアライズします。
境界ブロックは隣接チャンクのブロックを見に行くようにし、プレイヤーが移動して遠くなったチャンクはパージされるようにします。
ブロックの情報はメモリ容量について極めて重要な値なのですが、ここは 32bit など、とても小さいデータ構造にするのが大切とのことです。
最初に考えるのは、そのうち 16bit がブロックの種類、残りの 16bit は予約領域というものでした。

メモリ容量について簡単に計算してみましょう。
16x16x256 x 4[byte] = 262,144 [byte]
つまり、1チャンク 0.2 [MB] もメモリを消費することになります。

昨今のPCゲーム実行環境の最低メモリサイズは 3 [GB] ほどなので、最大でも 11,444 チャンクが限度といったところでしょうか?(一辺107チャンク四方程度)
シーン内の最大ブロック数は 750,000,000 (7億5千万) 程度ということになります。

もし1ブロックあたりのメモリサイズを 4byte よりも広げた場合、これらの数字は 1/2, 1/4, 1/16 とどんどん小さくなることになります。
なるべく広大な世界であることもプレイヤーを楽しませる重要な要素なので、1ブロックを占める情報は小さくしましょう。

ここで重要になってくるのが、ブロック同士の相互作用をどのようなアルゴリズムで実現するかというものです。
以前、水が蒸発していき、周囲のブロックに水蒸気を含ませ、やがて雲になって雨が降るといった自然現象を作ろうと構想したことがあります。

もし、空気ブロックに水蒸気が湿度50%ほど含まれているとしたい場合、ブロックには構成要素と割合の情報を記録したいところです。
これをどうやって4byteのデータに格納するのか、また、その水蒸気の情報をどのようにして拡散するのかを考えていきましょう。

まず、ブロックが何のブロックであるのかを識別する ID が必要です。
Minecraft では初期の頃 1byte をこれに割り当てたそうですが、その後どんどんブロックの種類が増えていき、1byte では足りなくなったそうです。
ここは 2byte にして、最大でブロックは 65,536 種類とします。

そのうちの一つ、0 を空気ブロックとした場合、構成要素を 2byte でどのように表現するか考えてみましょう。
割合を 16段階とするのはどうでしょうか? 4bit で表現できます。
空気を構成している要素を最大 16種とするのはどうでしょうか? 4bit で表現できます。
つまり 8bit (1byte) でメイン要素の種類と割合が書き込めます。

例えば、水ブロックであれば、メインの水の割合が 100% あり、その他不純物が無いという表現が可能となります。
もし何かを水の中に落としたとしても、落としたものが何ブロックに存在するかという情報にすることで、ブロック情報を拡張することなく解決します。

ただ、メインの要素をわざわざ割り当てるのももったいないですので、サブの不純物を 2種類までブロックに割合とともに書き込めるようにしてはどうかと考えます。
例えば鉄が多く含まれている岩ブロックがあるとします。そのブロックの 2byte はまず岩ブロックであることを定義し、メインの岩要素の割合を 2種類のサブ要素の割合の余りとする手法です。
不純物としてまずサブ要素の代表の鉄が1/16の割合で含まれているとし、サブが鉄であることに 4bit, 割合が1/16 であることに 4bit、合計 1byte でサブ要素がわかり
第二のサブ要素として、水が 2/16 の割合で含まれているとします。こちらも 1byte の情報が入り、岩のメイン要素は 13/16 の割合で含まれたブロックとします。

なんだか、このような構想は以前行っていたような気がしてきました。
ここで以前の構想の話を思い出してみます。
simplestar-tech.hatenablog.com

「すべてのブロックを大気ブロックとして、水位、密度、割合、圧力、熱という表現にオブジェクトがその割合だけ存在するというのはどうでしょうか?」

なるほど、圧力(応力)と熱(温度)ですか。
もし水ブロックがたい積していた場合、下の方の水の圧力が高まり、もし圧力の低いブロックがあるならば、圧が低い方へ移動するようにして、かなり単純なアルゴリズムで流体の動き出す方向が決められます。
熱もまた、隣接するものを凍らせたり、溶かしたりと、例えば雪原の中に溶岩ブロックがあれば、周囲の土ブロックは熱くなり、雪がそこだけ積もらないで、水や、水たまりが温泉になるなど
ほか、日中の太陽が当たると、一様に地面が温まり、地表の大気温度が上昇したり、雪が溶けだすなどの動きが、温度を見るだけで簡単に定義できます。

ということで、仮決めします。
12bit はブロックの種類(4096種)、4bit はそのブロックの種類における不純物の種類(16種)、4bit はその不純物の割合(16段階)、6bit は圧力(64段階)、6bitは温度(64段階) です。
合計 32bit の int 型として、16x16x256 の四角柱のチャンクを1チャンクとして合計 65536 要素の int 配列をチャンクとします。
配列の実装は3次元配列として、幅(x)、奥行き(z)、高さ(y) ですぐにアクセスできるものとし、ブロックを参照するときはチャンクインデックスとともに、チャンクをまたぐアクセスも可能とします。
チャンクもまた2次元配列とします。
チャンクは幅と奥行きそれぞれ 256 x 256 のチャンクがあるとし、地球一周 4,096 ブロックとします。
チャンク一つあたり 262,144 byte なので、地球丸ごと一つ 17,179,869,184 [byte] (17.2GB) ということになります。

4,096 ブロックで地球一周、1ブロックの幅は 1.732 unit [m] なので、地球一周 7094.272 [m] ということになります。
地球の直径は 2258.2 [m] です。(実際の 13,000,000 [m] と比べると、577倍も小さい惑星となります。)
Unity のカメラの最大深度は 1000 unit [m] なので、フォグの先の隠れた場所にしか循環先は現れないため、直線的に絶対にもう一人の自分を視認することができないということです。

参考イメージ
f:id:simplestar_tech:20171030085617j:plain
4096個先のブロックは 1pixel 以下で実際にカメラには 577ブロック程度しか映らない。

これならなんとかゲーム内のエージェントにとっては矛盾なくマイクロワールドとして機能しそうです。
それでは世界創造のコーディングを行っていきましょう!(次回以降)

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

AIに身体性を与えるためのマイクロワールドの構築4です。
これといって技術的な要素はありませんので、単なる進捗報告とします。

高さに応じてマテリアルを変えてみます。
f:id:simplestar_tech:20171018002910j:plain

UV座標も正しく設定してあげます。
f:id:simplestar_tech:20171018002939j:plain

縦軸についても、分割してみました。
f:id:simplestar_tech:20171023223111j:plain

頂点数を 21312 から 5280 まで減らしました。

f:id:simplestar_tech:20171024080636j:plain

f:id:simplestar_tech:20171024080700j:plain

f:id:simplestar_tech:20171024001455j:plain

テクスチャを作って貼りました。
f:id:simplestar_tech:20171024075933j:plain

作ったテクスチャはこちら
f:id:simplestar_tech:20171024080012p:plain

素材はこちらから取ってきました。
free-paper-texture.com

次の記事へ進みます!

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

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

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

従来型のこの手法は、収集した情報から意思決定をする処理を、すべて人間がデザインしなければなりません。
オープンワールドにおける状況や選択肢は膨大な組み合わせとなりますので、当然人間にはデザインしきれない量となります。
するとゲーム内のキャラクタ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 にマテリアルを設定するのか