
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
クマ〜〜〜〜

マイケル
今日は気晴らしにUnityで簡単なライフゲームを作ってみるよ!

エレキベア
ライフゲームって何クマ?

マイケル
ライフゲームとは、
生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム だ・・・!!
生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム だ・・・!!

エレキベア
せ、生命の誕生・・・
気晴らしに作ったライフゲームを壊す pic.twitter.com/GSAItc6Aue
— マイケル (@masarito617) April 3, 2022

マイケル
ゲームといってもこんな感じで基本的に眺めて楽しむだけで、
簡単なルールで面白い変化をすることから有名になったんだ!
簡単なルールで面白い変化をすることから有名になったんだ!

エレキベア
(なぜ壊したクマ・・・)
なるほどクマ、なんかみたことある気がするクマ
なるほどクマ、なんかみたことある気がするクマ

マイケル
今回作ったサンプルプロジェクトはGitHubにあげてるので
よかったら触ってみてくださいね!
よかったら触ってみてくださいね!
GitHub – masarito617/unity-life-game-sample

エレキベア
やったるクマ〜〜〜
ライフゲームとは

マイケル
それでは改めてライフゲームとは、
以下のように定義されているシミュレーションゲームだ!
以下のように定義されているシミュレーションゲームだ!
ライフゲームとは、生命の誕生、進化、淘汰などのプロセスを
簡易的なモデルで再現したシミュレーションゲームである
ライフゲーム – Wikipedia
参考文献:
ライフゲーム – Wikipedia

エレキベア
(Wikiの引用だったクマか・・・)

マイケル
セル・オートマトンという、隣接するセルの状態を元に、時間的に状態を変化させるモデルの一種で、下記のルールに従うことで再現できるんだ!
[基本ルール]
誕生:死亡セルに隣接する生存セルが3つあれば、次の世代が誕生する。
生存:生存セルに隣接する生存セルが2つか3つならば、次の世代でも生存する。
過疎:生存セルに隣接する生存セルが1つ以下ならば、過疎により死滅する。
過密:生存セルに隣接する生存セルが4つ以上ならば、過密により死滅する。

エレキベア
意外とシンプルなルールクマね

マイケル
多すぎても少なすぎても生存できないという、生態学の側面も取り入れられていて、
この4つのルールだけで非常に面白い動きになるんだ!
有名なパターンとしては下記のようなものがあるよ!
この4つのルールだけで非常に面白い動きになるんだ!
有名なパターンとしては下記のようなものがあるよ!



マイケル
ヒキガエルやグライダー
といった単純なパターンから・・・
といった単純なパターンから・・・



マイケル
ペンタデカスロンや銀河
といった複雑なものまで数多のパターンが発見されているよ!
といった複雑なものまで数多のパターンが発見されているよ!

エレキベア
ペンタデカスロン・・・??
でも見てて面白いしすごいクマ〜〜〜〜!!
でも見てて面白いしすごいクマ〜〜〜〜!!

マイケル
ここまで聞いたらすぐ作れそうだしやってみたくなるよね!
早速Unityで作ってみよう!
早速Unityで作ってみよう!

エレキベア
楽しみクマ〜〜〜!!
ライフゲームの実装

マイケル
GitHubのAssets配下にある、LifeGame.cs がライフゲームの処理です!
カメラを下記のように2D用に設定している状態でゲームオブジェクトにアタッチすると動くようになっています。
カメラを下記のように2D用に設定している状態でゲームオブジェクトにアタッチすると動くようになっています。
GitHub – masarito617/unity-life-game-sample

セルの生成

マイケル
まずセルの生成についてですが、
今回は簡単に「Cubeオブジェクトを1つのセル」として作ってみました!
そのためセルの状態を示すCellStatusとその配列、そして描画更新用にMeshRendererの配列を変数として用意しています。
今回は簡単に「Cubeオブジェクトを1つのセル」として作ってみました!
そのためセルの状態を示すCellStatusとその配列、そして描画更新用にMeshRendererの配列を変数として用意しています。
/// <summary>
/// セルの状態
/// </summary>
private enum CellStatus
{
Dead = 0, // 死亡
Life = 1, // 生存
}
/// <summary>
/// セル情報
/// </summary>
private CellStatus[] _cells; // セル配列
private MeshRenderer[] _cellMeshRenderers; // セルが持つMeshRenderer配列
private float _cellScale = 1.0f; // セルの大きさ
↑セルの状態を示す変数
マイケル
Awake処理でセルを生成して、用意した配列に格納します。
生存状態はマテリアルを2種類用意して切り替えることで表現するようにしています。
生存状態はマテリアルを2種類用意して切り替えることで表現するようにしています。
/// <summary>
/// 生成するセル数
/// </summary>
[SerializeField] private int widthCount = 32;
[SerializeField] private int heightCount = 32;
/// <summary>
/// カメラに収まる基準セル数
/// </summary>
private const int BaseWidthCount = 16;
private const int BaseHeightCount = 16;
/// <summary>
/// マテリアル
/// </summary>
private Material _lifeMaterial;
private Material _deadMaterial;
private void Awake()
{
// マテリアル生成
_lifeMaterial = new Material(Shader.Find("Unlit/Color"));
_lifeMaterial.color = Color.white;
_deadMaterial = new Material(Shader.Find("Unlit/Color"));
_deadMaterial.color = Color.black;
// カメラに収まるようスケールを調整
_cellScale = Mathf.Min((float) BaseWidthCount / widthCount, (float) BaseHeightCount / heightCount);
// セル生成
_cells = new CellStatus[widthCount * heightCount];
_cellMeshRenderers = new MeshRenderer[widthCount * heightCount];
for (var i = 0; i < widthCount * heightCount; i++)
{
var x = i % widthCount;
var y = i / widthCount;
// セルをCubeオブジェクトとして生成
var cell = GameObject.CreatePrimitive(PrimitiveType.Cube);
cell.transform.parent = gameObject.transform;
cell.transform.localScale = _cellScale * Vector3.one;
cell.transform.localPosition = _cellScale * new Vector3(
x - (widthCount / 2.0f - _cellScale / 2.0f),
y - (heightCount / 2.0f - _cellScale / 2.0f),
0);
// 初回は死亡している状態で設定
_cells[i] = CellStatus.Dead;
// MeshRendererも保持しておく
var meshRenderer = cell.gameObject.GetComponent<MeshRenderer>();
meshRenderer.sharedMaterial = _deadMaterial;
_cellMeshRenderers[i] = meshRenderer;
}
}

マイケル
カメラに映るようにセルの大きさを調整するため、BaseWidthCount、BaseHeightCount定数も用意しています。
scale1.0のセルを16×16でカメラを設定していたためですね・・・。
scale1.0のセルを16×16でカメラを設定していたためですね・・・。

マイケル
セルの生成についてはこれで完了です!!

エレキベア
準備完了クマね
世代の更新処理

マイケル
次に、先ほど説明したライフゲームのルールを実装してみます。
次の世代に更新する関数を用意して、判定分を記述しましょう!!
次の世代に更新する関数を用意して、判定分を記述しましょう!!
/// <summary>
/// 次の世代
/// </summary>
public void NextLife()
{
// セル更新処理
var nextCells = new CellStatus[widthCount * heightCount];
for (var i = 0; i < widthCount * heightCount; i++)
{
var x = i % widthCount;
var y = i / widthCount;
// 隣接するセルのindexを取得
var xl = x == 0 ? widthCount - 1 : x - 1; // 左
var xr = x == widthCount - 1 ? 0 : x + 1; // 右
var yt = y == 0 ? heightCount - 1 : y - 1; // 上
var yb = y == heightCount - 1 ? 0 : y + 1; // 下
// 隣接するセルの生存数をカウント
var lifeCount = 0;
if (IsLifeCell(xl + yt * widthCount)) lifeCount++; // 左上
if (IsLifeCell(x + yt * widthCount)) lifeCount++; // 上
if (IsLifeCell(xr + yt * widthCount)) lifeCount++; // 右上
if (IsLifeCell(xl + y * widthCount)) lifeCount++; // 左
if (IsLifeCell(xr + y * widthCount)) lifeCount++; // 右
if (IsLifeCell(xl + yb * widthCount)) lifeCount++; // 左下
if (IsLifeCell(x + yb * widthCount)) lifeCount++; // 下
if (IsLifeCell(xr + yb * widthCount)) lifeCount++; // 右下
// 生存判定
var isLife = false;
if (!IsLifeCell(i) && lifeCount == 3)
{
// 死亡セルに隣接せる生存セルが3つあれば誕生
isLife= true;
}
else if (IsLifeCell(i) && (lifeCount == 3 || lifeCount == 2))
{
// 生存セルに隣接する生存セルが2つか3つならば次の世代でも生存
isLife= true;
}
nextCells[i] = isLife ? CellStatus.Life : CellStatus.Dead;
}
// 次の世帯のセルに更新
_cells = nextCells;
// 再描画
UpdateDrawMesh();
}
/// <summary>
/// 生存セルか?
/// </summary>
private bool IsLifeCell(int index)
{
return _cells[index] == CellStatus.Life;
}
/// <summary>
/// メッシュ再描画
/// </summary>
private void UpdateDrawMesh()
{
for (var i = 0; i < widthCount * heightCount; i++)
{
_cellMeshRenderers[i].sharedMaterial = _cells[i] == CellStatus.Life ? _lifeMaterial : _deadMaterial;
}
}

マイケル
処理内容としては、隣接するセルの生存数をカウントして
生存or死亡を判定、描画更新しているだけですね!
生存or死亡を判定、描画更新しているだけですね!

エレキベア
簡単な判定クマね
ボタン押下時の処理

マイケル
あとはこれらを呼び出す処理を記述するのみ!
今回は検証しやすいように、各ボタン押下で処理を走らせるようにしています。
今回は検証しやすいように、各ボタン押下で処理を走らせるようにしています。

マイケル
生存セルの設定と破棄は下記の処理になります!
生存セルの設定についてはとりあえず乱数で設定するようにしました!
生存セルの設定についてはとりあえず乱数で設定するようにしました!
/// <summary>
/// セルのリセット
/// </summary>
public void ResetCell()
{
// セルをクリア
_cells = new CellStatus[widthCount * heightCount];
// 再描画
UpdateDrawMesh();
}
/// <summary>
/// セルをランダムに生成する
/// </summary>
public void GenerateRandomCell()
{
// 一定確率で命を与える
for (var i = 0; i < widthCount * heightCount; i++)
{
_cells[i] = Random.Range(0, 100) < 12 ? CellStatus.Life : CellStatus.Dead;
}
// 再描画
UpdateDrawMesh();
}
↑生存セルの設定、破棄
マイケル
セルの設定が完了したら、一定間隔で次の世代更新処理を呼び出しましょう!
コルーチンを使ってループ処理を記述します。
コルーチンを使ってループ処理を記述します。
/// <summary>
/// ループ中か?
/// </summary>
private bool _isLoop = false;
/// <summary>
/// ループ開始
/// </summary>
public void LifeLoopStart()
{
if (_isLoop) return;
StartCoroutine(LifeLoopCoroutine());
}
/// <summary>
/// ループ停止
/// </summary>
public void LifeLoopStop()
{
_isLoop = false;
}
/// <summary>
/// ループコルーチン
/// </summary>
/// <returns></returns>
private IEnumerator LifeLoopCoroutine()
{
// ループ停止されるまで一定間隔で次の世帯を呼び出す
_isLoop = true;
while (_isLoop)
{
yield return new WaitForSeconds(0.1f);
NextLife();
}
}
↑世代更新のループ処理
マイケル
以上でライフゲームが動作するようになっているはずです!!


エレキベア
やったクマ〜〜〜〜!!!
おわりに

マイケル
というわけで今回はUnityでのライフゲーム実装でした!
どうだったかな??
どうだったかな??

エレキベア
こんな単純なルールで面白い動きをするなんて
マジでびっくりクマ
マジでびっくりクマ

マイケル
ライフゲームに関しては書籍も出ているみたいだから
これも読んでみたら面白そうだね!
これも読んでみたら面白そうだね!

マイケル
それでは最後に繁殖型と呼ばれる複雑なパターンも作ったのでお見せしましょう・・・!


エレキベア
グライダーが増え続けてるクマ・・・
すげえクマ・・・
すげえクマ・・・

マイケル
みなさんも是非自分の手で作って遊んでみてください!!
それでは今日はこの辺で!アデュー!!
それでは今日はこの辺で!アデュー!!

エレキベア
クマ〜〜〜〜〜〜
【Unity】簡単なライフゲームを作ってみる 〜完〜
コメント