マトリックスLEDを使ったヘビゲームの制作
はじめに
今回の記事はAizu Advent Calendar 2021の2日目の記事です!
急遽、書くことを決めたため、今回の制作自体は去年のモノになります。
ヘビゲームとは
伸長するヘビを操作して、エサを食べ続けることがゲームの目的である。画面を全て埋め尽くすとゲームクリアとなるものと、エサを食べて大きくなるだけのものがある。エサではなく動いた軌跡がそのまま残り、なおかつランダムに障害物が置かれ、それを回避する目的のゲームもある。プレイヤーはヘビを操作し、その頭が壁あるいは自身の身体にぶつからないようにしながら、ランダムで出現するエサを回収する。エサを回収するたびにヘビの身体は1マスずつ長くなっていく。ヘビは静止することができず、常に動き回っているため、エサを回収すればするほど自身の身体を回避することが難しくなる。(Wikipedia より)
とのことです。
GooglがWebで公開しているヘビゲームもあるので是非遊んでみてください。
今回はこれを、マトリックスLEDをゲーム画面にしてスマホから操作出来るように、ESP32を用いて実装しようと思います。
実装手順1 ゲームの作成
まず最初に、ゲーム部分をPC内で動くようにクラスにして、作成します。
Siv3Dを利用して実装します。
main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.4.3
# include"Game.h"
void Main()
{
// 背景を水色にする
bool flg = false;
Scene::SetBackground(ColorF(0.8, 0.9, 1.0));
// ヘビゲームの初期化
SnakeGame game(32,16);
Window::Resize(Size(640,320));
unsigned long long t = Time::GetMillisec();
while (System::Update())
{
// 右キーを押すと、ゲームスタート
if (!flg&&KeyRight.down()) {
flg = true;
}
if (!flg)continue;
// 押されたキーを記憶する
if (KeyRight.down()) {
game.Chg_direction(RIGHT);
}
if (KeyLeft.down()) {
game.Chg_direction(LEFT);
}
if (KeyUp.down()) {
game.Chg_direction(UP);
}
if (KeyDown.down()) {
game.Chg_direction(DOWN);
}
// 150msごとにゲームを進行する
if (Time::GetMillisec() - t >= 150) {
t = Time::GetMillisec();
if (game.update() == OVER)break;
}
// ただの表示
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 32; x++) {
Rect(x * 640/32, y * 320/16, 640 / 32, 320 / 16).draw(game.map[y][x]==APPLE?Palette::Red:game.map[y][x]==EMPTY?Palette::White: Palette::Blue);
}
}
}
}
Game.h
#pragma once
// 方向やアイテムを定義する
#define UP 1
#define DOWN 2
#define RIGHT 3
#define LEFT 4
#define OVER 1
#define APPLE -1
#define EMPTY 0
class SnakeGame {
private:
int x,y,max_x, max_y,direction=RIGHT,len=3;
// りんごをランダムに置くプログラム
void put_apple() {
int randx, randy;
while (1) {
randx = Random(max_x-1);
randy = Random(max_y-1);
// ランダムな場所が空いてなかったらもう一回
if (map[randy][randx] != EMPTY) {
continue;
}
// ヘビの進行方向にりんごがあったらもう一回
switch (direction) {
case UP:
if (x != randx || randy > y) {
map[randy][randx] = APPLE;
return;
}
break;
case DOWN:
if (x != randx || randy < y) {
map[randy][randx] = APPLE;
return;
}
break;
case RIGHT:
if (y != randy || randx > x) {
map[randy][randx] = APPLE;
return;
}
break;
case LEFT:
if (y != randy || randx < x) {
map[randy][randx] = APPLE;
return;
}
break;
}
}
}
public:
int map[128][128];
SnakeGame(int _x, int _y) {
// マップの大きさの初期化
max_x = _x;
max_y = _y;
// ヘビの初期座標
x = max_x / 2;
y = max_y / 2;
// マップの初期化
for (int i = 0; i < max_y; i++) {
for (int j = 0; j < max_x; j++) {
map[i][j] = EMPTY;
}
}
map[y][x] = len;
put_apple();
}
// ヘビの次の移動方向の変化
void Chg_direction(int key) {
switch (key) {
case UP:
case DOWN:
if (direction == LEFT || direction == RIGHT) {
direction = key;
}
break;
case RIGHT:
case LEFT:
if (direction == UP || direction == DOWN) {
direction = key;
}
}
}
int update() {
// 方向によるヘビの移動
switch (direction) {
case UP:
y--;
break;
case DOWN:
y++;
break;
case RIGHT:
x++;
break;
case LEFT:
x--;
break;
}
// ヘビが壁や体に当たったらゲームオーバー
if (0 > y || y >= max_y || 0 > x || x >= max_x) {
return OVER;
}
if (map[y][x] != APPLE && map[y][x] != EMPTY) {
return OVER;
}
// ヘビがりんごをとったら、ヘビの長さをインクリメント&りんごを置く
if (map[y][x] == APPLE) {
len++;
put_apple();
} else {
for (int i = 0; i < max_y; i++) {
for (int j = 0; j < max_x; j++) {
// ヘビの体の通し番号を引く
if (map[i][j] > 0)map[i][j]--;
}
}
}
map[y][x] = len;
return 0;
}
};
詳しいコードは上にありますが、概要をざっくり説明します。
最初に、SnakeGameのコンストラクタが呼び出され以下のことをします。
- マップの初期化
- 初期のヘビとりんごの位置を求める
そして、main.cpp
で決められた時間ごとupdate
が呼び出されてヘビが移動します。
ヘビの移動の間にChg_direction
によって、ヘビの方向を変えることができます。
また、壁や自分の体に当たるとupdate
がOVERを返します。
実装手順2 Webページの作成
index.html
<!DOCTYPE html>
<html id='swip'>
<head>
<meta charset='utf-8' name='viewport' content='width=device-width, initial-scale=1' />
<style>
<!--見た目関係なので省略-->
</style>
</head>
<body>
操作方法<br>
<input type='radio' name='sb' checked='checked' onchange='Show(true);'>ボタン</input>
<input type='radio' name='sb' onchange="Show(false);">スワイプ</input>
<br>
<div id='b'>
<center>
<div class='Div'><button class='btn' id='up'>↑</button></div><br>
<div class='teki'>
<div class='Div float Left'><button class='btn' id='left'>←</button></div>
<div class='Div float Right'><button class='btn' id='right'>→</button></div>
<div class='clear'></div>
</div><br>
<div class='Div'><button class='btn' id='down'>↓</button></div>
</center>
</div>
<script type="text/javascript">
let flg = true;
let btns = ['up', 'right', 'left', 'down'];
let btn = new Array(4);
function Show(f) {
flg = f;
if (f == true) {
document.getElementById('b').hidden = false;
} else {
document.getElementById('b').hidden = true;
}
}
function Send(dir) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '?' + dir);
xhr.timeout = 1000;
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT');
xhr.responseType = 'document';
xhr.ontimeout = function (e) { xhr.abort(); };
xhr.send();
}
for (let i = 0; i < 4; i++) {
btn[i] = document.getElementById(btns[i]);
btn[i].onclick = function () { Send(btns[i]) };
}
// スワイプの方向を取得
let t = document.getElementById("swip");
let sx, sy, mx, my;
t.addEventListener("touchstart", function (e) {
e.preventDefault();
sx = e.touches[0].pageX;
sy = e.touches[0].pageY;
mx = e.touches[0].pageX;
my = e.touches[0].pageY;
});
t.addEventListener("touchmove", function (e) {
e.preventDefault();
mx = e.changedTouches[0].pageX;
my = e.changedTouches[0].pageY;
});
t.addEventListener("touchend", function (e) {
let RLUD = [sy - my, mx - sx, sx - mx, my - sy, 100];
let n = RLUD.indexOf(Math.max(...RLUD));
if (!flg && n < 4) { Send(btns[n]); }
});
</script>
</body>
</html>
上記がWebページのHTMLです。(JSもごっちゃになっています。すいません!)
今回、お話するのはSend
関数についてです。
本当はAjaxを利用したかったのですが、後述しますがインターネットにアクセスできない状況であり、JavaScriptのXMLHTttpRequest
を利用しています。
今考えると、fetchAPIを利用すればよかったと思っています。
送っているのは、GET /?{方向}
です。
本来のPOSTで送った方が良いのですが、この時の自分はメソッドについてよく理解していませんでした。
実装手順3 index.htmlをESP32で表示し、GETなどを取得する
Web.h
#include <WiFi.h>
String str ="ここに上記のhtmlを改行なしで入れる"
String header;
const IPAddress ip(192, 168, 0, 3);
const IPAddress subnet(255, 255, 255, 0);
WiFiServer server(80);
class _Web {
private:
char *ssid="ESP32";
public:
_Web(char* s) {
// SSIDの変更
ssid = s;
}
void init() {
WiFi.softAP(ssid);
delay(100);
WiFi.softAPConfig(ip, ip, subnet);
server.begin();
}
int _update() {
int dir = EMPTY;
WiFiClient client = server.available();
if (client) {
String currentLine = "";
while (client.connected()) {
if (client.available()) {
char c = client.read();
header += c;
if (c == '\n') {
if (currentLine.length() == 0) {
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
client.println(str);
if (header.indexOf("GET /?up") >= 0) {
dir = UP;
}
if (header.indexOf("GET /?right") >= 0) {
dir = RIGHT;
}
if (header.indexOf("GET /?left") >= 0) {
dir = LEFT;
}
if (header.indexOf("GET /?down") >= 0) {
dir = DOWN;
}
client.println();
// Break out of the while loop
break;
} else {
currentLine = "";
}
} else if (c != '\r') {
currentLine += c;
}
}
}
header = "";
client.stop();
}
return dir;
}
};
基本的には、GETが送られればHTMLファイルが送られます。
/?{方向}
のときには、方向が代入されて_update
の返り値になります。
実装手順4 マトリックスLEDを光らせる
SnakeGame.ino
#include"Game.h"
#include"Web.h"
const int RGB1[3] = {2, 0, 4}, RGB2[3] = {16, 17, 5};
const int CLK = 15, LAT = 32, OE = 33, ABC[3] = {25, 26, 27}, WIDTH = 32, HEIGHT = 16;
SnakeGame *game = new SnakeGame(32, 16);
_Web webs("SnakeGameAP");
void setup() {
Serial.begin(115200);
webs.init();
for (int i = 0; i < 3; i++) {
pinMode(RGB1[i], OUTPUT);
pinMode(RGB2[i], OUTPUT);
pinMode(ABC[i], OUTPUT);
}
pinMode(CLK, OUTPUT);
pinMode(LAT, OUTPUT);
pinMode(OE, OUTPUT);
digitalWrite(CLK, LOW);
digitalWrite(LAT, LOW);
digitalWrite(OE, LOW);
}
void loop() {
static int dir = OVER;
static unsigned long long int game_t = millis();
int dirW=webs._update();
if(dirW!=EMPTY){
if(dir==OVER){
for(int i=0;i<8;i++){
for(int j=0;j<32;j++){
game->_map[i][j]=0;
}
}
}
dir=dirW;
game->Chg_direction(dirW);
}
if (millis() - game_t >= 300) {
game_t = millis();
if(dir != OVER && game->_update() == OVER){
delete game;
game = new SnakeGame(32, 16);
dir = OVER;
}
}
//
for (int i = 0; i < HEIGHT / 2; i++) {
for (int j = 0; j < WIDTH; j++) {
int p1 = game->_map[i][j] > 0 ? 0b001 : game->_map[i][j] == APPLE ? 0b100 : 0;
int p2 = game->_map[i + HEIGHT / 2][j] > 0 ? 0b001 : game->_map[i + HEIGHT / 2][j] == APPLE ? 0b100 : 0;
for (int k = 0; k < 3; k++) {
digitalWrite(RGB1[k], (p1 >> (2 - k)) & 0b001);
digitalWrite(RGB2[k], (p2 >> (2 - k)) & 0b001);
}
digitalWrite(CLK, HIGH);
digitalWrite(CLK, LOW);
}
digitalWrite(LAT, HIGH);
digitalWrite(OE, HIGH);
delayMicroseconds(3);
for (int k = 0; k < 3; k++) {
digitalWrite(ABC[k], (i >> k) & 0b001 );
}
delayMicroseconds(3);
digitalWrite(LAT, LOW);
digitalWrite(OE, LOW);
}
}
こちらの記事を参考に、ライブラリを使わずにマトリックスLEDを光らせました。
Webクラスの_update
の内容からSnakeGameクラスのChg_direction
に値を渡し、方向を変えています。
完成したモノ
最後に
今回、急遽記事を書くことにしたのですが、去年の自分のコードの汚さと自分の成長を見れてよかったと感じてます。
今回のプログラムがあるリポジトリは以下から行けます。
https://github.com/AizuGeekDojo/SnakeGame