ATtiny85ミニアーケード:スネークゲーム - 手のひらサイズOLEDゲーム機の製作
必要な部品と材料
| Microchip ATtiny85 | ×1 |
| グラフィックOLED 128×64 | ×1 |
| SparkFun microB USBブレークアウト | ×1 |
| Arduino Nano R3(プログラミング用) | ×1 |
必要な工具と機械
- はんだごて
- 3Dプリンター
使用するアプリとオンラインサービス
- Autodesk Fusion 360
- Arduino IDE
プロジェクト概要
着想と過去のプロジェクト
2017年12月、Arduino NanoとOLED画面、2ボタンで携帯型Pongコンソールを作りました。
当時は良かったものの、コンソールが大型で扱いにくかったです。最近、過去のプロジェクトを再構築しようと考え、今回はスネークゲームが遊べる極小コンソールを目指しました。
採用コンポーネント
小型化のためArduino開発ボードは不使用。ATtiny85を採用しました。
ゲーム運用に十分なI/Oを備え、2つのADCピン、I2Cポート、GPIOピンを持ちます。操作にはシンプルな2軸ジョイスティック/スイッチモジュールを採用。信号用に3ピンのみで接続簡単です。
ディスプレイはDFRobotの128×64 OLEDを選択。小型ながら十分な解像度です。
システムのハンダ付け
ATtiny85をSOP-8 to DIP-8変換基板に取り付け、小さなパーフボードにハンダ付け。プログラミング用と画面用の2つのヘッダーを実装。アナログジョイスティックのピンをパーフボードにハンダ付けして配線。最後にmicro USBブレークアウトボードを電源用に配線してシステム完成。
エンクロージャの設計
Fusion 360で設計。実装部品を配置し、それらを囲むエンクロージャを作成。
80年代のアーケード筐体を模しつつ、極小サイズを維持。以下はレンダリング画像:
ゲームのプログラミング
スネークゲームのプログラミングは比較的容易。RAM節約のため最大蛇長を30に設定(餌29個でクリア)。蛇の各セグメント座標を格納する2次元配列で管理します。
頭が新位置へ移動すると、前の位置が順次下位セグメントへ伝播。餌を食べるとランダム位置に新セグメント生成。衝突判定は全セグメント座標を走査し頭座標と一致するか確認。壁衝突もゲームオーバー条件です。
スネークゲームをプレイ
電源投入後、画面に餌と蛇の初期セグメントが表示されるのを待ち、ジョイスティックで蛇を操作して餌を食べさせます。手のひらサイズで楽しめる優れた暇つぶしになります。
コード
- ATTiny85 Code
ATTiny85 Code C/C++
事前にU8g2libのインストールが必要
//#include <TinyWireM.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g(U8G2_R0, /* clock=*/ 2, /* data=*/ 0, /* reset=*/ U8X8_PIN_NONE);
#define MAX_LENGTH 30
#define X 0
#define Y 1
#define JOYSTICK_X 2
#define JOYSTICK_Y 3
#define DIR_THRESH 300
uint8_t segmentPositions[MAX_LENGTH][2];
uint8_t headPosition[2] = {63, 31};
uint8_t foodPosition[2];
uint8_t tempPosition0[2];
uint8_t tempPosition1[2];
int segmentLength = 1;
void gameUpdate();
enum DIRECTIONS { RIGHT, DOWN, LEFT, UP } currentDirection;
void setup() {
//TinyWireM.begin();
u8g.begin();
u8g.setPowerSave(0);
pinMode(JOYSTICK_X, INPUT);
pinMode(JOYSTICK_Y, INPUT);
randomSeed(analogRead(0));
beginGame();
}
void loop() {
u8g.firstPage();
do {
gameUpdate();
u8g.setColorIndex(1);
} while(u8g.nextPage());
}
void beginGame(){
currentDirection = RIGHT;
spawnFood();
delay(1000);
}
bool checkCollisions(){
for(int i=1; i<segmentLength; i++){
if(segmentPositions[i][X] == headPosition[X]){
if(segmentPositions[i][Y] == headPosition[Y]){
return 1;
}
}
}
if(headPosition[X] <= 0 || headPosition[X] >= 128) return 1;
else if(headPosition[Y] <= 0 || headPosition[Y] >= 64) return 1;
return false;
}
void spawnFood(){
int randomX = random(5, 123);
int randomY = random(5, 60);
foodPosition[X] = randomX;
foodPosition[Y] = randomY;
}
void checkFoodEaten(){
if(headPosition[X] == foodPosition[X] || headPosition[Y] == foodPosition[Y]){
segmentLength += 1;
spawnFood();
}
}
void updateDirection(){
int joy_x_val = analogRead(JOYSTICK_X);
int joy_y_val = analogRead(JOYSTICK_Y);
if(joy_x_val <= DIR_THRESH) currentDirection = LEFT;
else if(joy_x_val >= 1023-DIR_THRESH) currentDirection = RIGHT;
else if(joy_y_val <= DIR_THRESH) currentDirection = UP;
else if(joy_y_val >= 1023-DIR_THRESH) currentDirection = DOWN;
}
void displaySegments(){
for(int segment=0; segment < segmentLength; segment++){
u8g.drawPixel(segmentPositions[segment][X], segmentPositions[segment][Y]);
}
u8g.drawPixel(foodPosition[X], foodPosition[Y]);
}
void gameUpdate(){
updateDirection();
tempPosition0[X] = headPosition[X];
tempPosition0[Y] = headPosition[Y];
switch(currentDirection){
case UP: headPosition[Y] -= 1; break;
case DOWN: headPosition[Y] += 1; break;
case LEFT: headPosition[X] -= 1; break;
case RIGHT: headPosition[X] += 1; break;
}
for(int segment=1; segment < segmentLength; segment+= 2){
tempPosition1[X] = segmentPositions[segment][X];
tempPosition1[Y] = segmentPositions[segment][Y];
segmentPositions[segment][X] = tempPosition0[X];
segmentPositions[segment][Y] = tempPosition0[Y];
tempPosition0[X] = segmentPositions[segment+1][X];
tempPosition0[Y] = segmentPositions[segment+1][Y];
segmentPositions[segment+1][X] = tempPosition1[X];
segmentPositions[segment+1][Y] = tempPosition1[Y];
}
displaySegments();
checkFoodEaten();
if(checkCollisions()) endGame();
if(segmentLength>= MAX_LENGTH) endGame();
delay(50);
}
void endGame(){
segmentLength = 1;
headPosition[0] = 63;
headPosition[1] = 31;
beginGame();
}
カスタムパーツとエンクロージャ
回路図

製造プロセス
- TinyML言語検出器:Edge ImpulseとArduino Nano 33 BLE Senseで「はい」を多言語判別
- Arduino NanoとMPU-6050で作るリアルタイムジャイロスコープゲーム
- Arduinoデジタルダイス: LCD表示の乱数ジェネレーター自作
- Arduino Nanoで37LEDルーレットゲームを自作:ヨーロピアンスタイル完全再現
- ポータブル距離検出器:Arduino Nano自作キット(HC-SR04、18650電源、3Dプリントケース付き)
- Arduino Nanoで作る音声反応デスクライト:音楽に合わせて光るイルミネーション
- 銅メッキプロジェクト:Arduino UNOで作るスマート電気メッキシステム
- Arduino Nanoで作るNeoPixelマトリックス版Pongゲーム
- Arduinoシフトレジスタで美しい光のシーケンスを作成
- Arduino 製スマートプラグ - 120V 対応、RTC 内蔵で時刻制御可能なインテリジェントコンセント
- Arduinoで作るセキュリティシステム:超音波センサーとサーボで広範囲監視・アラート