0
Help us understand the problem. What are the problem?

posted at

マトリックスLEDを使ったヘビゲームの制作

マトリックスLEDを使ったヘビゲームの制作

はじめに

今回の記事はAizu Advent Calendar 2021の2日目の記事です!
急遽、書くことを決めたため、今回の制作自体は去年のモノになります。

ヘビゲームとは

伸長するヘビを操作して、エサを食べ続けることがゲームの目的である。画面を全て埋め尽くすとゲームクリアとなるものと、エサを食べて大きくなるだけのものがある。エサではなく動いた軌跡がそのまま残り、なおかつランダムに障害物が置かれ、それを回避する目的のゲームもある。プレイヤーはヘビを操作し、その頭が壁あるいは自身の身体にぶつからないようにしながら、ランダムで出現するエサを回収する。エサを回収するたびにヘビの身体は1マスずつ長くなっていく。ヘビは静止することができず、常に動き回っているため、エサを回収すればするほど自身の身体を回避することが難しくなる。(Wikipedia より)

とのことです。
GooglがWebで公開しているヘビゲームもあるので是非遊んでみてください。
今回はこれを、マトリックスLEDをゲーム画面にしてスマホから操作出来るように、ESP32を用いて実装しようと思います。

実装手順1 ゲームの作成

まず最初に、ゲーム部分をPC内で動くようにクラスにして、作成します。
Siv3Dを利用して実装します。


main.cpp
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
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
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
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に値を渡し、方向を変えています。

完成したモノ

IMG_20211202_132953.jpg
IMG_20211202_133048.jpg
ここには書いていませんが、最大点数表示も追加しました。

最後に

今回、急遽記事を書くことにしたのですが、去年の自分のコードの汚さと自分の成長を見れてよかったと感じてます。
今回のプログラムがあるリポジトリは以下から行けます。
https://github.com/AizuGeekDojo/SnakeGame

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?