Arduinoテニスゲーム:NeoPixelマトリクス・9DoFセンサー・Bluetoothで作るバーチャルラケット体験
プロジェクト概要
15×10のNeoPixelマトリクスを画面とし、9DoFセンサー(LSM9DS1)内蔵の自作ラケットでプレイするテニスゲームです。Wii Sportsテニスのような「奥行き感」をLEDの大きさと位置で表現し、Bluetooth経由でラケットのスイングを検知してラリーを楽しめます。Pythonスクリプトがブリッジ役を務め、2台のArduino Nanoを連携させます。
システム構成
| パート | ハードウェア | 役割 |
|---|---|---|
| ディスプレイ | Arduino Nano + 15×10 NeoPixelマトリクス | ゲームロジック・描画・スコア管理・EEPROMハイスコア保存 |
| コントローラ | Arduino Nano + SparkFun 9DoF Sensor Stick + HC-05 Bluetooth | 加速度検知(±1.5G閾値)でスイング判定、'1'を送信 |
| ブリッジ | PC上のPythonスクリプト | Bluetooth COMポート↔マトリクスCOMポートを中継、1秒デバウンス |
パート1:ディスプレイ(ゲームボード)
ゲームルール
- ボールはマトリクス上部(Y=2)から出現、下方向(Y増)に移動しながら半径拡大=「こちらへ飛んでくる」演出
- Y=5〜6の手前でスイング検知('1'受信)すると反転、Y減方向・半径縮小で「相手へ返す」
- ヒットごとに左右方向反転(ランダム要素あり)
- プレイヤーがミス(Y≥8到達)→ COM得点。COMがミス(25%確率)→ プレイヤー得点
- COMが10点先取でゲームオーバー
- ハイスコアはI2C EEPROM(アドレス0x50、オフセット0x04)に不揮発保存
マトリクス製作
150個のNeoPixelをジグザグ配線で15列×10行に配置。詳細な組み立てガイドはこちら参照。
パート2:コントローラ(ラケット)
ハードウェア
- Arduino Nano(3.3V動作推奨、またはレギュレータ経由で9V電池をVinへ)
- SparkFun 9DoF Sensor Stick(LSM9DS1、I2C、3.3V専用)
- HC-05 Bluetoothモジュール(シリアル透過)
- 9V電池(Vinピン接続必須!RAW/5Vピンに直結するとレギュレータ破損)
- 段ボールでラケット形状を切り抜き、白紙にデザイン描画して装飾
スイング検知ロジック
10ms間隔でX/Y軸加速度を読み、絶対値が1.5Gを超えたらスイングと判定、シリアルで 1 を送信。600msのクールダウンで連打防止。
Bluetooth通信とPythonブリッジ
ラケットとディスプレイは直接通信しません。PC上のPythonが両シリアルポートを開き、ラケットからの 1 を受信したらディスプレイ側へ 1 を転送。両Arduinoのリセットタイミング同期も兼ねます。
import serial
import time
matrix_port = "COM3" # ディスプレイ側Arduino
raquet_port = "COM9" # ラケット側Bluetooth
matrix = serial.Serial(matrix_port, 9600)
racquet = serial.Serial(raquet_port, 9600)
time.sleep(10) # 両Arduinoの起動待ち
while True:
data = racquet.read()
print(data)
if data == "1":
print("hit")
matrix.write("1")
time.sleep(1) # デバウンス
time.sleep(0.05)
ラケット製作フォトガイド
ソースコード
Matrix Code(ディスプレイ側Arduino Nano)
#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <EEPROM.h>
#define PIN 6
#define EEPROM_ADR 0x50
#define HS_ADR 0x02
#define NOTE_C1 33
#define SPKR_PIN 3
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(15, 10, PIN,
NEO_MATRIX_BOTTOM + NEO_MATRIX_LEFT +
NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG,
NEO_GRB + NEO_KHZ800);
int ball_x = 7, ball_y = 2, radius = 1, r_incr = 1;
bool isSwung = false;
int ball_x_dir = 1, ball_y_dir = 1;
bool isDirRight = true;
int score = 0, COM_score = 0, highscore = 0;
int framerate = 50;
int serial_data;
String score_string = "";
uint16_t colors[] = {
matrix.Color(255,0,0), matrix.Color(0,255,0), matrix.Color(150,200,0)
};
static unsigned long lastFrame = 0;
void setup() {
Serial.begin(9600);
matrix.begin();
matrix.fillScreen(0);
matrix.setTextColor(colors[1]);
randomSeed(analogRead(A2));
display_scores();
highscore = read_HS();
matrix.setCursor(0,1); matrix.print("HS: "); matrix.show(); delay(1000);
matrix.fillScreen(0); matrix.setCursor(0,1); matrix.print(highscore); matrix.show(); delay(1000);
score_string = "";
}
void loop() {
if ((lastFrame + framerate) < millis()) {
update_frame();
lastFrame = millis();
}
}
void update_frame() {
serial_data = Serial.parseInt();
isSwung = (serial_data != 0);
if (ball_y >= 5 && ball_y < 7 && isSwung) {
if (isDirRight) { ball_x_dir = -1; ball_y_dir = -1; }
else { ball_x_dir = 1; ball_y_dir = -1; }
r_incr = -1;
}
else if (ball_y >= 8) {
COM_score++; end_round();
}
else if (ball_y <= 2) {
isDirRight = !isDirRight;
int randNum = random(4);
if (randNum == 2) { // 25% COMミス
score++;
if (score > highscore) write_HS();
end_round();
} else {
if (isDirRight) { ball_x_dir = 1; ball_y_dir = 1; }
else { ball_x_dir = -1; ball_y_dir = 1; }
r_incr = 1;
}
}
ball_x += ball_x_dir;
ball_y += ball_y_dir;
radius += r_incr;
matrix.fillScreen(0);
matrix.fillCircle(ball_x, ball_y, radius, colors[2]);
matrix.show();
}
void end_round() {
if (COM_score >= 10) end_game();
isDirRight = true; r_incr = 1;
ball_x_dir = 1; ball_y_dir = 1;
ball_x = 7; ball_y = 2; radius = 1;
display_scores();
matrix.fillScreen(0);
matrix.fillCircle(ball_x, ball_y, radius, colors[2]);
matrix.show();
}
void end_game() {
matrix.fillScreen(0);
matrix.setCursor(0,1);
matrix.setTextColor(colors[0]);
matrix.drawLine(3,0,12,9,colors[0]);
matrix.drawLine(11,0,2,9,colors[0]);
matrix.show();
delay(500);
while(1); // 停止
}
void display_scores() {
matrix.fillScreen(0);
matrix.setTextColor(colors[1]);
matrix.setCursor(0,1);
score_string = String(score) + "-" + String(COM_score);
scrollText(score_string);
matrix.fillScreen(0);
delay(2000);
matrix.show();
}
void scrollText(String text) {
int x = matrix.width();
for (int i=0; i<24; i++) {
matrix.fillScreen(0);
matrix.setCursor(x,2);
matrix.print(text);
x--;
matrix.show();
delay(150);
}
}
void write_HS() { EEPROM.write(0x04, (int)score); }
int read_HS() { return EEPROM.read(0x04); }
Racquet Code(ラケット側Arduino Nano)
#include <Wire.h>
#include <SPI.h>
#include <SparkFunLSM9DS1.h>
LSM9DS1 imu;
#define LSM9DS1_M 0x1E
#define LSM9DS1_AG 0x6B
#define PRINT_SPEED 10
static unsigned long lastPrint = 0;
float accelx = 0, accely = 0;
void setup() {
Serial.begin(9600);
imu.settings.device.commInterface = IMU_MODE_I2C;
imu.settings.device.mAddress = LSM9DS1_M;
imu.settings.device.agAddress = LSM9DS1_AG;
if (!imu.begin()) { while(1); }
}
void loop() {
if ((lastPrint + PRINT_SPEED) < millis()) {
if (imu.accelAvailable()) imu.readAccel();
lastPrint = millis();
}
accelx = imu.calcAccel(imu.ax);
accely = imu.calcAccel(imu.ay);
if (accelx <= -1.5 || accelx >= 1.5) {
Serial.print(1); delay(600);
} else if (accely <= -1.5 || accely >= 1.5) {
Serial.print(1); delay(600);
}
}
Python Bridge Code
import serial, time
matrix_port = "COM3"
raquet_port = "COM9"
matrix = serial.Serial(matrix_port, 9600)
racquet = serial.Serial(raquet_port, 9600)
time.sleep(10)
while 1:
data = racquet.read()
print(data)
if data == "1":
print("hit")
matrix.write("1")
time.sleep(1)
time.sleep(.05)
回路図
まとめ
NeoPixelマトリクスとIMUセンサー、Bluetoothを組み合わせたレトロフューチャーなテニスゲーム。ハードウェア製作(マトリクス配線・ラケット工作)に手間はかかりますが、完成時の達成感と遊びごたえは格別です。Pythonブリッジを介した2マイコン協調動作の勉強にもなります。ぜひ挑戦してみてください!
製造プロセス
- TinyML言語検出器:Edge ImpulseとArduino Nano 33 BLE Senseで「はい」を多言語判別
- Arduino NanoとMPU-6050で作るリアルタイムジャイロスコープゲーム
- Arduinoデジタルダイス: LCD表示の乱数ジェネレーター自作
- Arduinoでアイアンマンを自作:部品・センサー・手順ガイド
- ArduinoとBluetoothで作るスマート紛失防止タグ「Find Me」
- DIY Arduino加湿器コントローラー:リレーを用いた安全な高電圧制御
- Arduino超音波センサーとProcessingで作るリアルタイム距離測定ソナー
- MobBob:Arduino製ロボットをAndroidスマホでシームレスに制御
- Arduino Nanoで作る音声反応デスクライト:音楽に合わせて光るイルミネーション
- Arduino Nanoで作るNeoPixelマトリックス版Pongゲーム
- ゲーム用カスタムArduinoジョイスティックハンドルの製作