PACMAN with M5STACK
PACMAN ゲームを M5STACK FACES に移植しました。 2018.03.07
.
Pacman は Arduino Due 用に Github CrazyLabz! の DrNCXCortex氏 が作成しました。
YouTube:Pacman on Arduino Due
github:Pacman Game for Arduino Due
その ソースを基に @Tw_Mhage氏 が ESP32 + 240×320 ILI9328 TFT Display へ移植しました。
Qiita:ESP32でゲームを作ってみた:MhageGH / esp32_ILI9328_Pacman
移植方法:@Tw_Mhage氏の esp32_ILI9328_Pacman を M5STACK FACES用に変更します。
スケッチで注目する技術は 画像表示に たった1行の命令で構成されている事です。
DrNCXCortex氏 と @Tw_Mhage氏 に 感謝致します。
.
準備:
M5Stack Store:
M5STACK BASIC :$35.00
M5STACK FACES Pocket Computer:$89.90 ( 9498円:03/07 1$ 105.6円)。
スケッチ:esp32_ILI9328_Pacman を ダウンロードします。
.
構成:
M5STACK FACES と GameBoy Panel を使用します。
原作の画像は 320 (縦) x 240 ( 横 ) です。FACES は 240 (縦) x 320 ( 横 ) です。
FACES を縦に使用しますと 画像の縦横の変更が必要になる為に FACES を横にします。
操作は START, SELECT , CROSS key で行います。
DEMO モードがあり ボタン操作しなくとも デモ画面を見る事ができます。
FACES でなく M5STACK Basic の A,B,C ボタンでも可能ですが 3つのボタンで
6つの操作をする為にスケッチの工夫が必要になります。
方法は esp32 snake with M5STACK を参照下さい。
.
移植:以下の8つを変更します。
_ esp32_ILI9328_Pacman.ino:変更あり。
_ DrawindexedMap.h:画像表示ルーチン:変更あり。
_ centsc.h , carpal.h, font8x8.cpp:変更なし。
変更:esp32_ILI9328_Pacman.ino:
1:追加1:24行目
#include <M5Stack.h>
2:削除1:25-27行を削除:
const char ssid[] = "ESP32"; const char password[] = "esp32pass"; const int localPort = 10000;
3:削除2:53-55行を削除:
#include <SPI.h> #include <WiFi.h> #include <WiFiUdp.h>
4:削除3:57-61行を削除:
#define CS 5 #define RESET 17 ili9328SPI tft(CS, RESET); WiFiUDP udp;
5:変更1:1394-1402行:setup を M5STACK用に変更します。
void setup() { randomSeed(analogRead(0)); Serial.begin(115200); M5.begin(); Wire.begin(); M5.Lcd.setRotation(3); M5.Lcd.fillScreen(TFT_BLACK); pinMode(5, INPUT_PULLUP); delay(100); }
6:変更2:1394-1402行:KeyPadLoop を FACES GameBoy 用に 以下2-14を追加します。
void KeyPadLoop(){ char r; if(digitalRead(5) == LOW) { Wire.requestFrom(0X88, 1); // request 1 byte from keyboard while (Wire.available()) { uint8_t key_val = Wire.read(); // receive a byte as character if ( key_val == 191) { r = 'z';} // select if ( key_val == 127) { r = 'x';} // start if ( key_val == 247) { r = '8';} // up if ( key_val == 251) { r = '2';} // down if ( key_val == 254) { r = '4';} // left if ( key_val == 253) { r = '6';} // right } } if (r == 'z') { ClearKeys(); but_A=true; delay(300); } //else but_A=false; if (r == 'x') { ClearKeys(); but_B=true; delay(300); } //else but_B=false; if (r == '8') { ClearKeys(); but_UP=true; } //else but_UP=false; if (r == '2') { ClearKeys(); but_DOWN=true; } //else but_DOWN=false; if (r == '4') { ClearKeys(); but_LEFT=true; } //else but_LEFT=false; if (r == '6') { ClearKeys(); but_RIGHT=true; } //else but_RIGHT=false; }
変更:DrawindexedMap.h:
7:削除4:16行目を削除:#include “ili9328.h”
8:変更3:46-61行:
_ word を uint16_t に変更。
_ TFT 表示:tft->drawFastHLine1 を M5.Lcd.drawFastHLine に変更します。
_ 注目:画像表示処理は 9行目の「drawFastHLine」1行で行われています。
void drawIndexedmap(uint8_t* indexmap, int16_t x, uint16_t y) { byte i = 0; uint16_t color = _paletteW[indexmap[0]]; for (byte tmpY = 0; tmpY < 8; tmpY++) { byte width = 1; for (byte tmpX = 0; tmpX < 8; tmpX++) { uint16_t next_color = _paletteW[indexmap[++i]]; if ((color != next_color && width >= 1) || tmpX == 7) { M5.Lcd.drawFastHLine(x + tmpX - width, y + tmpY, width, color); color = next_color; width = 0; } width++; } } }
.
Gameboy Panel:
Gameboy Panel は ATmega328P で構成され I2C で通信しています。
アドレスは 0x88 です。
参考:
M5-hardware: M5-hardware/FACES/GameBoy.ino:U,D,L,R,A,B,s,S が定義されています。
Modules/FACES/I2C_Keyboard/:
例:CROSS Key の上は BIN = 0xFE, DEC = 254, ASCii = U です。
.
価格:
2月28日から スイッチサイエンスより M5STACK が発売されました。
_ 4640円。(4490+150):スイッチサイエンス
_ 海外との価格の比較は以下で 最安値の販売店より 1252円 程高いです。
_ AliExpress 最安値:$30.50 ( 3238円 03/09 1$ 106.1円換算 )
_ $30.50 ( 3238円 03/09 1$ 106.1円換算 ):SHENGSUN SENOR Store
_ $30.50 ( 3238円 03/09 1$ 106.1円換算 ):BETTERSHENGSUN SHENGSUN Store
_ $30.88 ( 3278円 03/09 1$ 106.1円換算 ):CFSUNBIRD Store
_ $31.50 ( 3343円 03/09 1$ 106.1円換算 ):Shop3213115 Store
_ $35.00 ( 3715円 03/09 1$ 106.1円換算 ):M5Stack Store:本家
_ M5STACK FACES:M5Stack Store との価格差は 2536円 と高いです。
_ スイッチサイエンス:価格 12034円。
_ 注意:SS の FACES には サンプルが書き込まれていません。
_ M5Stack Store:価格 $89.90 ( 9498円:03/07 1$ 105.6円)。
_ サンプルには マリオが 書き込まれています。
_ 輸送期間:M5STACK STORE は 2月実績では 10日で届いています。
Maker Faire Shenzhen(深セン) 2017 での価格:M5STACK BASIC 2700円。
_ Maker Faire Shenzhen(深セン) 2017に行ってきました その2
_ 記事中の 3/4 あたりに記載されています。
_ COREのディベロッパーキットを買いました(158元、約2,700円)。
.
M5STACK COLLECTION:Github:Parts and Circuits
_ GitHub に M5STACK のスケッチを収集した場所があります。
_ 私が移植した M5Stack-SpaceShooter, M5Stack-Tetris が掲載されています。
_ gojimmypi, Kongduino, tomsuch へのリンクも掲載されており便利です。
.
感想:
M5STACK の ディスプレーとボタンの標準装備は 工作不要で大変便利です。
特に ゲーム用の GameBoy アダプターは 作るとなると手間と多大な時間を使用します
ので最高です。
原作:DrNCXCortex氏による PACMAN のリストから学ぶ物は多く 感嘆致します。
_ メインの esp32_ILI9328_Pacman.ino は 変数の処理に多大な内容を使用し、
_ グラフィック表示の為の DrawindexedMap.h は たった1行の 「drawFastHLine」
_ で行っています。つまりスムーズに動いている画像は 横書きの線で行われています。
_ 素晴らしいコードにより 移植を容易に行なう事ができました。
_ Pacman-Arduino-Due-master は 2014年に作成され 冒頭に以下の様に書かれています。
_ /* Copyright (c) 2014 Dr. NCX (mirracle.mxx@gmail.com) */
_ /* THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL */
.
スケッチ:PACMAN with M5STACK : 2018.03.08 Transplant by macsbug
//======================================================================== // PACMAN with M5STACK : 2018.03.08 Transplant by macsbug // Controller : FACES / GameBoy // Github : https://macsbug.wordpress.com/2018/03/07/pacman-with-m5stack/ /******************************************************************************/ /* PACMAN GAME FOR ARDUINO DUE */ /******************************************************************************/ /* Copyright (c) 2014 Dr. NCX (mirracle.mxx@gmail.com) */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL */ /* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED */ /* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR */ /* BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES */ /* OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, */ /* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, */ /* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS */ /* SOFTWARE. */ /* */ /* MIT license, all text above must be included in any redistribution. */ /* * Controller configuration: * Buttons UP, RIGHT, DOWN, LEFT, PAUSE and RESTART are each assigned * on characters '8', '6', '2', '4', 'x', 'z' * in the both case of SerialPort and WiFi UDP. */ // Pacman Game for Arduino Due // https://github.com/DrNCXCortex/Pacman-Arduino-Due // https://qiita.com/Tw_Mhage/items/ca9c52c52b4258ff27f1 #include <M5Stack.h> #include "M5StackUpdater.h" byte SPEED = 1; // 1=SLOW 2=NORMAL 4=FAST //do not try other values!!! /******************************************************************************/ /* MAIN GAME VARIABLES */ /******************************************************************************/ #define BONUS_INACTIVE_TIME 600 #define BONUS_ACTIVE_TIME 300 #define START_LIFES 2 #define START_LEVEL 1 byte MAXLIFES = 5; byte LIFES = START_LIFES; byte GAMEWIN = 0; byte GAMEOVER = 0; byte DEMO = 1; byte LEVEL = START_LEVEL; byte ACTUALBONUS = 0; //actual bonus icon byte ACTIVEBONUS = 0; //status of bonus byte GAMEPAUSED = 0; byte PACMANFALLBACK = 0; #include "DrawindexedMap.h" /******************************************************************************/ /* Controll KEYPAD LOOP */ /******************************************************************************/ boolean but_A = false; //38 boolean but_B = false; //40 boolean but_UP = false; //52 boolean but_DOWN = false; //50 boolean but_LEFT = false; //48 boolean but_RIGHT = false; //46 /******************************************************************************/ /* GAME VARIABLES AND DEFINITIONS */ /******************************************************************************/ #include "PacmanTiles.h" //------------------------------------------------------------------------------/ enum GameState { ReadyState, PlayState, DeadGhostState, // Player got a ghost, show score sprite and only move eyes DeadPlayerState, EndLevelState }; //------------------------------------------------------------------------------/ enum SpriteState{ PenState, RunState, FrightenedState, DeadNumberState, DeadEyesState, AteDotState, // pacman DeadPacmanState }; //------------------------------------------------------------------------------/ enum { MStopped = 0, MRight = 1, MDown = 2, MLeft = 3, MUp = 4, }; //------------------------------------------------------------------------------/ #define ushort uint16_t #define BINKY 0 #define PINKY 1 #define INKY 2 #define CLYDE 3 #define PACMAN 4 #define BONUS 5 //------------------------------------------------------------------------------/ const byte _initSprites[] ={ BINKY, 14, 17 - 3, 31, MLeft, PINKY, 14 - 2, 17, 79, MLeft, INKY, 14, 17, 137, MLeft, CLYDE, 14 + 2, 17, 203, MRight, PACMAN, 14, 17 + 9, 0, MLeft, BONUS, 14, 17 + 3, 0, MLeft, }; //------------------------------------------------------------------------------/ // Ghost colors const byte _palette2[] ={ 0, 11, 1, 15, // BINKY red 0, 11, 3, 15, // PINKY pink 0, 11, 5, 15, // INKY cyan 0, 11, 7, 15, // CLYDE brown 0, 11, 9, 9, // PACMAN yellow 0, 11,15, 15, // FRIGHTENED 0, 11, 0, 15, // DEADEYES 0, 1, 15, 2, // cherry 0, 1, 15, 12, // strawberry 0, 7, 2, 12, // peach 0, 9, 15, 0, // bell 0, 15, 1, 2, // apple 0, 12, 15, 5, // grape 0, 11, 9, 1, // galaxian 0, 5, 15, 15, // key }; //------------------------------------------------------------------------------/ const byte _paletteIcon2[] ={ 0, 9, 9, 9, // PACMAN 0, 2, 15, 1, // cherry 0, 12,15, 1, // strawberry 0, 12, 2, 7, // peach 0, 0, 15, 9, // bell 0, 2, 15, 1, // apple 0, 12,15, 5, // grape 0, 1, 9, 11, // galaxian 0, 5,15, 15, // key }; //------------------------------------------------------------------------------/ #define PACMANICON 1 #define BONUSICON 2 #define FRIGHTENEDPALETTE 5 #define DEADEYESPALETTE 6 #define BONUSPALETTE 7 #define FPS 60 #define CHASE 0 #define SCATTER 1 #define DOT 7 #define PILL 14 #define PENGATE 0x1B const byte _opposite[] = { MStopped, MLeft, MUp, MRight, MDown }; #define OppositeDirection(_x) pgm_read_byte(_opposite + _x) const byte _scatterChase[] = { 7, 20, 7, 20, 5, 20, 5, 0 }; const byte _scatterTargets[] = { 2, 0, 25, 0, 0, 35, 27, 35 }; // inky/clyde scatter targets are backwards //const char _pinkyTargetOffset[] = { 4, 0, 0, 4, -4, 0, -4, 4 }; // Includes pinky target bug const char _pinkyTargetOffset[] = { 4, 0, 0, 4, 4, 0, 4, 4 }; // Includes pinky target bug #define FRIGHTENEDGHOSTSPRITE 0 #define GHOSTSPRITE 2 #define NUMBERSPRITE 10 #define PACMANSPRITE 14 const byte _pacLeftAnim[] = { 5, 6, 5, 4 }; const byte _pacRightAnim[] = { 2, 0, 2, 4 }; const byte _pacVAnim[] = { 4, 3, 1, 3 }; word _BonusInactiveTimmer = BONUS_INACTIVE_TIME; word _BonusActiveTimmer = 0; /******************************************************************************/ /* GAME - Sprite Class */ /******************************************************************************/ class Sprite{ public: int16_t _x, _y; int16_t lastx, lasty; byte cx, cy; // cell x and y byte tx, ty; // target x and y SpriteState state; byte pentimer; // could be the same byte who; byte _speed; byte dir; byte phase; // Sprite bits byte palette2; // 4->16 color map index byte bits; // index of sprite bits signed char sy; //-------------------------------------------------------------------------/ void Init(const byte* s){ who = pgm_read_byte(s++); cx = pgm_read_byte(s++); cy = pgm_read_byte(s++); pentimer = pgm_read_byte(s++); dir = pgm_read_byte(s); _x = lastx = (int16_t)cx * 8 - 4; _y = lasty = (int16_t)cy * 8; state = PenState; _speed = 0; Target(random(20), random(20)); } //-------------------------------------------------------------------------/ void Target(byte x, byte y){ tx = x; ty = y; } int16_t Distance(byte x, byte y){ int16_t dx = cx - x; int16_t dy = cy - y; return dx * dx + dy * dy; // Distance to target } //-------------------------------------------------------------------------/ // once per sprite, not 9 times void SetupDraw(GameState gameState, byte deadGhostIndex){ sy = 1; palette2 = who; byte p = phase >> 3; if (who == BONUS) { //BONUS ICONS bits = 21 + ACTUALBONUS; palette2 = BONUSPALETTE + ACTUALBONUS; return; } if (who != PACMAN){ bits = GHOSTSPRITE + ((dir - 1) << 1) + (p & 1); // Ghosts switch (state){ case FrightenedState: bits = FRIGHTENEDGHOSTSPRITE + (p & 1); // frightened palette2 = FRIGHTENEDPALETTE; break; case DeadNumberState: palette2 = FRIGHTENEDPALETTE; bits = NUMBERSPRITE + deadGhostIndex; break; case DeadEyesState: palette2 = DEADEYESPALETTE; break; default: ; } return; } // PACMAN animation byte f = (phase >> 1) & 3; if (dir == MLeft) f = pgm_read_byte(_pacLeftAnim + f); else if (dir == MRight) f = pgm_read_byte(_pacRightAnim + f); else f = pgm_read_byte(_pacVAnim + f); if (dir == MUp) sy = -1; bits = f + PACMANSPRITE; } //-------------------------------------------------------------------------/ // Draw this sprite into the tile at x,y void Draw8(int16_t x, int16_t y, byte* tile){ int16_t px = x - (_x - 4); if (px <= -8 || px >= 16) return; int16_t py = y - (_y - 4); if (py <= -8 || py >= 16) return; // Clip y int16_t lines = py + 8; if (lines > 16) lines = 16; if (py < 0){ tile -= py * 8; py = 0; } lines -= py; // Clip in X byte right = 16 - px; if (right > 8) right = 8; byte left = 0; if (px < 0){ left = -px; px = 0; } // Get bitmap signed char dy = sy; if (dy < 0) py = 15 - py; // VFlip byte* data = (byte*)(pacman16x16 + bits * 64); data += py << 2; dy <<= 2; data += px >> 2; px &= 3; const byte* palette = _palette2 + (palette2 << 2); while (lines){ const byte *src = data; byte d = pgm_read_byte(src++); d >>= px << 1; byte sx = 4 - px; byte x = left; do{ byte p = d & 3; if (p){ p = pgm_read_byte(palette + p); if (p) tile[x] = p; } d >>= 2; // Next pixel if (!--sx){ d = pgm_read_byte(src++); sx = 4; } } while (++x < right); tile += 8; data += dy; lines--; } } }; /******************************************************************************/ /* GAME - Playfield Class */ /******************************************************************************/ class Playfield{ Sprite _sprites[5]; Sprite _BonusSprite; //Bonus byte _dotMap[(32 / 4) * (36 - 6)]; GameState _state; long _score; // 7 digits of score long _hiscore; // 7 digits of score long _lifescore; signed char _scoreStr[8]; signed char _hiscoreStr[8]; byte _icons[14]; // Along bottom of screen ushort _stateTimer; ushort _frightenedTimer; byte _frightenedCount; byte _scIndex; // ushort _scTimer; // next change of sc status bool _inited; byte* _dirty; public: Playfield() : _inited(false) { // Swizzle palette TODO just fix in place // byte * p = (byte*)_paletteW; // for (int16_t i = 0; i < 16; i++) // { // ushort w = _paletteW[i]; // Swizzle // *p++ = w >> 8; // *p++ = w; // } } //-------------------------------------------------------------------------/ // Draw 2 bit BG into 8 bit icon tiles at bottom void DrawBG2(byte cx, byte cy, byte* tile){ byte index = 0; signed char b = 0; index = _icons[cx >> 1]; // 13 icons across bottom if (index == 0){ memset(tile, 0, 64);return;} index--; index <<= 2; // 4 tiles per icon b = (1 - (cx & 1)) + ((cy & 1) << 1); // Index of tile const byte* bg = pacman8x8x2 + ((b + index) << 4); const byte* palette = _paletteIcon2 + index; byte x = 16; while (x--){ byte bits = (signed char)pgm_read_byte(bg++); byte i = 4; while (i--){ tile[i] = pgm_read_byte(palette + (bits & 3)); bits >>= 2; } tile += 4; } } //-------------------------------------------------------------------------/ byte GetTile(int16_t cx, int16_t ty){ if (_state != ReadyState && ty == 20 && cx > 10 && cx < 17) {return (0);} //READY TEXT ZONE if (LEVEL % 5 == 1) return pgm_read_byte(playMap1 + ty * 28 + cx); if (LEVEL % 5 == 2) return pgm_read_byte(playMap2 + ty * 28 + cx); if (LEVEL % 5 == 3) return pgm_read_byte(playMap3 + ty * 28 + cx); if (LEVEL % 5 == 4) return pgm_read_byte(playMap4 + ty * 28 + cx); if (LEVEL % 5 == 0) return pgm_read_byte(playMap5 + ty * 28 + cx); } //-------------------------------------------------------------------------/ // Draw 1 bit BG into 8 bit tile void DrawBG(byte cx, byte cy, byte* tile){ if (cy >= 34) { //DRAW ICONS BELLOW MAZE DrawBG2(cx, cy, tile); return; } byte c = 11; if (LEVEL % 8 == 1) c = 11; // Blue if (LEVEL % 8 == 2) c = 12; // Green if (LEVEL % 8 == 3) c = 1; // Red if (LEVEL % 8 == 4) c = 9; // Yellow if (LEVEL % 8 == 5) c = 2; // Brown if (LEVEL % 8 == 6) c = 5; // Cyan if (LEVEL % 8 == 7) c = 3; // Pink if (LEVEL % 8 == 0) c = 15; // White byte b = GetTile(cx, cy); const byte* bg; // This is a little messy memset(tile, 0, 64); if (cy == 20 && cx >= 11 && cx < 17){ if (DEMO == 1 && ACTIVEBONUS == 1) return; if ((_state != ReadyState && GAMEPAUSED != 1 && DEMO != 1) || ACTIVEBONUS == 1) b = 0; // hide 'READY!' else if (DEMO == 1 && cx == 11) b = 0; else if (DEMO == 1 && cx == 12) b = 'D'; else if (DEMO == 1 && cx == 13) b = 'E'; else if (DEMO == 1 && cx == 14) b = 'M'; else if (DEMO == 1 && cx == 15) b = 'O'; else if (DEMO == 1 && cx == 16) b = 0; else if (GAMEPAUSED == 1 && cx == 11) b = 'P'; else if (GAMEPAUSED == 1 && cx == 12) b = 'A'; else if (GAMEPAUSED == 1 && cx == 13) b = 'U'; else if (GAMEPAUSED == 1 && cx == 14) b = 'S'; else if (GAMEPAUSED == 1 && cx == 15) b = 'E'; else if (GAMEPAUSED == 1 && cx == 16) b = 'D'; } else if (cy == 1){ if (cx < 7) b = _scoreStr[cx]; else if (cx >= 10 && cx < 17) b = _hiscoreStr[cx - 10]; // HiScore } else { if (b == DOT || b == PILL){ // DOT==7 or PILL==16 if (!GetDot(cx, cy)) return; c = 14; } if (b == PENGATE) c = 14; } bg = playTiles + (b << 3); if (b >= '0') c = 15; // text is white for (byte y = 0; y < 8; y++){ signed char bits = (signed char)pgm_read_byte(bg++); ///WARNING CHAR MUST BE signed !!! byte x = 0; while (bits){ if (bits < 0) tile[x] = c; bits <<= 1; x++; } tile += 8; } } //-------------------------------------------------------------------------/ // Draw BG then all sprites in this cell void Draw(uint16_t x, uint16_t y, bool sprites){ byte tile[8 * 8]; // Fill with BG if (y == 20 && x >= 11 && x < 17 && DEMO == 1 && ACTIVEBONUS == 1) return; DrawBG(x, y, tile); // Overlay sprites x <<= 3; y <<= 3; if (sprites){ for (byte i = 0; i < 5; i++) _sprites[i].Draw8(x, y, tile); //AND BONUS if (ACTIVEBONUS) _BonusSprite.Draw8(x, y, tile); } // Show sprite block #if 0 for (byte i = 0; i < 5; i++) { Sprite* s = _sprites + i; if (s->cx == (x >> 3) && s->cy == (y >> 3)){ memset(tile, 0, 8); for (byte j = 1; j < 7; j++) tile[j * 8] = tile[j * 8 + 7] = 0; memset(tile + 56, 0, 8); } } #endif x += (240 - 224) / 2;//x += -0; y += (320 - 288) / 2;//y += -26; // Should be a direct Graphics call byte n = tile[0]; byte i = 0; word color = (word)_paletteW[n]; drawIndexedmap(tile, x, y); //******** drawIndexedmap(&tft, tile, x, y); } boolean updateMap [36][28]; // Mark tile as dirty (should not need range checking here) void Mark(int16_t x, int16_t y, byte* m){ x -= 4; y -= 4; updateMap[(y >> 3)][(x >> 3)] = true; updateMap[(y >> 3)][(x >> 3) + 1] = true; updateMap[(y >> 3)][(x >> 3) + 2] = true; updateMap[(y >> 3) + 1][(x >> 3)] = true; updateMap[(y >> 3) + 1][(x >> 3) + 1] = true; updateMap[(y >> 3) + 1][(x >> 3) + 2] = true; updateMap[(y >> 3) + 2][(x >> 3)] = true; updateMap[(y >> 3) + 2][(x >> 3) + 1] = true; updateMap[(y >> 3) + 2][(x >> 3) + 2] = true; } //-------------------------------------------------------------------------/ void DrawAllBG(){ for (byte y = 0; y < 36; y++) for (byte x = 0; x < 28; x++) { Draw(x, y, false); } } // Draw sprites overlayed on cells void DrawAll(){ byte* m = _dirty; // Mark sprite old/new positions as dirty for (byte i = 0; i < 5; i++){ Sprite* s = _sprites + i; Mark(s->lastx, s->lasty, m); Mark(s->_x, s->_y, m); } // Mark BONUS sprite old/new positions as dirty Sprite* _s = &_BonusSprite; Mark(_s->lastx, _s->lasty, m); Mark(_s->_x, _s->_y, m); // Animation for (byte i = 0; i < 5; i++) _sprites[i].SetupDraw(_state, _frightenedCount - 1); _BonusSprite.SetupDraw(_state, _frightenedCount - 1); for (byte tmpY = 0; tmpY < 36; tmpY++) { for (byte tmpX = 0; tmpX < 28; tmpX++) { if (updateMap[tmpY][tmpX] == true) Draw(tmpX, tmpY, true); updateMap[tmpY][tmpX] = false; } } } int16_t Chase(Sprite* s, int16_t cx, int16_t cy){ while (cx < 0) // Tunneling cx += 28; while (cx >= 28) cx -= 28; byte t = GetTile(cx, cy); if (!(t == 0 || t == DOT || t == PILL || t == PENGATE)) return 0x7FFF; if (t == PENGATE){ if (s->who == PACMAN) return 0x7FFF; // Pacman can't cross this to enter pen if (!(InPen(s->cx, s->cy) || s->state == DeadEyesState)) return 0x7FFF; // Can cross if dead or in pen trying to get out } int16_t dx = s->tx - cx; int16_t dy = s->ty - cy; return (dx * dx + dy * dy); // Distance to target } //-------------------------------------------------------------------------/ void UpdateTimers(){ // Update scatter/chase selector, low bit of index indicates scatter if (_scIndex < 8){ if (_scTimer-- == 0){ byte duration = pgm_read_byte(_scatterChase + _scIndex++); _scTimer = duration * FPS; } } // BONUS timmer if (ACTIVEBONUS == 0 && _BonusInactiveTimmer-- == 0) { _BonusActiveTimmer = BONUS_ACTIVE_TIME; //5*FPS; ACTIVEBONUS = 1; } if (ACTIVEBONUS == 1 && _BonusActiveTimmer-- == 0) { _BonusInactiveTimmer = BONUS_INACTIVE_TIME; //10*FPS; ACTIVEBONUS = 0; } // Release frightened ghosts if (_frightenedTimer && !--_frightenedTimer){ for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; if (s->state == FrightenedState){ s->state = RunState; s->dir = OppositeDirection(s->dir); } } } } // Target closes pill, run from ghosts? void PacmanAI(){ Sprite* pacman; pacman = _sprites + PACMAN; // Chase frightened ghosts Sprite* closestGhost = NULL; Sprite* frightenedGhost = NULL; Sprite* closestAttackingGhost = NULL; Sprite* DeadEyesStateGhost = NULL; int16_t dist = 0x7FFF; int16_t closestfrightenedDist = 0x7FFF; int16_t closestAttackingDist = 0x7FFF; for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; int16_t d = s->Distance(pacman->cx, pacman->cy); if (d < dist){ dist = d; if (s->state == FrightenedState ) { frightenedGhost = s; closestfrightenedDist = d; } else { closestAttackingGhost = s; closestAttackingDist = d; } closestGhost = s; if ( s->state == DeadEyesState ) DeadEyesStateGhost = s; } } PACMANFALLBACK = 0; if (DEMO == 1 && !DeadEyesStateGhost && frightenedGhost ){ pacman->Target(frightenedGhost->cx, frightenedGhost->cy); return; } // Under threat; just avoid closest ghost if (DEMO == 1 && !DeadEyesStateGhost && dist <= 32 && closestAttackingDist < closestfrightenedDist ) { if (dist <= 16) { pacman->Target( pacman->cx * 2 - closestAttackingGhost->cx, pacman->cy * 2 - closestAttackingGhost->cy); PACMANFALLBACK = 1; } else { pacman->Target( pacman->cx * 2 - closestAttackingGhost->cx, pacman->cy * 2 - closestAttackingGhost->cy); } return; } if (ACTIVEBONUS == 1) { pacman->Target(13, 20); return; } // Go for the pill if (GetDot(1, 6)) pacman->Target(1, 6); else if (GetDot(26, 6)) pacman->Target(26, 6); else if (GetDot(1, 26)) pacman->Target(1, 26); else if (GetDot(26, 26)) pacman->Target(26, 26); else{ // closest dot int16_t dist = 0x7FFF; for (byte y = 4; y < 32; y++){ for (byte x = 1; x < 26; x++){ if (GetDot(x, y)){ int16_t d = pacman->Distance(x, y); if (d < dist){ dist = d; pacman->Target(x, y); } } } } if (dist == 0x7FFF) { GAMEWIN = 1; // No dots, GAME WIN! } } } //-------------------------------------------------------------------------/ void Scatter(Sprite* s){ const byte* st = _scatterTargets + (s->who << 1); s->Target(pgm_read_byte(st), pgm_read_byte(st + 1)); } //-------------------------------------------------------------------------/ void UpdateTargets(){ if (_state == ReadyState) return; PacmanAI(); Sprite* pacman = _sprites + PACMAN; // Ghost AI bool scatter = _scIndex & 1; for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; // Deal with returning ghost to pen if (s->state == DeadEyesState){ if (s->cx == 14 && s->cy == 17){ // returned to pen s->state = PenState; // Revived in pen s->pentimer = 80; } else s->Target(14, 17); // target pen continue; // } // Release ghost from pen when timer expires if (s->pentimer){ if (--s->pentimer) // stay in pen for awhile continue; s->state = RunState; } if (InPen(s->cx, s->cy)){ s->Target(14, 14 - 2); // Get out of pen first } else { if (scatter || s->state == FrightenedState) Scatter(s); else { // Chase mode targeting signed char tx = pacman->cx; signed char ty = pacman->cy; switch (s->who){ case PINKY:{ const char* pto = _pinkyTargetOffset + ((pacman->dir - 1) << 1); tx += pgm_read_byte(pto); ty += pgm_read_byte(pto + 1); } break; case INKY:{ const char* pto = _pinkyTargetOffset + ((pacman->dir - 1) << 1); Sprite* binky = _sprites + BINKY; tx += pgm_read_byte(pto) >> 1; ty += pgm_read_byte(pto + 1) >> 1; tx += tx - binky->cx; ty += ty - binky->cy; } break; case CLYDE:{ if (s->Distance(pacman->cx, pacman->cy) < 64){ const byte* st = _scatterTargets + CLYDE * 2; tx = pgm_read_byte(st); ty = pgm_read_byte(st + 1); } } break; } s->Target(tx, ty); } } } } // Default to current direction byte ChooseDir(int16_t dir, Sprite* s){ int16_t choice[4]; choice[0] = Chase(s, s->cx, s->cy - 1); // Up choice[1] = Chase(s, s->cx - 1, s->cy); // Left choice[2] = Chase(s, s->cx, s->cy + 1); // Down choice[3] = Chase(s, s->cx + 1, s->cy); // Right if (DEMO == 0 && s->who == PACMAN && choice[0] < 0x7FFF && but_UP) dir = MUp; else if (DEMO == 0 && s->who == PACMAN && choice[1] < 0x7FFF && but_LEFT) dir = MLeft; else if (DEMO == 0 && s->who == PACMAN && choice[2] < 0x7FFF && but_DOWN) dir = MDown; else if (DEMO == 0 && s->who == PACMAN && choice[3] < 0x7FFF && but_RIGHT) dir = MRight; else if (DEMO == 0 && choice[0] < 0x7FFF && s->who == PACMAN && dir == MUp) dir = MUp; else if (DEMO == 0 && choice[1] < 0x7FFF && s->who == PACMAN && dir == MLeft) dir = MLeft; else if (DEMO == 0 && choice[2] < 0x7FFF && s->who == PACMAN && dir == MDown) dir = MDown; else if (DEMO == 0 && choice[3] < 0x7FFF && s->who == PACMAN && dir == MRight) dir = MRight; else if ((DEMO == 0 && s->who != PACMAN) || DEMO == 1 ) { // Don't choose opposite of current direction? int16_t dist = choice[4 - dir]; // favor current direction byte opposite = OppositeDirection(dir); for (byte i = 0; i < 4; i++){ byte d = 4 - i; if ((d != opposite && choice[i] < dist) || (s->who == PACMAN && PACMANFALLBACK && choice[i] < dist)) { if (s->who == PACMAN && PACMANFALLBACK) PACMANFALLBACK = 0; dist = choice[i]; dir = d; } } } else { dir = MStopped; } return dir; } bool InPen(byte cx, byte cy){ if (cx <= 10 || cx >= 18) return false; if (cy <= 14 || cy >= 18) return false; return true; } byte GetSpeed(Sprite* s){ if (s->who == PACMAN) return _frightenedTimer ? 90 : 80; if (s->state == FrightenedState) return 40; if (s->state == DeadEyesState) return 100; if (s->cy == 17 && (s->cx <= 5 || s->cx > 20)) return 40; // tunnel return 75; } //-------------------------------------------------------------------------/ void PackmanDied() { // Noooo... PACMAN DIED 😦 if (LIFES <= 0) { GAMEOVER = 1; LEVEL = START_LEVEL; LIFES = START_LIFES; DEMO = 1; Init(); } else { LIFES--; _inited = true; _state = ReadyState; _stateTimer = FPS / 2; _frightenedCount = 0; _frightenedTimer = 0; const byte* s = _initSprites; for (int16_t i = 0; i < 5; i++) _sprites[i].Init(s + i * 5); _scIndex = 0; _scTimer = 1; memset(_icons, 0, sizeof(_icons)); //AND BONUS _BonusSprite.Init(s + 5 * 5); _BonusInactiveTimmer = BONUS_INACTIVE_TIME; _BonusActiveTimmer = 0; for (byte i = 0; i < ACTUALBONUS; i++) { _icons[13 - i] = BONUSICON + i; } for (byte i = 0; i < LIFES; i++) { _icons[0 + i] = PACMANICON; } //Draw LIFE and BONUS Icons for (byte y = 34; y < 36; y++) for (byte x = 0; x < 28; x++) { Draw(x, y, false); } DrawAllBG(); } } //-------------------------------------------------------------------------/ void MoveAll(){ UpdateTimers(); UpdateTargets(); // Update game state if (_stateTimer){ if (--_stateTimer <= 0){ switch (_state){ case ReadyState: _state = PlayState; _dirty[20 * 4 + 1] |= 0x1F; // Clear 'READY!' _dirty[20 * 4 + 2] |= 0x80; for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false); // ReDraw (clear) 'READY' position break; case DeadGhostState: _state = PlayState; for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; if (s->state == DeadNumberState) s->state = DeadEyesState; } break; default: ; } } else { if (_state == ReadyState) return; } } for (byte i = 0; i < 5; i++){ Sprite* s = _sprites + i; // In DeadGhostState, only eyes move if (_state == DeadGhostState && s->state != DeadEyesState) continue; // Calculate speed s->_speed += GetSpeed(s); if (s->_speed < 100) continue; s->_speed -= 100; s->lastx = s->_x; s->lasty = s->_y; s->phase++; int16_t x = s->_x; int16_t y = s->_y; if ((x & 0x7) == 0 && (y & 0x7) == 0) // cell aligned s->dir = ChooseDir(s->dir, s); // time to choose another direction switch (s->dir){ case MLeft: x -= SPEED; break; case MRight: x += SPEED; break; case MUp: y -= SPEED; break; case MDown: y += SPEED; break; case MStopped: break; } // Wrap x because of tunnels while (x < 0) x += 224; while (x >= 224) x -= 224; s->_x = x; s->_y = y; s->cx = (x + 4) >> 3; s->cy = (y + 4) >> 3; if (s->who == PACMAN) EatDot(s->cx, s->cy); } // Collide Sprite* pacman = _sprites + PACMAN; // Collide with BONUS Sprite* _s = &_BonusSprite; if (ACTIVEBONUS == 1 && _s->cx == pacman->cx && _s->cy == pacman->cy) { Score(ACTUALBONUS * 50); ACTUALBONUS++; if (ACTUALBONUS > 7) { ACTUALBONUS = 0; if (LIFES < MAXLIFES) LIFES++; //reset all icons memset(_icons, 0, sizeof(_icons)); for (byte i = 0; i < LIFES; i++) { _icons[0 + i] = PACMANICON; } } for (byte i = 0; i < ACTUALBONUS; i++) { _icons[13 - i] = BONUSICON + i; } //REDRAW LIFE and BONUS icons for (byte y = 34; y < 36; y++) for (byte x = 0; x < 28; x++) { Draw(x, y, false); } ACTIVEBONUS = 0; _BonusInactiveTimmer = BONUS_INACTIVE_TIME; } for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; //if (s->cx == pacman->cx && s->cy == pacman->cy) if (s->_x + SPEED >= pacman->_x && s->_x - SPEED <= pacman->_x && s->_y + SPEED >= pacman->_y && s->_y - SPEED <= pacman->_y) { if (s->state == FrightenedState) { s->state = DeadNumberState; // Killed a ghost _frightenedCount++; _state = DeadGhostState; _stateTimer = 10; Score((1 << _frightenedCount) * 100); } else { // pacman died if (s->state == DeadNumberState || s->state == FrightenedState || s->state == DeadEyesState) { } else { PackmanDied(); } } } } } //-------------------------------------------------------------------------/ // Mark a position dirty void Mark(int16_t pos){ for (byte tmp = 0; tmp < 28; tmp++) updateMap[1][tmp] = true; } //-------------------------------------------------------------------------/ void SetScoreChar(byte i, signed char c){ if (_scoreStr[i] == c) return; _scoreStr[i] = c; Mark(i + 32); //Score //Mark(i+32+10); //HiScore } //-------------------------------------------------------------------------/ void SetHiScoreChar(byte i, signed char c){ if (_hiscoreStr[i] == c) return; _hiscoreStr[i] = c; //Mark(i+32); //Score Mark(i + 32 + 10); //HiScore } //-------------------------------------------------------------------------/ void Score(int16_t delta){ char str[8]; _score += delta; if (DEMO == 0 && _score > _hiscore) _hiscore = _score; if (_score > _lifescore && _score % 10000 > 0) { _lifescore = (_score / 10000 + 1) * 10000; LIFES++; // EVERY 10000 points = 1UP for (byte i = 0; i < LIFES; i++) { _icons[0 + i] = PACMANICON; } //REDRAW LIFE and BONUS icons for (byte y = 34; y < 36; y++) for (byte x = 0; x < 28; x++) { Draw(x, y, false); } _score = _score + 100; } sprintf(str, "%ld", _score); byte i = 7 - strlen(str); byte j = 0; while (i < 7) SetScoreChar(i++, str[j++]); sprintf(str, "%ld", _hiscore); i = 7 - strlen(str); j = 0; while (i < 7) SetHiScoreChar(i++, str[j++]); } bool GetDot(byte cx, byte cy){ return _dotMap[(cy - 3) * 4 + (cx >> 3)] & (0x80 >> (cx & 7)); } void EatDot(byte cx, byte cy){ if (!GetDot(cx, cy)) return; byte mask = 0x80 >> (cx & 7); _dotMap[(cy - 3) * 4 + (cx >> 3)] &= ~mask; byte t = GetTile(cx, cy); if (t == PILL){ _frightenedTimer = 10 * FPS; _frightenedCount = 0; for (byte i = 0; i < 4; i++){ Sprite* s = _sprites + i; if (s->state == RunState){ s->state = FrightenedState; s->dir = OppositeDirection(s->dir); } } Score(50); } else Score(10); } //-------------------------------------------------------------------------/ void Init(){ if (GAMEWIN == 1) { GAMEWIN = 0; } else { LEVEL = START_LEVEL; LIFES = START_LIFES; ACTUALBONUS = 0; //actual bonus icon ACTIVEBONUS = 0; //status of bonus _score = 0; _lifescore = 10000; memset(_scoreStr, 0, sizeof(_scoreStr)); _scoreStr[5] = _scoreStr[6] = '0'; } _inited = true; _state = ReadyState; _stateTimer = FPS / 2; _frightenedCount = 0; _frightenedTimer = 0; const byte* s = _initSprites; for (int16_t i = 0; i < 5; i++) _sprites[i].Init(s + i * 5); //AND BONUS _BonusSprite.Init(s + 5 * 5); _BonusInactiveTimmer = BONUS_INACTIVE_TIME; _BonusActiveTimmer = 0; _scIndex = 0; _scTimer = 1; memset(_icons, 0, sizeof(_icons)); // SET BONUS icons for (byte i = 0; i < ACTUALBONUS; i++) { _icons[13 - i] = BONUSICON + i; } // SET Lifes icons for (byte i = 0; i < LIFES; i++) { _icons[0 + i] = PACMANICON; } //Draw LIFE and BONUS Icons for (byte y = 34; y < 36; y++) for (byte x = 0; x < 28; x++) { Draw(x, y, false); } // Init dots from rom memset(_dotMap, 0, sizeof(_dotMap)); byte* map = _dotMap; for (byte y = 3; y < 36 - 3; y++){ // 30 interior lines for (byte x = 0; x < 28; x++){ byte t = GetTile(x, y); if (t == 7 || t == 14){ byte s = x & 7; map[x >> 3] |= (0x80 >> s); } } map += 4; } DrawAllBG(); } //-------------------------------------------------------------------------/ void Step(){ int16_t keys = 0; if (GAMEWIN == 1) { LEVEL++; Init(); } // Start GAME if (but_A && DEMO == 1 && GAMEPAUSED == 0) { but_A = false; DEMO = 0; Init(); } else if (but_A && DEMO == 0 && GAMEPAUSED == 0) { // Or PAUSE GAME but_A = false; GAMEPAUSED = 1; } if (GAMEPAUSED && but_A && DEMO == 0) { but_A = false; GAMEPAUSED = 0; for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false); } // Reset / Start GAME if (but_B) { DEMO = 0; Init(); } else if (!_inited) { DEMO = 1; Init(); } // Create a bitmap of dirty tiles byte m[(32 / 8) * 36]; // 144 bytes memset(m, 0, sizeof(m)); _dirty = m; if (!GAMEPAUSED) MoveAll(); // IF GAME is PAUSED STOP ALL if ((ACTIVEBONUS == 0 && DEMO == 1) || GAMEPAUSED == 1 ) for (byte tmpX = 11; tmpX < 17; tmpX++) Draw(tmpX, 20, false); // Draw 'PAUSED' or 'DEMO' text DrawAll(); } }; /******************************************************************************/ /* SETUP */ /******************************************************************************/ #define BLACK 0x0000 // 16bit BLACK Color void setup() { randomSeed(analogRead(0)); M5.begin(); Wire.begin(); if(digitalRead(BUTTON_A_PIN) == 0){ delay(100);updateFromFS(SD);ESP.restart(); } M5.Lcd.setRotation(2); M5.Lcd.fillScreen(TFT_BLACK); pinMode(5, INPUT_PULLUP); delay(100); } /******************************************************************************/ /* LOOP */ /******************************************************************************/ void ClearKeys() { but_A =false; but_B =false; but_UP =false; but_DOWN =false; but_LEFT =false; but_RIGHT=false; } void KeyPadLoop(){ char r; if(digitalRead(5) == LOW) { Wire.requestFrom(0X88, 1); // request 1 byte from keyboard while (Wire.available()) { uint8_t key_val = Wire.read(); // receive a byte as character if ( key_val == 127) { r = 'x';} // start if ( key_val == 191) { r = 'z';} // select if ( key_val == 254) { r = '4';} // left if ( key_val == 253) { r = '6';} // right if ( key_val == 251) { r = '2';} // down if ( key_val == 247) { r = '8';} // up } } if (r == 'z') { ClearKeys(); but_A=true; delay(300); } //else but_A=false; if (r == 'x') { ClearKeys(); but_B=true; delay(300); } //else but_B=false; if (r == '8') { ClearKeys(); but_UP=true; } //else but_UP=false; if (r == '2') { ClearKeys(); but_DOWN=true; } //else but_DOWN=false; if (r == '4') { ClearKeys(); but_LEFT=true; } //else but_LEFT=false; if (r == '6') { ClearKeys(); but_RIGHT=true; } //else but_RIGHT=false; } Playfield _game; void loop() { _game.Step(); KeyPadLoop(); }
[…] 「おー!パックマン移植しとる人おるやん!」 【PACMAN with M5Stack】 […]
M5Stack Mini Arcade【前半】 | Makership Inc.
10月 26, 2018 at 8:58 pm
Makershipさん、サイトの訪問をありがとうございます。
M5Stack Mini Arcade を拝見しています。素晴らしい出来ですね。
私も作りたかったので見て楽しんでいます。
M5の命令は時々 変わる場合があり M5のバージョンによって再検討が必要かと思います。
M5Stack Mini Arcadeの記事は 新規に変更方法を含み 丁寧な解説は解りやすく助かっています。
macsbug
10月 30, 2018 at 4:44 pm
[…] PACMAN with M5STACK : macsbug https://macsbug.wordpress.com/2018/03/07/pacman-with-m5stack/ […]
1/24ゲーム筐体コレクション(テーブル型) + 0.85インチ液晶 – しかるのち
8月 14, 2023 at 3:25 pm
しかるのち さん
1/24ゲーム筐体コレクション(テーブル型) + 0.85インチ液晶
https://shikarunochi.matrix.jp/?p=5137
じっくり見させて頂きました。
確かに 0.85インチ はあり128×128に良く入りましたね。
この大きさに収めた技量に感服致します。
テーブルの造り込みにも驚くばかりです。
macsbug
8月 14, 2023 at 3:53 pm