LoginSignup
7
0

More than 3 years have passed since last update.

M5StickVで音ゲーをプレイしてみよう

Last updated at Posted at 2020-05-23

以前M5Stackで画面をタップするものを作り、

M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061

色認識してターゲットを追跡するものを作ったので、

RoverCとM5StickCとM5StickVでグリッパロボットを作る - Qiita
https://qiita.com/coppercele/items/2c4e6c84a7a96e348dc1

これなら音ゲーをプレイするものが作れるんじゃないか?と思ったので作ってみました

ミリシタはレーンが2個しかないモードがあるのでうってつけでした

ミリシタでレッツリズム!

アイドルマスター ミリオンライブ! シアターデイズ | バンダイナムコエンターテインメント公式サイト
https://millionlive.idolmaster.jp/theaterdays/

ちなみにミリシタの仕様として
・ホールドは途中で離していい
・ホールドの最後はタップでもリリースでもどっちでもいい
という感じになっているのでピンクの玉をタップすればフリック以外は取れます

ハードウェアの構築

M5stack
M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061

こちらで使ったものをそのまま使用します

M5StickV
音ゲーの画面をカメラに映すためレゴでやぐらを組んで設置します
M5StickVを固定している透明の部品は以前3Dプリンタで作ったホルダーを流用しました

image.png
image.png

上手いことこういう感じに映るように調整します

image.png
※白と緑の四角は画面上の座標を確認するため

M5StickVとM5StackをGroveケーブルで直結します
ちなみにM5StackのGroveは21,22を使うのでリレータッチボードを21に接続してると使えなくなります(Groveが優先)
上側のGPIOは22,23,19,18は他の機能と兼用されてるので避けないといけないので左側のGPIO16に接続しました

上手いこと調整できたら歌織さんの胸ノーツを叩くポイントにリレータッチボードを貼り付けてセッティングします

image.png

ソフトウェアの構築

M5StickVとM5Stack間はUARTで通信します

M5SticV側ソース(送信のみ)

from fpioa_manager import fm
from machine import UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)

data_packet = bytearray([0, 0])
uart_Port.write(data_packet)

M5Stack側(受信のみ)

void setup() {
  Serial1.begin(115200, SERIAL_8N1, 21, 22);
}


void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        for (int i = 0; i < rx_size; i++) {
            Serial.printf("%d, ", rx_buffer[i]);
        }
        Serial.printf("\n");
    }
  delay(1);
}

ノーツを認識する

こちらを参考にして閾値エディタでピンクを認識できるようにします

【キョロキョロV④】m5StickV サーボ2軸マシンでまずは色検出をしてみました - パスコンパスの日記
https://yoichi-41.hatenablog.com/entry/2019/09/15/204835

image.png

また、色認識するには周囲の環境の影響が大きいです
昼光色、電球色、自然光や画面に表示してるもの明るさによってカメラの露出などが変わってしまいさっきまで認識できていたのに認識できなくなってしまうということがよく起きます
なのでなるべくカメラの影響が少なくなるように設定します


sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0) #カメラの明るさ
sensor.set_saturation(0) #カメラの飽和度?
sensor.set_contrast(0)   #カメラのコントラスト
sensor.run(1)

それぞれ-2~2の間で設定できるので自分の環境に合わせて調整しましょう

ちゃんと設定するとこんな感じで認識できるようになります
また、PerfectのPの赤に反応したりするので認識する部分を緑の部分だけに限定します

red_threshold   = (88, 54, 21, 93, -6, 60)

img=sensor.snapshot()
blobs = img.find_blobs([red_threshold])
    if blobs:
        for blob in reversed(blobs):
            #認識したノーツを逆順に処理する
            if (blob[2] * blob[3] < 200):
                #幅*高さで面積を出し小さいものは除外
                continue
            if  160 < blob[6]  :
                #緑枠の下限より下の物は除外
                #blob[6]は認識領域の中心y座標
                continue

            if  blob[6]  < 180 and blob[5] < 90 or  230 < blob[5] :
                #緑領域の中か判定
                #blob[5]は認識領域の中心x座標
                #四角と十字を描画
                tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
                tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
                c=img.get_pixel(blob[5], blob[6])
                if blob[5] < 160:
                    #左側ならM5Stackに0を送信
                    data_packet = bytearray([0])
                else:
                    #右側ならM5Stackに1を送信
                    data_packet = bytearray([1])
                uart_Port.write(data_packet)
                break #タイミングラインに近い一番後ろの要素を処理したら終了

音ゲーである以上ノーツを認識してからいい感じのタイミングでタップしないとミスになってしまいます

タイミングラインの近くで認識したら短く、遠くで認識されたら長くsleepを入れます


                if blob[5] < 160:
                    #左側ならM5Stackに0を送信
                    data_packet = bytearray([0])
                else:
                    #右側ならM5Stackに1を送信
                    data_packet = bytearray([1])
                # 画面の1番上なら0.6秒、タイミングラインに近ければ0になるようにwaitを計算する
                wait =  (160 - blob[6]) / 160 * 0.6
                print("wait=" + str(wait))
                #待ってから送信
                time.sleep(wait)
                uart_Port.write(data_packet)
                break #タイミングラインに近い一番後ろの要素を処理したら終了

M5Stack側では受信した内容に合わせて画面をタップします


void setup() {
  M5.begin();
  pinMode(26, OUTPUT);
  pinMode(16, OUTPUT);
  digitalWrite(26, LOW);
  digitalWrite(16, LOW);

}

void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        if (rx_buffer[0] == 0x00) {
          // 0なら左タップ
            Serial.printf("left\n");
            // 左
            digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
        }
        else {
          // 0以外なら右タップ
            Serial.printf("right\n");
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);
        }
    }

音ゲーをプレイしてみよう

いい感じで構成してからミリシタのリハーサルで試してみます(通常プレイするとBANされるかも知れない)

このツイートでは9割と言ってますがフリック以外フルコン取れるようになってます

ソース
M5SticV側

m5sticv
import sensor
import image
import lcd
import time
from fpioa_manager import fm
from machine import UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)

lcd.init()
lcd.rotation(2)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0)
sensor.set_saturation(0)
sensor.set_contrast(0)
sensor.run(1)
red_threshold   =            (88, 54, 21, 93, -6, 60)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)

while True:
    img=sensor.snapshot()
    img.draw_rectangle(140, 0, 40, 40, color=(255,255,255))
    img.draw_rectangle(100, 40, 120, 40, color=(255,255,255))
    img.draw_rectangle(90, 80, 140, 40, color=(255,255,255))
    img.draw_rectangle(90, 120, 140, 40, color=(255,255,255))
    img.draw_rectangle(90, 160, 140  , 40, color=(255,255,255))
    img.draw_rectangle(0, 0, 90  , 180, color=(0,255,0))
    img.draw_rectangle(230, 0, 90  , 180, color=(0,255,0))


    blobs = img.find_blobs([red_threshold])
    if blobs:
        for blob in reversed(blobs):
            if (blob[2] * blob[3] < 200):
                continue
            if  160 < blob[6]  :
                continue

            if  blob[6]  < 180 and blob[5] < 90 or  230 < blob[5] :


                tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
                tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
                c=img.get_pixel(blob[5], blob[6])
                if blob[5] < 160:
                    data_packet = bytearray([0])
                else:
                    data_packet = bytearray([1])
                wait =  (160 - blob[6]) / 160 * 0.6
                print("wait=" + str(wait))
                time.sleep(wait)
                uart_Port.write(data_packet)
                break

    lcd.display(img)


M5Stack側

M5Stack
#include <M5Stack.h>

// タッチの間隔
int milli = 500;
// スイッチ連打状態かのフラグ
bool state = false;

void drawScreen() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0,0);
  // 間隔を表示
  M5.Lcd.printf("%4dms",milli);

  M5.Lcd.setCursor(0,220);
  // 画面下部の表示
  M5.Lcd.printf("     -      %s      +", state ? "OFF" : "ON ");
}

void setup() {
 Serial.begin(115200);
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setBrightness(64);
  M5.Lcd.fillScreen(BLACK);
  pinMode(26, OUTPUT);
  pinMode(16, OUTPUT);
  digitalWrite(26, LOW);
  digitalWrite(16, LOW);
  drawScreen();
  Serial1.begin(115200, SERIAL_8N1, 21, 22);

}

void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        for (int i = 0; i < rx_size; i++) {
            Serial.printf("%d, ", rx_buffer[i]);
        }
        Serial.printf("\n");
        if (rx_buffer[0] == 0x00) {
          if(state) {
            Serial.printf("left\n");
            // 左
            digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
          }
        }
        else {
          if(state) {
            Serial.printf("right\n");
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);
          }
        }
    }
  if ( M5.BtnA.wasPressed() ) {
             digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
  }
  if ( M5.BtnB.wasPressed() ) {
    state = !state;
    drawScreen();
  }
  if ( M5.BtnC.wasPressed() ) {
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);

  }
  delay(1);
}


7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0