ライト&サウンド ボトルマンをつくる

今回は珍しく、特撮玩具以外の作品です。本体の改造は不要で、ボトルマンに取り付けるだけでマンガやアニメっぽい(?)発光•音声エフェクトを追加できる拡張パーツを作ってみました。

開発経緯

玩具好きの間で何かと話題(?)のタカラトミー発のシューティングホビー『キャップ革命 ボトルマン』。自分はこの手の競技玩具(ミニ四駆とかベイブレードとか)に対して熱心なわけではないのですが、ボトルマンについては、過去に(初期のボンバーマン時代の)ビーダマンで遊んでいたことや、ボトルマンそのもののアイデアの秀逸さに惹かれるところもあって、ハマり過ぎない程度に集めています(←図体の大きいライジングミルク、フウジンブラック、それからカラバリ系以外)。

こういう玩具は大体メディアミックスが前提でアニメやマンガがセットになることが多いのですが、皆様ご存知の通り、メディアの方では色々ド派手なことが起こったりします。超速回転で土煙を上げながら進む弾丸、轟く轟音、壁にめり込む弾丸、吹っ飛ぶ人..具体的にこのシーン、と言えるわけではないのですが、なんとなくそんなイメージです。もちろん実際の玩具でそんなことはできるわけはないのですが、それっぽい雰囲気を現実でちょっとでも出せたら面白いんじゃないかと思って作ってみました。

 

特徴

今回実際に作ったものがこちらです。『ボトルマン専用 ライト&サウンドパック』(仮称)です。ボトルマンにセットすることで、発光や音声再生機能を付与できる、という代物です。ボトルマン本体には一切手を加えずに使用可能、というのが大きなポイントです。発光や音声を追加するということであれば、こういう拡張パーツのような形にせずに、特定のボトルマンの機体に特化してゴリゴリ改造するやり方もあるのですが、今回はいろんな機体で手軽に発光&音声を楽しめるように、後付けパーツ方式にしました。

 

使い方は簡単で、ボトルマン背部のキャップ投入口からこのパックをセットしまます。これで電源がONになります。

 

某人工知能搭載人型ロボの起動音みたいな音声が流れますが、気にしないでください。

続いて、キャップをセットします。本来の投入口を完全に塞いでしまっているので、フロントからセットする形になります。

 

某1000%社長の武器みたいな音声が流れますが、気にしないでください。キャップセット時の発光をわかりやすくするために、動画冒頭でちょっと周囲を暗くしています。

キャップをセットしてトリガーを押し込むと、エネルギーがチャージされている感じの発光・音声になり、

 

打ち出すと爆発音が鳴ります。ちなみに動画では省略していますが、チャージ完了前にキャップを打ち出すと、弱攻撃扱いで爆発音がちょっと弱めの音声になります(←最後の大爆発音がない)。

こんな感じで、キャップ投入口を完全に塞いてしまうことからもわかるように、競技性能の向上には全く貢献しない(むしろ下がっている)、完全なお遊び改造となっております。

 

ハードウェア解説

まずは今回使用した具材です。何はともあれボトルマン本体です。

一番最後のまとめでも触れますが、今回の改造は上記三体(カラバリ含む)での利用を想定しています。アクアスポーツは売りのロングマガジンを外してもらわないといけないのが申し訳ないですが。

 

次に電子工作部品です。まずはマイコンから。

コンパクトで使いやすいので愛用しているマイコンなのですが、半導体不足の影響か、この記事の執筆時点(2021年8月上旬)ではどこも品切れ…今回は手元に残っていた在庫でなんとかなりましたが、それも尽きてしまったので、次作以降どうしよう、というのが結構深刻な悩みだったりします。

続いてMP3プレイヤーです。

今回のような作品では必要な音源はごく少数なので、通常であればDFPlayerではなくMP3ボイスモジュールの方を使用するのですが、こちらはサイズがやや大きい、という弱点があります。これが今回は結構クリティカルで、とにかく部品を納めるスペースに余裕がなかったので、性能的にはややオーバースペックにはなりますが、よりサイズの小さいDFPlayerの方を採用しました。なお、DFPlayerは別途マイクロSDカードが必要なのですが、それについてはこちらを使用しています。

スピーカーについても、同様にサイズ重視で以下を使用しています。

LEDには高輝度白色チップLEDを使用しています。

白色にしておけば、どのキャップを装填してもそのキャップの色で光っているように見える、という寸法です。

また、スイッチとして以下のディテクタスイッチを3つ、使用しています。

最後に、全体を動作させるためのバッテリーとして、以下を使用しています。

必要な具材はこんな感じで、あとは配線のためのリード線ぐらいです。

 

上記の具材を以下のように配線します。

 

動作イメージは以下のようになります。

  1. パーツ本体をボトルマンの頭部にセットすると、POWER_SWがOFFからONになって電源ON、音声再生
  2. キャップを前から押し込むと、SET_SWがOFFからONになってキャップ装填認識、発光&音声再生
  3. トリガーを押し込むと、キャップが少し前に出てSET_SWがONからOFFになって押し込み認識、発光&音声再生
  4. さらにトリガーを押し込むと、SHOT_SWがONからOFFになってキャップ打ち出し認識、発光&音声再生
  5. パーツ本体をボトルマンの頭部から外すと、POWER_SWがONからOFFになって電源OFF

あとはこれを実現できるような筐体を用意できればOK…なのですが、これが今回の一番難しいところになります。パーツを本体にセットしたときの見た目を気にしなければいくらでもやりようはあると思いますが、コンパクトにまとめようと思うと難易度がぐんぐん上がります。

とりあえず、苦手なりに頑張ってFusion360で設計してみました。

これらを3Dプリンタで出力したものが以下になります。

後はこれに部品を組み込みつつ配線しつつ、ということになります。

今回一番難しいというか、センシティブなのがこの部分です。上図右側がキャップ装填認識用スイッチ(SET_SW)、左側がキャップ打ち出し認識用スイッチ(SHOT_SW)になります。

実際に組み上げた後に裏から見ると、こんな感じにスイッチが並びます。これで先程の動作イメージが実現できることを確認します。

 

キャップのセット前はSHOT_SW, SET_SW共にOFFで、

キャップを奥までセットすると、SHOT_SWとSET_SWが共にOFFからONに。SET_SWのOFF→ONがキャップ装填認識の条件です。

トリガーを少し押し込むと、SET_SWがONからOFF。これで押し込み認識です。

さらにトリガーを押し込むと、SHOT_SWがONからOFF。これで打ち出し認識完了です。こうやって表すと簡単そうに見えますが、この形に行き着くまでに頭の中でかなり試行錯誤しました。

ちなみにここまでキャップが正位置(?)の状態で説明してきましたが、上下を逆にしても問題なく動作します。

上下逆でキャップを押し込むと、SET_SWの方はキャップの縁がかかってOFFからONになります。SHOT_SWの方は正位置のときと違ってOFFのままですが、キャップ装填の認識にこのSHOT_SWの状態は関係ないので問題ありません。

トリガーを少し押し込むと、SET_SWがONからOFF。これで押し込み認識。ここでも、SHOT_SWの状態は関係ありません。

さらにトリガーを押し込むと、キャップの縁がかかってここでSHOT_SWが一瞬ONになり…

最後までトリガーを押し込むと、SHOT_SWがONからOFFになって打ち出し認識完了です。

 

キャップを正位置にしてセットするか逆向きにセットするかで、発光の仕方が結構変わってきます。

逆向きにしてセットするときには、内側にアルミシールとかを貼っておくと発光が結構強くなるので、YouTube動画ではわかりやすいようにこの状態で撮影しています。せっかくなので、通常のキャップを正位置でセットしたときとの発光の仕方の違いを比較してみましょう。

まずこちらが、通常のキャップを正位置でセットした場合の発光になります。熱が内側に閉じ込められているような感じで、個人的には結構好きです。

こちらが、内側にアルミシールを貼ったキャップを逆位置でセットした場合です。エネルギーが今にも炸裂しそうな感じで、こっちはこっちでかっこいいと思います。お好みでどちらでも、という感じです。

 

仕組み解説がちょっと長くなりましたが、最後まで部品を組み上げていくとこんな感じになります。

スペースに全然余裕がなく、本当にギリギリでした。

 

ソフトウェア解説

最後にプログラムの解説ですが、今回は特筆すべきことはないかなと思います。

 

こんな感じで状態遷移図をしっかり描けば、あとはそんなに難しいことはないかと思います。ソースコードの全文は、本記事の最後に掲載しています。発光処理のところだけちょっと難しく感じるかもしれせんが、あんまり深く考えずにそのまんま流用してしまうのも全然アリだと思います。

 

まとめ

以上、ライト&サウンド ボトルマン(ボトルマン専用 ライト&サウンドパック)のご紹介でした。

ボトルマンの改造は基本的に、

  • 競技ホビーとして、競技性能を追求する改造
  • 現実のドリンクを意識したりしながら、オリジナル機体を作る改造

の2方向が主かと思うのですが、今回の改造はそのどちらでもない、ちょっと珍しいタイプの改造になったかなと思います。

 

ちなみにこのパック、残念ながら使用できるのは(おそらく)初期型&基本形のボトルマンのみになります。

ワンダーグレープに使用できないのは言わずもがなですが、

(記事執筆時点で)最新型のスカルピストルにも使用できません。

というのも、実はスカルピストルのボトルマン素体は、パッと見は従来と同じですがちょっと変更が加えられていて、

頭部の内側の部分が、

こんな感じで埋められるようになりました。これはおそらく、キャップのジャムりの発生を抑制するための改善処置と思われます。

今回のパックはスカルピストルの発売前に開発着手してしまったため、頭部内側の空洞をフルに活用する形になっています。しかし、今後はスカルピストルタイプの素体が主流になってくると思われるため、現状のままだと今後のボトルマンでは使用できないことになります。残念。今後もし何か面白いアイデアが思いついたら、そのときはこの新素体でも使えるように作っていきたいと思います。

 

さて、今回は自分としてはかなり珍しく、特撮(仮面ライダー)以外の玩具の改造となりました。今後も何か面白いアイデアが思いつけばまたボトルマンの改造とかもやるかもしれませんが、多分次はまたライダー系の作品になると思います。『リバイス』も始まることですし。

 

ソースコード

////////// 基本定義 ////////////////////////////////////////////////////////////

#define LOOP_DELAY_MS 20

#define MP3_RX_PIN  0
#define MP3_TX_PIN  1
#define LED_PIN     2
#define SET_SW_PIN  3
#define SHOT_SW_PIN 4

#define ON  LOW
#define OFF HIGH

uint8_t set_sw  = OFF;
uint8_t shot_sw = OFF;
uint8_t prev_set_sw  = OFF;
uint8_t prev_shot_sw = OFF;

#define STATE_INIT     0
#define STATE_READY    1
#define STATE_CHARGING 2
#define STATE_CHARGED  3

uint8_t state      = STATE_INIT;
uint8_t prev_state = STATE_INIT;

#define CHARGING_MS  3500
unsigned long charge_start_time = 0;

void print_state(){
  if(prev_state != state){
    switch(state){
    case STATE_INIT:     Serial.println(F("STATE_INIT"));     break;
    case STATE_READY:    Serial.println(F("STATE_READY"));    break;
    case STATE_CHARGING: Serial.println(F("STATE_CHARGING")); break;
    case STATE_CHARGED:  Serial.println(F("STATE_CHARGED"));  break;
    default: ;
    }
  }
}

////////// 音声処理 /////////////////////////////////////////////////////////////
#include <DFPlayerMini_Fast.h>
#include <SoftwareSerial.h>

SoftwareSerial ss_mp3_player(MP3_RX_PIN, MP3_TX_PIN);
DFPlayerMini_Fast mp3_player;

#define SOUND_VOLUME_DEFAULT 22 // 0〜30

#define SOUND_POWER_ON 1
#define SOUND_READY    2
#define SOUND_CHARGE   3
#define SOUND_FIRE_W   4
#define SOUND_FIRE_S   5

void play_sound(uint8_t sound_num){
  mp3_player.playFromMP3Folder(sound_num);
  Serial.print(F("Play sound: "));
  Serial.println(sound_num);
}

void pause_sound(){
  mp3_player.pause();
  Serial.print(F("Pause sound: "));
}

void control_sound(){
  if(prev_state != state){
    switch(state){
    case STATE_INIT:
      switch(prev_state){
      case STATE_CHARGING:
        play_sound(SOUND_FIRE_W);
        break;
      case STATE_CHARGED:
        play_sound(SOUND_FIRE_S);
        break;
      default:
        ;
      }
      break;
    case STATE_READY:
      play_sound(SOUND_READY);
      break;
    case STATE_CHARGING:
      play_sound(SOUND_CHARGE);
      break;
    case STATE_CHARGED:
      break;
    default:
      ;
    }
  }
}

////////// 発光処理 /////////////////////////////////////////////////////////////

#define LED_BRIGHTNESS 255 // 255が上限値

boolean is_led_active = false; // 電源投入直後のSTATE_INITのときのLED発光を無効にするためのフラグ

unsigned long led_pattern_start_time = 0;
unsigned long prev_blink_time        = 0;
unsigned long inc_dim_start_time = 0;
boolean is_lighting = false;
boolean is_inc = true;

int  prev_interval_ms = 0;
uint8_t prev_steps = 0;

void led_base_pattern_on(){
  analogWrite(LED_PIN, LED_BRIGHTNESS);
  prev_interval_ms = 0;
  prev_steps = 0;
}

void led_base_pattern_off(){
  analogWrite(LED_PIN, 0);
  prev_interval_ms = 0;
  prev_steps = 0;
}

void led_base_pattern_blink(unsigned long now_ms, int interval_ms){
  if(now_ms - prev_blink_time >= interval_ms){
    if(is_lighting){
      analogWrite(LED_PIN, 0);
    }else{
      analogWrite(LED_PIN, LED_BRIGHTNESS);
    }
    is_lighting = !is_lighting;
    prev_blink_time = now_ms;
  }

  prev_interval_ms = interval_ms;
  prev_steps = 0;
}

void led_base_pattern_inc(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*current_step);

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_dim(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*(steps-current_step));

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_blink_slowly(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
    is_inc = true;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  if(is_inc){
    analogWrite(LED_PIN, l_step*current_step);
  }else{
    analogWrite(LED_PIN, l_step*(steps-current_step));
  }
  if(now_ms - inc_dim_start_time >= interval_ms){
    is_inc = !is_inc;
    inc_dim_start_time = now_ms;
  }

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_pattern_init(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_off();
}

void led_pattern_ready(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 2500){led_base_pattern_on();}
  else{                 led_base_pattern_off();}
}

void led_pattern_charging(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_inc(now_ms, CHARGING_MS, 40);
}

void led_pattern_charged(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_blink(now_ms, 60);
}

void led_pattern_fired(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 1000){led_base_pattern_dim(now_ms, 1000, 20);}
  else{                 led_base_pattern_off();}
}

void control_led(){

  unsigned long now_ms = millis();

  if(prev_state != state){
    // 電源ON直後はLEDをOFFにしているので、最初の状態遷移以降にLEDを有効化させるための処理
    is_led_active = true;
    led_pattern_start_time = now_ms;
  }

  unsigned long passed_ms = now_ms - led_pattern_start_time;

  switch(state){
  case STATE_INIT:
    if(is_led_active){
      led_pattern_fired(passed_ms, now_ms);
    }else{
      led_pattern_init(passed_ms, now_ms);
    }
    break;
  case STATE_READY:
    led_pattern_ready(passed_ms, now_ms);
    break;
  case STATE_CHARGING:
    led_pattern_charging(passed_ms, now_ms);
    break;
  case STATE_CHARGED:
    led_pattern_charged(passed_ms, now_ms);
    break;
  default:
    ;
  }
}

////////// メイン処理 ////////////////////////////////////////////////////////////

void setup(){
  Serial.begin(115200);
  pinMode(LED_PIN,     OUTPUT);
  pinMode(SET_SW_PIN,  INPUT_PULLUP);
  pinMode(SHOT_SW_PIN, INPUT_PULLUP);

  // MP3プレイヤーセットアップ
  ss_mp3_player.begin(9600);
  if(!mp3_player.begin(ss_mp3_player)) {
    Serial.println(F("Unable to begin music_player:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while(true);
  }
  mp3_player.volume(SOUND_VOLUME_DEFAULT);
  play_sound(SOUND_POWER_ON);

  led_base_pattern_off();
}

void loop(){
  unsigned long now_ms = millis();

  //////////////////// 状態遷移管理(スイッチ) ////////////////////
  set_sw  = digitalRead(SET_SW_PIN);
  shot_sw = digitalRead(SHOT_SW_PIN);

  switch(state){
  case STATE_INIT:
    if(prev_set_sw == OFF && set_sw == ON){
      state = STATE_READY;
    }
    break;
  case STATE_READY:
    if(prev_set_sw == ON && set_sw == OFF){
      state = STATE_CHARGING;
      charge_start_time = now_ms;
    }
    break;
  case STATE_CHARGING:
    if(prev_set_sw == OFF && set_sw == ON){
      state = STATE_READY;
    }else if(prev_shot_sw == ON && shot_sw == OFF){
      state = STATE_INIT;
    }
    break;
  case STATE_CHARGED:
    if(prev_set_sw == OFF && set_sw == ON){
      state = STATE_READY;
    }else if(prev_shot_sw == ON && shot_sw == OFF){
      state = STATE_INIT;
    }
    break;
  default:
    ;
  }

  //////////////////// 状態遷移管理(時間経過) ////////////////////
  if(state == STATE_CHARGING && now_ms - charge_start_time >= CHARGING_MS){
    state = STATE_CHARGED;
  }

  ////////// 音声再生処理 ////////////////////
  control_sound();

  ////////// LED発光処理 ////////////////////
  control_led();

  ////////// デバッグ処理 ////////////////////
  print_state();

  ////////// 処理状態の保持 //////////////////
  prev_set_sw  = set_sw;
  prev_shot_sw = shot_sw;
  prev_state   = state;

  delay(LOOP_DELAY_MS);
}