【M5ATOM Matrix】でライフゲームをやってみた
note.comを始めたきっかけは、よりよい盆栽ライフを模索する様子をお伝えしたいと思ったからです。
最初のチャレンジ、自動潅水装置の製作を進めるうち、最新鋭のマイクロコントローラーM5ATOM Matrixとの出会いがありました。
M5ATOM Matrixには5*5のカラーLEDがあります。この機能について楽しみながら習得したい。まずはテストプログラムを書いてみることからと思い、LEDの表示を試しています。
まずは、動画をご覧ください。
[グライダー]
[ランダム]
[ブリンカー]
[ひきがえる]
[ビーコン]
[時計]
この小さな筐体の中で動くグライダーがとても愛おしい。
開発環境のArduino IDEにて、M5ATOM Matrix用のサンプルを一通り確認し、LEDの表示は次の命令で行うことが分かりました。
M5.dis.drawpix(pos, grb);
第1引数の「pos」ですが、左上から横方向に並んでいています。行と列で指定したくなります。
#define P(col, row) (((row) * 5) + (col))
:
M5.dis.drawpix(P(0, 1), grb);
こんな感じでしょうか。
第2引数の「grb」ですが、色を表していて、G(reen)、R(ed)、B(lue)1バイトずつの指定です。
これも、RGB(r, g, b)で指定したくなります。
#define P(col, row) (((row) * 5) + (col))
#define RGB(r, g, b) (((g) << 16) + ((r) << 8) + (b))
:
M5.dis.drawpix(P(0, 1), RGB(0xf0, 0x00, 0x20));
でよいと思います。
プリプロセッサに仕事をさせ、プログラムのスタックを消費しない古い書き方をしたつもりですがいかがしょうか。私はこのやり方が安心できます。
ここまでで、5x5のフルカラースクリーンを手に入れました。
これを使って何か面白いものが作れないかと考えたとき、ライフゲームの実装を思いつきました。
ライフゲームは、デフォルメされた生態系のシミュレーションで、細胞の繁殖するようすを簡単なルールを用いて表現します。
細胞が次のターンで生存する条件は、周囲の細胞の数が3か4のとき、
何もない場所で細胞が発生する条件は、周囲の細胞の数が3のとき。
それ以外では死滅する。
これだけの条件で予想のつかない不思議なふるまいをするため、表現の貧弱なキャラクターベースのPC時代よりデモンストレーションソフトとしてよく作成されていました。
ネットで勉強できる材料を探したところ「ゲヱム道館 Gamedokan」さんのチャンネルでライフゲームのプログラムを紹介されておりました。この動画とてもよくできていて、流し見で見ていたにもかかわらず、内容が頭の中にスルスル入り、次の日、すきま時間でサクサク作れてしまいました。
ゲヱム道館さんのプログラミングで、とても感動した所は、いわゆるマップデータの保持部分で、こんな感じでした。
#define CELL_WIDTH 7
#define CELL_HEIGHT 7
int cells[2][CELL_HEIGHT][CELL_WIDTH];
これにより、仮想の7*7のマップを作り、そこに細胞の生存状況を書き込みます。
それが、2枚用意されていて、変数layerで管理されています。(layer ^ 1)で次のターンの情報を書き換えながら進めます。
※「^」は排他的論理和(xor)演算です。
layerが0のとき、(layer ^ 1)は1、
layerが1のとき、(layer ^ 1)は0を表わします。
Arduino IDEでのプログラムの場合、main()はライブラリ側で管理されているため、
起動時に一度だけ呼び出されるsetup()関数と定期的に呼び出されるloop()関数を用いてプログラムを作成します。
void setup() {
M5.begin(true, false, true);
delay(50);
pixColor = colors[random(12)];
setGlider();
M5.dis.clear();
}
こんな感じです。
M5.begin(true, false, true);
は初期化命令でしょうか。おまじないとして呼び出しました。
pixColor = colors[random(12)];
色の設定を行います。
好みの色を12個colors配列にセットしています。
そのうちの一つをランダムに取り出します。
setGlider();
で、グライダーを初期盤面に配置しました。
void setGlider() {
cells[0][1][0] = 1;
cells[0][2][1] = 1;
cells[0][0][2] = 1;
cells[0][1][2] = 1;
cells[0][2][2] = 1;
}
このような配置になります。
ライフゲームでは有名なパターンに名前がついています。
遷移状態は以下のとおりです。
グライダーはこんな感じの変化を繰り返します。
4サイクルでx+1、y+1の位置へ移動します。
M5.dis.clear();
LEDを消します。
以上で、初期化は終わりです。
次に、メインループについてです。
void loop() {
static int mode = 0;
if (M5.Btn.wasPressed()) {
resetCells();
layer = 0;
pixColor = colors[random(12)];
switch (mode) {
case 0: setRandom(); break;
case 1: setBlinker(); break;
case 2: setToad(); break;
case 3: setBeacon(); break;
case 4: setTokei(); break;
case 5: setGlider(); break;
}
if (++mode > 5) mode = 0;
}
progress();
dispField();
delay(200);
M5.update();
}
この書き方、あんまりしないかな。
ここはちょっとしたこだわりポイントなんです。
static int mode = 0;
関数内でstaticを付けて変数宣言すると、初期化可能でスコープは関数内に留めることができます。
組み込み系のサンプルには、グローバル変数をたくさん使用する傾向が強いのですが、BASICでスコープの概念がない状態でプログラムをしていた時代から、Cになって構造化できるようになった時の喜びを思い出し、適切に変数宣言をしています。
if (M5.Btn.wasPressed()) {
もし、M5Atomのボタンが押されていたら、カッコ内の処理を行います。
if (M5.Btn.wasPressed()) {
resetCells();
layer = 0;
pixColor = colors[random(12)];
switch (mode) {
case 0: setRandom(); break;
case 1: setBlinker(); break;
case 2: setToad(); break;
case 3: setBeacon(); break;
case 4: setTokei(); break;
case 5: setGlider(); break;
}
if (++mode > 5) mode = 0;
}
ボタンが押されるたびに、
仮想の7*7のマップをリセットし、レイヤを0にし、色を選びます。
Wikiで見つけた面白そうで、そして、7*7の極小空間でも動作するパターンをセットしています。
グライダー:
ランダム:
発生確率を30%にしています。
ブリンカー:
ひきがえる:
ビーコン:
時計:
次に、メイン中のメインの処理
progress();
dispField();
delay(200);
M5.update();
世代を進めて、画面を表示し、0.2秒待ち、システムに制御を渡しています。
void progress() {
int res = 0;
for (int r = 0; r < CELL_HEIGHT; r++) {
for (int c = 0; c < CELL_WIDTH; c++) {
int n = getNeighborN(c, r);
if (cells[layer][c][r] == 1)
res = (n == 2 || n == 3);
else
res = (n == 3);
cells[(layer ^ 1)][c][r] = res;
}
}
layer ^= 1;
}
ゲヱム道館さんのグログラミングで、魅せられた関数がこれです。
世代を進める処理を行っています。
全てのセルについて、周囲の細胞の数を数え、
今、生存している場合、周囲の細胞の数が3か4のとき、生存、
存在していないとき、周囲の細胞の数が3のとき、発生、
それ以外は死滅する。
その結果を、次のレイヤに書き込みます。
全てのセルの調査が終わったら、レイヤを変えます。
周囲の細胞の数を数える関数です。
int getNeighborN(int c, int r) {
int n = 0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
if (x == 0 && y == 0)
continue;
n += cells[layer][(CELL_WIDTH + c + x) % CELL_WIDTH][(CELL_HEIGHT + r + y) % CELL_HEIGHT];
}
}
return n;
}
引数で、調査するセルの位置を渡し、そこから現在地以外の-1~+1の範囲で8か所について、生存状態を調査し、その数をカウントします。
マップの回り込みを考慮して、正しく配列内をポイントできるよう配慮しています。
最後に表示部分です。
void dispField()
{
for (int r = 0; r < 5; r++) {
for (int c = 0; c < 5; c++) {
M5.dis.drawpix(P(c, r), pixColor * cells[layer][c][r]);
Serial.printf("%d", cells[layer][c][r]);
}
Serial.printf("\r\n");
}
Serial.printf("-----\r\n");
}
表示はセルの状態が1か0で表現されているので、色を掛け合わせた結果を用いています。
ということで、せっかく作ったRGB()は使いませんでした。
どのようなパターン変化をしたかトレースしたくて、結果は同時にシリアルモニタに飛ぶようにしています。
結局、ほぼ全部の説明をしました。
ただ、勝手に紹介しているだけで申し訳ないのですが、ゲヱム道館さんのプログラミング動画は、本当にすごかった。
一発撮りでと書かれていたけど、よどみなく、わかりやすく説明されているでの、これからも、見せて頂こうと思いました。
スピンアウトの記事ながら、書いてしまいました。
最後まで読んでくださってありがとうございます。
そして、今後製作を進める盆栽のための自動潅水装置SALZ3の行く末も、あたたかい目で見守っていただければ、幸いです。
最後に、今回のソースコードを掲載します。
// M5AtomLG bbd
#include "M5Atom.h"
#define P(col, row) (((row) * 5) + (col))
#define RGB(r, g, b) (((g) << 16) + ((r) << 8) + (b))
long colors[] = {
0x6432f0, 0xC832f0, 0xf032b4, 0xf03250,
0xf07832, 0xf0dc32, 0xa0f032, 0x3cf032,
0x32f08c, 0x32f0f0, 0x328cf0, 0xf08c32
};
#define CELL_WIDTH 7
#define CELL_HEIGHT 7
int cells[2][CELL_WIDTH][CELL_HEIGHT];
int layer = 0; // 0 or 1
long pixColor;
void resetCells() {
for (int r = 0; r < CELL_HEIGHT; r++) {
for (int c = 0; c < CELL_WIDTH; c++) {
cells[0][c][r] = 0;
cells[1][c][r] = 0;
}
}
}
void setGlider() {
cells[0][1][0] = 1;
cells[0][2][1] = 1;
cells[0][0][2] = 1;
cells[0][1][2] = 1;
cells[0][2][2] = 1;
}
void setRandom() {
for (int r = 0; r < CELL_HEIGHT; r++) {
for (int c = 0; c < CELL_WIDTH; c++) {
cells[0][c][r] = (random(10) > 6);
}
}
}
void setBlinker() {
cells[0][1][2] = 1;
cells[0][2][2] = 1;
cells[0][3][2] = 1;
}
void setToad() {
cells[0][2][1] = 1;
cells[0][3][1] = 1;
cells[0][1][2] = 1;
cells[0][4][3] = 1;
cells[0][2][4] = 1;
cells[0][3][4] = 1;
}
void setBeacon() {
cells[0][1][1] = 1;
cells[0][2][1] = 1;
cells[0][1][2] = 1;
cells[0][4][3] = 1;
cells[0][3][4] = 1;
cells[0][4][4] = 1;
}
void setTokei() {
cells[0][3][1] = 1;
cells[0][1][2] = 1;
cells[0][2][2] = 1;
cells[0][3][3] = 1;
cells[0][4][3] = 1;
cells[0][2][4] = 1;
}
int getNeighborN(int c, int r) {
int n = 0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
if (x == 0 && y == 0)
continue;
n += cells[layer][(CELL_WIDTH + c + x) % CELL_WIDTH][(CELL_HEIGHT + r + y) % CELL_HEIGHT];
}
}
return n;
}
void dispField()
{
for (int r = 0; r < 5; r++) {
for (int c = 0; c < 5; c++) {
M5.dis.drawpix(P(c, r), pixColor * cells[layer][c][r]);
Serial.printf("%d", cells[layer][c][r]);
}
Serial.printf("\r\n");
}
Serial.printf("-----\r\n");
}
void progress() {
int res = 0;
for (int r = 0; r < CELL_HEIGHT; r++) {
for (int c = 0; c < CELL_WIDTH; c++) {
int n = getNeighborN(c, r);
if (cells[layer][c][r] == 1)
res = (n == 2 || n == 3);
else
res = (n == 3);
cells[(layer ^ 1)][c][r] = res;
}
}
layer ^= 1;
}
void setup() {
M5.begin(true, false, true);
delay(50);
pixColor = colors[random(12)];
setGlider();
M5.dis.clear();
}
void loop() {
static int mode = 0;
if (M5.Btn.wasPressed()) {
resetCells();
layer = 0;
pixColor = colors[random(12)];
switch (mode) {
case 0: setRandom(); break;
case 1: setBlinker(); break;
case 2: setToad(); break;
case 3: setBeacon(); break;
case 4: setTokei(); break;
case 5: setGlider(); break;
}
if (++mode > 5) mode = 0;
}
progress();
dispField();
delay(200);
M5.update();
}
#電子工作 #ライフゲーム #M5Atom #LED #Arduino #ESP32 #C言語 #プログラミング