ベイブレイラウザーをつくる

今回は『ベイブレード』 x 『仮面ライダー剣(ブレイド)』で『ベイブレイラウザー』です。言いたかっただけです。

開発背景

長男もそこそこ大きくなってきて、遊べる玩具が増えてきたこともあり、私の兄が長男の誕生日にベイブレードをプレゼントしてくれました。

これ。

ベイブレードについてはレオトイさんやためにならない!!さんの動画でよく見てはいましたが、実物を触ったのはこれが初めてです。長男の主な対戦相手として私も結構遊んでいるので、せっかくなのでベイブレードを題材に何か作れないかな、というのをぼんやり考え始めました。

とはいえ、現役バリバリ(?)、大人気の競技スポーツ玩具なので、あんまり変なことはしない方が良いなあという思いはありました。方々にご迷惑はかけないように。でも面白いものにはしたい。

このあたりから考えた結果、まず『ベイブレード』そのものと、打ち出すための『ランチャー』。この二つには手を出さないことにしました。ここを守ればベイブレードの競技性に水を差すことはないだろう、という判断です。そうなると残るは、『ベイバトルパス』。

スマホアプリと連携して遊びの幅を広げるためのアイテム。これも、ベイバトルパスそのものをハックするようなものは良くないですが、同じような立ち位置の、アタッチメント的な別アイテムなら良いのでは、と思いました。

ベイバトルパスの基本機能はシュートパワー(以下SP)の計測です。とりあえずその仕組みを理解するところから始めることにしました。そして、アプリ連携不要でその場でSPがわかるようなものができれば、とりあえず実用性はありそうかなと思いました。例えば7セグでデジタル表示するようなイメージ。

そういえば、ライダー玩具でも、7セグ表示のあるアイテムがあったような…

 

− 矢木に電流走る −!

 

仮面ライダー剣(ブレイド)。ベイブレード。

つくるものが決まりました。

 

特徴

『ベイバトルパス』と同様にランチャーに装着して使用します。

この状態でベイをセットし、

シュートすると、『醒剣ブレイラウザー』のラウズカードスラッシュ音が鳴った後、SPが表示されます。

 

また、『ブレイド』が所有するラウズカードから一つ、カード名が読み上げられます。

このとき読み上げられるカード名のルールはちょっと変則的で、

  • SP 10,000未満 → SPの百の位の数字に応じて、チェンジ〜タイム
  • SP 10,000以上 11,000未満 → フュージョン
  • SP 11,000以上 12,000未満 → アブゾーブ
  • SP 12,000以上 → エボリューション

としています。素直にSPが1,000上がるごとに音声を変えていく仕様にすると、だいたいいつも同じ音声が流れてしまう可能性大でおもしろくないので、SPが10,000未満のときにはある程度音声がバラけるようにこの形にしました。SP 10,000以上は、少なくとも私の場合は滅多に出せない…というより一度も出せたことがないので、出せたときに達成感を感じやすいように、こちらは意図的に固定にしました。

SPについては、本家『ベイバトルパス』と全く同じではありませんが、近しい値が出るようになっています。実はここが一番苦労したところですが、それについては後述します。

あと、SPが10,000以上出ても、表示は9,999でカンストになります(※音声は前述の通り変化)。これは、5桁にすることも頑張ればできたかもしれませんが、

  • ブレイラウザーは4桁
  • 私がSP10,000以上を出せることがほぼないので、4桁でも基本的に困らない

ということで、ブレイラウザーに合わせる形で4桁にしました。下2桁を0に固定するのも、ブレイラウザーに合わせています。

機能としてはそれだけです。まあ、おもしろアイテムみたいなものなので。なお、無線通信機能は搭載しておりません。

 

ハードウェア解説

まずは具材から。

『ベイバトルパス』本体。3Dプリンタでフルスクラッチすることもできたかもしれませんが、剛性とかベイブレードらしさとか作業効率とか、これらを考えると、元のベイバトルパスをそのまま活かす方が良いだろうと考えました。中の基板は使わないので、ちょっともったいないですが。

 

ブレイラウザーっぽさを増すために、グリップも塗装して使用することにしました。これは塗装だけで、電子工作とは全然関係ありません。

 

今回使用したマイコンです。本来のベイバトルパスはスマホと通信するので無線機能が必須なのですが、今回は無線機能を搭載してまでやりたいことは特になかったので、無線機能なしで手軽に使えるSeeeduino XIAOにしました。

 

7セグ。素の7セグをゼロから点灯させようとするとそれなりに大変なのですが、これはドライバ付きなので制御がラクです。しかも、桁数を好きに追加していっても制御に必要な信号線数はそのまま。大変便利です。

 

MP3再生モジュール。普段はDFPlayerを使うことが多いですが、今回は音声少なめでファイルサイズが小さいのでこれで充分です。サイズがやや大きいのが困りどころですが。セットで、スピーカーはこちらを使用。

 

子供の玩具なのでリチウムイオン充電池は使いたくないものの、『ベイバトルパス』デフォルトの単四電池2本ではMaxでも3.0Vにしかならず、マイコンやMP3モジュールが動きません。ということで、これをかまして単四電池2本で5Vを得られるようにしました。

 

フォトリフレクタ。実はこれが一番重要な部品かもしれません。白と黒の変化を検出するための部品で、SPの算出に使用します。

 

ディテクタスイッチ。ベイのセット&リリースを検出します。

 

明るさセンサ。これは、最終的に出来上がる『ベイブレイラウザー』には組み込まれないのですが、SPの算出ロジックを導出する過程で使用しました。詳しくは後述。

 

主な具材はこんな感じで、他には以下のような部品を細々と。

  • 2mmねじ(長さ12mm)4本
  • スライドスイッチ
  • 電気抵抗器 220Ω x 1個、100kΩ x 1個
  • 配線用コード
  • 塗装用スプレー

回路図はこんな感じになります。

実際に配線したのがこちら。スペースに余裕がなくてなかなか大変でした。

電池ケースの上に7セグを載せる形になるので、そのままだとペイバトルパスに収まりません。

ということで、間に3Dプリンタで作成したスペーサーを挟んで、高さを確保するようにしました。これでもスペース的には結構ギリギリ。

 

ソフトウェア解説

今回のポイントは、「いかに正確にSPを表示させるか」というところです。

まず、本家『ベイバトルパス』がどうやってSPを計測するかですが、これはランチャーを見れば、なんとなくはすぐにわかります。

ワインダー/ストリングを引くと、中心の窓から見える部分が白黒交互に変わりますので、その変化の回数や速さを検知することで、シュートパワーが決まってくるものと思われます。

もう一つ。ベイをセットすると中心部分が迫り上がり、ベイバトルパスのスイッチが押されます。これで、ベイバトルパスはベイがセットされたことを検知できます。そして、ベイがシュートされるとスイッチがリリースされて、ベイがシュートされたことを検知できる。このあたりは、「いつまで白黒反転を計測するか」を決めるのに関わっていそうな気がします。

ということで、シュートパワーを算出するには、

①白黒反転の回数

②ワインダー/ストリングを引き始めてからベイがシュートされるまでの時間

と、

・SP(シュートパワー)

これらの関係式を導出する必要があります。どうやって?

ここが一番悩んだところでした。①②を計測するにはセンサが必要ですが、同時に、シュートパワーを計測するにはベイバトルパスも必要です。つまり、ランチャーのセンサを取り付けたいところをベイバトルパスが塞いでしまうという状態になります。

 

考えた結果、ランチャーを一個、計測用に改造して計測器を作ることにしました。まず、白黒反転の計測用の穴を別に開ける。これは簡単。

これで、フォトリフレクタを使って①を計測できるようになります。

難しいのは②です。先の白黒反転が計測可能になったことで「ワインダー/ストリングを引き始めたタイミング」を捉えることはできるようになりましたが、これに加えて、ベイが離れるタイミングを捉えることも必要です。ランチャーの元々のスイッチに手を加えることは、玩具設計的にかなり困難でした。

あれこれ考えた結果、全く別の手段でベイが離れたことを検知させるようにしました。具体的には、

こういう治具を3Dプリンタで2個作りまして。

片方にはLED、片方には明るさセンサをセット。そして、

こんな感じで、LEDと明るさセンサが向き合うようにランチャーに取り付けます。

LEDはつけっぱなしにしておいて、ベイがセットされると、

ベイが壁になって明るさセンサの値が小さくなり、ベイがシュートされると光を遮るものがなくなり明るさセンサの値が大きくなる。これでベイがシュートされたことを検知できる、というわけです。

 

これで晴れてデータがとれるようになりましたので、あとはひたすら計測です。ベイブレードアプリの『シュートジム』の機能を使ってSPを確認しつつ、同時に①②を計測します。

 

これを120回ほど繰り返してデータを収集しました。例えばこんな感じ。

 

ここからは、SPの計算式を仮説立てながら考えていきます。

シンプルに考えると、「勢いよくワインダー/ストリングを引くとSPは強くなる」と言えそうです。では、「勢いよく」はどう数値で表されるか?

素直に考えると、まず「ワインダー/ストリングを引き始めてからベイが落ちるまでの時間」が短いと、「勢いよく」と言えそうです。ただ、 それだけだと、シュートミスで早くベイが落下してもシュートパワーが高くなってしまいます。

ここで、白黒反転の数の使い道を考えてみます。これも、シンプルに考えると、白黒反転の数が多い方が「勢いよく」回転してそうな気はしますが、こちらもこれだけだと、ワインダー/ストリングを速く引いても遅く引いても反転数が同じならシュートパワーは同じ、となってしまいます。

この2点で考えると、「単位時間あたりの白黒反転数」あたりの数が、シュートパワーと正の相関関係にあるのでは、という気がします。ということで、これを横軸、シュートパワーを縦軸にプロットしてみたのがコチラ。

お手製の計測器での計測なので、多分に計測誤差が含まれていることを考えると、ほぼほぼ正解な気がします。

一次の線形回帰式で表現すると、

決定係数も0.95で、ざっくりとしたSPを計算するには充分な近似式だと思います。

あとは、この計算式をプログラムに組み込めばOKですが、この計算式はあくまで「ベイの落下タイミングを外付けの明るさセンサで検知」した場合の式です。実際にベイブレイラウザーに組み込む際には、ベイの落下タイミングは明るさセンサではなく、本家『ベイバトルパス』と同様にスイッチで判定することになるので、その差異で値の出方はまたちょっと変わると思います。実際、組み立てた後に実際にSPを表示させてみると、ちょっと低めに出る傾向があったので、最終的にはプログラム中でちょっと補正を入れています。でもまあ、今回はあくまで玩具。おおよそ正しい値が出て、なにより『ブレイラウザーっぽくSPが表示される』というのが一番の肝なので、そこがクリアされていればOKとしました。

 

 

ちなみに、この計算式が正しいとした場合。

ワインダー/ストリングを引き切ったときの白黒反転数の最大値は、自分が120回計測した中では、26回。ここから、SP 10,000以上を出すために、ワインダー/ストリングを引き切るのに求められる時間は…

 

0.113秒以内。

 

天翔龍閃?

 

まとめ

以上、『ベイブレイラウザー』のご紹介でした。出オチみたいなアイデアですが、作っていて非常に楽しかったです。CSMブレイバックルでBGMを鳴らしながら遊べば気分もバクアゲです。ブンブンジャーです。

 

 

ソースコード

全文は以下になります。ちょっとズボラして、計測のときに書いたコードをそのまま少し変更して最終的な『ベイブレイラウザー』としてのコードにしたので、計測時に書いていたコードがコメントアウトで残っています。

////////// 基本定義 ////////////////////////////////////////////////////////////
#include<SPI.h>
#include <SoftwareSerial.h>

#define LOOP_DELAY_MS   1

#define PIN_MP3_RX      1
#define PIN_MP3_TX      2
#define PIN_DETECT_BEY  3
#define PIN_READ_CODE   4
#define PIN_SPI_LATCH   7
#define PIN_SPI_SCK     8
#define PIN_SPI_SDI    10

#define ON  LOW
#define OFF HIGH

#define STATE_INIT         0
#define STATE_BEY_SET      1
#define STATE_BEY_SHOOTING 2
uint8_t state = STATE_INIT;
uint8_t prev_state = STATE_INIT;

void change_state(uint8_t new_state){
  state = new_state;
  switch(state){
  case STATE_INIT:         Serial.println(F("STATE_INIT"));         break;
  case STATE_BEY_SET:      Serial.println(F("STATE_BEY_SET"));      break;
  case STATE_BEY_SHOOTING: Serial.println(F("STATE_BEY_SHOOTING")); break;
  default: ;
  }
}

bool is_bey_detecting = false;
unsigned long bey_detect_start_point_ms = 0;
unsigned long bey_shoot_start_point_ms = 0;
unsigned long bey_shoot_end_point_ms = 0;
unsigned long bey_shoot_time_ms = 0;

#define BIN_WHITE 1
#define BIN_BLACK 0
#define BIN_THRETHOLD 630

//#define DETECT_BEY_LX_THRETHOLD  800 // 計測用
#define DETECT_BEY_MS_THRETHOLD 1000

uint8_t sw = OFF;
uint8_t prev_sw = OFF;

//uint16_t lx_raw = 0; // 計測用

uint8_t rotate_bin = 0;
uint8_t prev_rotate_bin = 0;

uint8_t read_code(uint8_t pin){
  uint16_t raw = analogRead(pin);
  //Serial.println(raw);
  uint8_t  bin = 0;
  if(raw < BIN_THRETHOLD){
    bin = BIN_BLACK;
  }else{
    bin = BIN_WHITE;
  }
  return bin;
}

uint16_t bin_change_counter = 0;
uint16_t bey_rotate_count = 0;

uint16_t calc_shoot_power(uint16_t bey_rotate_count, uint16_t bey_shoot_time_ms){
  double shoot_power = 43.195 * ((double)bey_rotate_count / ((double)bey_shoot_time_ms/1000.0)) + 81.402;
  return (uint16_t) shoot_power;
}

uint16_t shoot_power = 0;
uint16_t shoot_power_disp = 0;

/*-------------------------- 7セグメントLED関係 --------------------------*/

// Seeeduino XIAOで使用する場合は、VDDは3.3Vに接続

/* 7セグの位置とデータビットの関係
  A(0)
 -----=           例)LSBFIRSTの場合
|F(5)  |B(1)         0 = 0b11111100
| G(6) |             1 = 0b01100000
 ------              2 = 0b11011010
|E(4)  |C(2)         3 = 0b11110010
| D(3) |             4 = 0b01100110
 ------  .DP(7)      5 = 0b10110110
*/

#define SEG_ON  0b11111110
#define SEG_OFF 0b00000000
#define SEG_0   0b11111100
#define SEG_1   0b01100000
#define SEG_2   0b11011010
#define SEG_3   0b11110010
#define SEG_4   0b01100110
#define SEG_5   0b10110110
#define SEG_6   0b10111110
#define SEG_7   0b11100000
#define SEG_8   0b11111110
#define SEG_9   0b11110110
#define SEG__   0b00000010

const byte SEG_NUM[] = {
  SEG_0, SEG_1, SEG_2, SEG_3, SEG_4, SEG_5, SEG_6, SEG_7, SEG_8, SEG_9
};

// 計測用7セグ表示
// #define DISP_CHANGE_TIME_MS 1500
// unsigned long disp_change_point_ms = 0;
// bool is_showing_count = false;

void show_off(){
  digitalWrite(PIN_SPI_LATCH, LOW);
  SPI.transfer(SEG_OFF);
  SPI.transfer(SEG_OFF);
  SPI.transfer(SEG_OFF);
  SPI.transfer(SEG_OFF);
  digitalWrite(PIN_SPI_LATCH, HIGH);
}

void show_number(uint16_t num){
  uint8_t digit_1000 = num/1000;
  uint8_t digit_100  = num%1000/100;
  uint8_t digit_10   = num%1000%100/10;
  uint8_t digit_1    = num%1000%100%10;
  digitalWrite(PIN_SPI_LATCH, LOW);

  if(digit_1000 == 0){
    SPI.transfer(SEG_OFF);
  }else{
    SPI.transfer(SEG_NUM[digit_1000]);
  }

  if(digit_1000 == 0 && digit_100 == 0){
    SPI.transfer(SEG_OFF);
  }else{
    SPI.transfer(SEG_NUM[digit_100]);
  }

  if(digit_1000 == 0 && digit_100 == 0 && digit_10 == 0 ){
    SPI.transfer(SEG_OFF);
  }else{
    SPI.transfer(SEG_NUM[digit_10]);
  }

  SPI.transfer(SEG_NUM[digit_1]);

  digitalWrite(PIN_SPI_LATCH, HIGH);
}

void show_ready(){
  //"----"表示
  digitalWrite(PIN_SPI_LATCH, LOW);
  SPI.transfer(SEG__);
  SPI.transfer(SEG__);
  SPI.transfer(SEG__);
  SPI.transfer(SEG__);
  digitalWrite(PIN_SPI_LATCH, HIGH);
}

#define SP_COUNT_UP_MS 50
uint16_t sp_count_up_unit = 0;
uint16_t sp_count = 0;
unsigned long prev_sp_count_up_point_ms = 0;

void show_shoot_power(unsigned long now_ms){
  if(prev_state != state){
    if(shoot_power >= 10000){
      // カンスト表現
      shoot_power_disp = 9999;
    }else{
      // なるべくブレイラウザーの表示に寄せる(下二桁を0に固定)
      shoot_power_disp = shoot_power - shoot_power%100;
    }
    sp_count_up_unit = shoot_power_disp/20;
    sp_count = 0;
    prev_sp_count_up_point_ms = 0;
  }

  unsigned long passed_ms = now_ms - bey_shoot_end_point_ms;

  if(passed_ms < 1500){
    show_number(0);
  }else if(1500 <= passed_ms && passed_ms < 2500){
    if(passed_ms - prev_sp_count_up_point_ms >= SP_COUNT_UP_MS){
      sp_count += sp_count_up_unit;
      show_number(sp_count);
      prev_sp_count_up_point_ms = passed_ms;
    }
  }else if(2500 <= passed_ms && passed_ms < 2650){
    show_number(shoot_power_disp);
  }else if(2650 <= passed_ms && passed_ms < 2800){
    show_off();
  }else if(2800 <= passed_ms && passed_ms < 2950){
    show_number(shoot_power_disp);
  }else if(2950 <= passed_ms && passed_ms < 3100){
    show_off();
  }else if(3100 <= passed_ms && passed_ms < 3250){
    show_number(shoot_power_disp);
  }else if(3250 <= passed_ms && passed_ms < 3400){
    show_off();
  }else{
    show_number(shoot_power_disp);
  }
}

/*-------------------------- 音声処理 --------------------------*/
SoftwareSerial ss_mp3_player(PIN_MP3_RX, PIN_MP3_TX);

#define MP3_VOLUME 30 // 0〜30

void mp3_play(uint8_t track){
  // この定義だと再生曲数は255に限定されるので注意。
  // Upper-Byte, Lower-Byteをちゃんと処理してやれば65535曲まで対応可能だが、
  // 容量的にそこまで使うことはほぼないと思われる。

  // Macの場合は、MP3データを全て"ZH"フォルダにコピーした後、
  // ターミナルでZHフォルダに入り、"$ rm ._*"のコマンドで、
  // "._01.mp3"のような名前で裏で勝手に生成されているファイル
  // ("$ ls -a"のコマンドでないと見えない、AppleDouble Header file)
  // を削除しないと、挙動がおかしくなるので注意

  uint8_t play[6] = {0xAA,0x07,0x02,0x00,track,track+0xB3};
  ss_mp3_player.write(play,6);
}

void mp3_set_volume(uint8_t vol){
  uint8_t volume[5] = {0xAA,0x13,0x01,vol,vol+0xBE};
  ss_mp3_player.write(volume,5);
}

void play_rouze_card_sound(uint16_t sp){
  if(sp < 10000){
    uint8_t digit_100 = sp%10000%1000/100;
    if(digit_100 != 0){
      mp3_play(digit_100);
    }else{
      mp3_play(10);
    }
  }else if(10000 <= sp && sp < 11000){
    mp3_play(11);
  }else if(11000 <= sp && sp < 12000){
    mp3_play(12);
  }else{
    mp3_play(13);
  }
}
/*-------------------------------------------------------------*/

void setup(){
  Serial.begin(115200);
  //pinMode(PIN_DETECT_BEY, INPUT); // 計測用
  pinMode(PIN_DETECT_BEY, INPUT_PULLUP);
  pinMode(PIN_READ_CODE, INPUT);
  pinMode(PIN_SPI_LATCH, OUTPUT);
  pinMode(PIN_SPI_SCK, OUTPUT);
  pinMode(PIN_SPI_SDI, OUTPUT);
  SPI.begin();
  SPI.setBitOrder(LSBFIRST);
  SPI.setDataMode(SPI_MODE0);

  // MP3プレイヤーセットアップ
  ss_mp3_player.begin(9600);
  delay(500);
  mp3_set_volume(MP3_VOLUME);

  show_number(0);
}

void loop() {
  //lx_raw = analogRead(PIN_DETECT_BEY); // 計測用
  sw = digitalRead(PIN_DETECT_BEY);
  rotate_bin = read_code(PIN_READ_CODE);

  // 計測デバッグ用
  //Serial.print(F("BEY: "));
  //Serial.println(lx_raw);

  unsigned long now_ms = millis();

  switch(state){
  case STATE_INIT:

  /* // 計測用
    if(lx_raw < DETECT_BEY_LX_THRETHOLD){
      // ベイのセット開始を検知
      if(!is_bey_detecting){
        is_bey_detecting = true;
        bey_detect_start_point_ms = now_ms;
      }
    }else{
      is_bey_detecting = false;
    }
  */
    if(prev_sw == OFF && sw == ON){
      // ベイのセット開始を検知
      if(!is_bey_detecting){
        is_bey_detecting = true;
        bey_detect_start_point_ms = now_ms;
      }
    }
    if(sw == OFF){
      is_bey_detecting = false;
    }

    if(is_bey_detecting && now_ms - bey_detect_start_point_ms > DETECT_BEY_MS_THRETHOLD){
      // ベイのセットを確定
      is_bey_detecting = false;
      bin_change_counter = 0;
      bey_rotate_count = 0;
      show_ready();
      change_state(STATE_BEY_SET);
    }

    break;
  case STATE_BEY_SET:
    //if(lx_raw >= DETECT_BEY_LX_THRETHOLD){ // 計測用
    if(prev_sw == ON && sw == OFF){
      // ベイが離れたことを検知
      change_state(STATE_INIT);
    }

    if(prev_rotate_bin != rotate_bin){
      // 白黒反転の始まりを検知
      bey_shoot_start_point_ms = now_ms;
      change_state(STATE_BEY_SHOOTING);
    }

    break;
  case STATE_BEY_SHOOTING:

    //if(lx_raw >= DETECT_BEY_LX_THRETHOLD){ // 計測用
    if(prev_sw == ON && sw == OFF){
      // ベイが離れたことを検知
      bey_shoot_end_point_ms = now_ms;
      bey_shoot_time_ms = bey_shoot_end_point_ms - bey_shoot_start_point_ms;
      bey_rotate_count = bin_change_counter + 2; // +2は計測時と実装時の差異の調整

      Serial.print(F("COUNT: "));
      Serial.println(bey_rotate_count);
      Serial.print(F("SHOOT_TIME: "));
      Serial.println(bey_shoot_time_ms);
      shoot_power = calc_shoot_power(bey_rotate_count, bey_shoot_time_ms);
      Serial.print(F("SHOOT_POWER: "));
      Serial.println(shoot_power);

      play_rouze_card_sound(shoot_power);
      //show_shoot_power(shoot_power);
      change_state(STATE_INIT);
    }

    break;
  default:
    ;
  }

  // 白黒反転のカウント
  if(prev_rotate_bin != rotate_bin){
    bin_change_counter++;
  }

  // 計測用7セグ表示(1.5秒ごとに白黒反転数とシュート時間を表示切り替え)
  /*
  if(state == STATE_INIT && now_ms - disp_change_point_ms >= DISP_CHANGE_TIME_MS){
    if(is_showing_count){
      show_number(bey_shoot_time_ms);
    }else{
      show_number(bey_rotate_count);
    }
    disp_change_point_ms = now_ms;
    is_showing_count = !is_showing_count;
  }
  */

  if(state == STATE_INIT){
    show_shoot_power(now_ms);
  }

  prev_rotate_bin = rotate_bin;
  prev_sw = sw;

  prev_state = state;

  delay(LOOP_DELAY_MS);
}