MENU

ムゲンカプセムをつくる

今回は『仮面ライダーゼッツ』より、『ムゲンカプセム』をつくってみました。

目次

開発経緯

毎度、新ライダーの玩具が出て来ると「なんか面白いもの作れないかな」を考えるのがもはや習慣になっています。

今年の玩具はカプセム。内容はめっちゃシンプルです。

認識は素直な押しピン方式。オルゴール方式ですらないので、認識もMAXで63パターンと少なめです。

そして本体ギミックは、カプセム中央部が回転し、それによってパラパラ漫画のようにアニメーションが見えるというもの。

この本体ギミックですが、うーん、個人的には微妙です。回すこと自体は、まあ楽しいと言えば楽しいです。ハズレ個体を引くとカチカチいって綺麗に回らなくてとてもストレスですが。ちなみに上の動画は私が自力で補正した後のもので、元は以下のようなハズレ個体でした。

また、ベルトにセットした状態で回すと、物理的にスイッチを押す仕様なので、回転がすぐに止まってしまってあまり爽快感がないです。

で、一番微妙なのが、アニメーションがよく見えないことです。先述のとおり、ベルトにセットした状態だと回転がすぐに止まりますし、じゃあベルトにセットしていない状態で回せばよく見えるかというと、そういうことでもない。いい感じに見える回転速度を探すのがとても難しいです。

ということで、アイデアとしては悪くないと思うのですが、正直、あまり機能しないギミックになってしまっている気がします。もったいない。なので、これをよく見えるようにすると、それだけである程度価値はあるんじゃないかな、という気はしました。自分の場合、ディスプレイつきのマイコンでパラパラ漫画アニメーションを表示する方法は実装経験があるので、小型ディスプレイを搭載すれば実現できそうな気はします。

あとはそれをどういうコンセプトで作品として仕上げるかです。カプセム、どう見てもガシャポンとの相性が抜群、というよりは、本編でもそういう扱いなので、『ガシャポン』という縛りで色々考えていたところ、過去に直球で、『ディスプレイが付いたガシャポンのカプセル』と言える玩具が存在していたのを思い出しました。それが、

ムゲンガシャポン』。

これのカプセム版を作る、と考えれば、収まりが良さそうです。

特徴

ど真ん中に鎮座するディスプレイが目を惹きますが、ポイントは背面にある、

こちらの小さなガシャポンハンドルです。これを回すと…

カプセルが出現。そしてカプセルに触れるようにディスプレイを押し込むと…

カプセルが割れて…

カプセムが出現!2025年11月15日時点で発売済の11種類のカプセムからランダムで1つが選ばれます。

この状態でディスプレイを押し込むと、以下のような表示に。

これは、カプセムに対応するピン配置を表していますので、●になっているピンが後ろから出てくるように、手動で調整します。

調整したら、ゼッツドライバーにセット。

通常のカプセムと同様の変身シーケンスに入りますが、一点違いがあって、このムゲンカプセムでは機構上、「カプセムを回して変身」ができません。その代わりに、

中央の赤いボタンを押し込むことで変身するようになっています。それと同時に絵が動き出します。ここが今回一番やりたかったところですね。

基本的な遊び方は以上なのですが、毎回カプセムがランダム選択だとそれはそれで遊びづらいこともあると思うので、裏モードとして、好きなカプセムを自由に選べるようにもしてあります。

正面ボタンを2秒ほど押した後にハンドルを回すと、カプセムが順に切り替わるようになります。あとの遊び方は同じで、再び正面ボタンを長押しすると、ランダム選択(ガシャポンモード)に戻ります。

ハードウェア解説

まずは必要な具材から。

今回、カプセム本体の大部分は3Dプリンタで作成していますが、カプセムの上部と中心の軸パーツについては、商品のカプセムの部品をそのまま流用しています。ということで、ダブりのカプセムがあればそれを使いましょう。今回は、ガシャポンのみならず、普通にDX版を買い集めているだけでもダブりやすい売り方になっているので、多分ダブりがある人は多いはず…私はリカバリーを使用しました。

そしてメインマイコン。ATOM S3を使用しています。

ディスプレイ付きでこのサイズ。素敵。バッテリーだけ何とかしないといけないのがつらいところですが。

続いてMP3プレイヤー。毎度お馴染みのDFPlayerです。

ちなみにスピーカーはこちら。マイクロSDカードも忘れずに。

今回、カプセムの内部スペースに全然余裕がなかったので、MP3プレイヤーとスピーカーはなしにして、本家ムゲンガシャポンのように電子音のブザーにする、ということも検討したのですが、やっぱりガシャポンを開けたときにカプセム名が呼ばれる方が盛り上がるかなあと思ったので、頑張って入れ込みました。

ガシャポンハンドル用に。実はロータリーエンコーダーはこれまで使ったことがなかったので、サイズとか部品選定を色々間違えましたが、最終的にはこれを使用しました。

最近はなるべく使わないようにしていたリチウムイオンポリマー充電池ですが、今回は流石に使わざるを得ませんでした。これしか入らん。

前面のスイッチの押し込み判定に使用しました。

今回、ピン配列を自由に変更できるようにする必要があったので、そのピンとして使用。20mmと15mmが欲しかったので、こちらのセットにしました。

最初は2mmの金属ネジを回してピンを出し入れする形で設計していたのですが、2mmだとピンがやや細くて認識ミスが起こるかなと思って、途中で2.5mmに変更しました。で、金属ネジじゃなくてナイロンネジを使ってみたところ、偶然ですがドライバーなしで手でいい感じに押し引きして調整できるぐらいのハマり具合になったので、こちらを採用しました。

今回、なるべくゼッツドライバーの発光を活かしたかったので、出来る限り透明性の高いフィラメントで3Dプリントすることにしました。ただ、私の経験上、クリア系のフィラメントはサポートを外すのがとても大変…ということで、なるべくサポートを外しやすいフィラメントを探していたのですが、今回のは過去一で扱いやすかったです。サポートが綺麗に剥がれる。価格は高めで、湿気にとても弱いらしいので保管もちょっと手間ですが、買った甲斐はありました。

具材はこんな感じで、あとはいらなくなった玩具に入っていたバネとかネジを流用しています。

配線はこんな感じです。ちなみにSW_REはロータリーエンコーダに搭載されている押しボタンスイッチで、最初はここの長押しをモードチェンジボタンに割り当てていたのですが、カプセムをドライバーにセットしたときに長押しされることがあることが判明したため、最終的には使わずじまいになりました。

実際に配線したものがこちら。これを、

3Dモデリング&プリントした筐体に埋め込んでいきます。

上部は市販のカプセムの部品をそのまま流用で、印刷した底パーツと一緒にメタリックグリーンで塗装。

ついでにマイコン本体も、おっかなびっくりで同色に塗装。白のままでもデザイン的にそこまでおかしいわけではなかったのですが、こっちの方がカッコいいだろうと思ってこうしました。

底パーツのピン代わりのネジは、全部同じ長さにするとちょっとイマイチな感じだったので、両サイドだけ短くして、なるべく球体が維持されるようにしました。

押しボタン部分は、こんな感じでバネを入れ込んでいます。奥の突起部分が、後で出てくるパーツと組み合わさって、ゼッツドライバーのスイッチを押し込めるようになっています。

続いてクリア部分。とにかくスペースが厳しい。このデザインに行き着くまでに設計とデザインを何度やり直したことか。

ちょっと見にくいですが、底に2つ穴が空いていて、中央付近の穴はバッテリーをセットするための穴です。クリア部分だけではどうにも収まらなかったので、貫通させて底パーツまで深さを広げることで、何とかバッテリーを入れることができました。

向かって左側の穴は、先ほどの底パーツの押しボタンの突起が出てくるところです。ここに、

この矢印みたいな部品をはめ込むことで、

押しボタンの操作に連動してこの矢印部品がゼッツドライバーのスイッチを押し込み、変身が可能になります。

ちなみにゼッツドライバーに最適化する形で設計した結果、ブレイカムゼッツァーではこの押し込みボタンで必殺技は発動できません。無念。

両サイドにMP3プレイヤーとスピーカーを分散配置、背面にロータリーエンコーダー、前面にマイコン、で何とかおさまりました。

電源スイッチはこちらです。

ソフトウェア解説

状態遷移図はこんな感じです。比較的シンプル。

ロータリーエンコーダーは今回初めて採用した部品ですが、こちらのライブラリで上手く動作してくれました。助かる。

アニメーションのプログラムには、M5Unifiedに統合されているM5GFXライブラリを使用。デジタルゴチゾウのときと同じですね。スプライトはアニメーション用に4枚、ピン配置表示用に1枚用意して、これらを状態に応じて切り替えています。

各画像データは、PNGファイルを用意して、それをこちらのサイトでバイト列に変換(←”RAW Dump”を選択)してimg ファイルにまとめています。例えばハンドル画像はこんな感じになります。

#include <pgmspace.h>

const unsigned char handle[797] PROGMEM = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x08, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x3e, 0x61, 
0xcb, 0x00, 0x00, 0x00, 0x44, 0x65, 0x58, 0x49, 0x66, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 
0x08, 0x00, 0x01, 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x03, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 
0x00, 0xa0, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0xa0, 0x03, 0x00, 
0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x08, 0xe2, 0x55, 
0xee, 0x00, 0x00, 0x02, 0x94, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0xed, 0xdd, 0x51, 0x6e, 0xc2, 
0x30, 0x10, 0x00, 0x51, 0x53, 0x71, 0x2c, 0x0e, 0x93, 0x1c, 0x2c, 0x82, 0xc3, 0x70, 0xaf, 0xf6, 
0x9f, 0xd8, 0xd1, 0x5a, 0x5b, 0x3b, 0x41, 0x33, 0xef, 0xb3, 0xa2, 0x22, 0xa5, 0x23, 0x6b, 0x63, 
0x25, 0xe4, 0x56, 0xbe, 0xdc, 0xb2, 0x2c, 0xbf, 0x67, 0xbe, 0xff, 0xeb, 0xf5, 0xba, 0x9d, 0xf9, 
0xfe, 0x59, 0x3f, 0x67, 0x1f, 0x80, 0xce, 0x65, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0xf7, 
0xb3, 0x0f, 0xa0, 0xa6, 0x67, 0xb0, 0x7b, 0x3e, 0x9f, 0x23, 0x0f, 0x25, 0x22, 0x7c, 0xac, 0x57, 
0x1c, 0x18, 0x5d, 0x01, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0xa6, 
0x4e, 0xa5, 0xd1, 0xe9, 0xfe, 0x02, 0x93, 0xfd, 0x10, 0xeb, 0xba, 0x86, 0x5e, 0x37, 0xf3, 0x6c, 
0xc1, 0x15, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0x86, 0x0c, 0x1b, 0xad, 0x61, 
0x6f, 0xc4, 0x70, 0x57, 0x1b, 0xac, 0x46, 0x0d, 0x91, 0xb3, 0xde, 0xab, 0x35, 0x2c, 0x8e, 0x18, 
0x0e, 0x5d, 0x01, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0x2e, 0x7d, 0x3d, 0x40, 0x6d, 
0xe0, 0xcb, 0x0e, 0x46, 0xef, 0xf7, 0x3b, 0xf5, 0xfb, 0x33, 0xf5, 0x1c, 0xeb, 0xe3, 0xf1, 0x08, 
0xbd, 0xee, 0xe0, 0xf3, 0xdb, 0x7d, 0xd6, 0xd9, 0xc1, 0xd0, 0x15, 0x00, 0xce, 0x00, 0xe0, 0x0c, 
0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xae, 0xeb, 0x2c, 0x80, 0x3e, 0xf1, 0x67, 0xd5, 0xfe, 0xd6, 
0xe8, 0x99, 0x41, 0x29, 0xcd, 0xcf, 0x3a, 0x75, 0x66, 0xe0, 0x0a, 0x00, 0x67, 0x00, 0x70, 0x06, 
0x00, 0x67, 0x00, 0x70, 0x53, 0x6f, 0x0d, 0x23, 0x0d, 0x7c, 0x51, 0xd9, 0xc1, 0x30, 0xcb, 0x15, 
0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0xaa, 0x43, 0x60, 0xf6, 0xa2, 0x4e, 0x87, 
0xbd, 0x9c, 0xd6, 0xe7, 0x57, 0x1b, 0x0e, 0xa3, 0xbb, 0x83, 0xa5, 0xd4, 0x77, 0x08, 0x5d, 0x01, 
0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 
0x0c, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xee, 0x9e, 0xbd, 0xd0, 0xd3, 0x6d, 0xdf, 0x79, 0xa2, 
0xd7, 0x0e, 0xf4, 0xdc, 0x5a, 0xe6, 0x0a, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 
0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 
0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 
0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 
0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 
0x70, 0x06, 0x00, 0x77, 0x6f, 0x3c, 0x60, 0x28, 0xfc, 0x55, 0xb1, 0xb5, 0xfb, 0xd3, 0xfd, 0xce, 
0x80, 0x31, 0xa2, 0xcf, 0x11, 0x58, 0xd7, 0xb5, 0xfa, 0x73, 0xbf, 0x2a, 0x56, 0x3b, 0x06, 0x00, 
0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 
0x00, 0x70, 0x06, 0x00, 0x57, 0x7d, 0x6a, 0xd8, 0xc1, 0xf3, 0xe7, 0x43, 0x5f, 0x2b, 0xdb, 0xda, 
0xb2, 0x74, 0x8b, 0x38, 0xa6, 0xe7, 0xd1, 0xb1, 0xb5, 0x6d, 0xdf, 0x83, 0xff, 0xdf, 0x8e, 0x2b, 
0x00, 0x9c, 0x01, 0xc0, 0x19, 0x00, 0x9c, 0x01, 0xc0, 0x55, 0x87, 0xc0, 0x51, 0xbc, 0x76, 0x60, 
0xaf, 0x67, 0xe0, 0x1b, 0xc1, 0x15, 0x00, 0xce, 0x00, 0xe0, 0x0c, 0x00, 0xce, 0x00, 0xe0, 0xba, 
0x86, 0xc0, 0xe8, 0x05, 0xa4, 0x3d, 0x0f, 0x9d, 0x22, 0x0d, 0x86, 0xd9, 0x81, 0x2f, 0xbb, 0xeb, 
0x57, 0xe3, 0x0a, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x97, 0xde, 
0x0a, 0xce, 0xde, 0x5a, 0x56, 0xd3, 0x33, 0x2d, 0x6f, 0xdb, 0x16, 0x7e, 0xed, 0x08, 0x23, 0xb6, 
0x72, 0x7b, 0x6e, 0xed, 0xca, 0x72, 0x05, 0x80, 0x33, 0x00, 0x38, 0x03, 0x80, 0x33, 0x00, 0xb8, 
0x7f, 0x1f, 0x2a, 0x8e, 0x2c, 0xcb, 0x52, 0x1d, 0x0e, 0x3f, 0xf5, 0x6c, 0x25, 0x7f, 0x93, 0xd6, 
0x70, 0xf7, 0x69, 0xc4, 0xb0, 0xd7, 0xe2, 0x0a, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 
0x70, 0x53, 0x87, 0xc0, 0xa8, 0xe8, 0xb0, 0x58, 0xca, 0xf9, 0x03, 0x63, 0x74, 0xb0, 0x2b, 0x65, 
0xee, 0x70, 0x17, 0xe5, 0x0a, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 0x67, 0x00, 0x70, 0x06, 0x00, 
0x77, 0xb9, 0xa9, 0xb4, 0x57, 0xcf, 0x19, 0xc3, 0x08, 0x57, 0x9c, 0xec, 0x7b, 0xb8, 0x02, 0xc0, 
0x19, 0x00, 0x9c, 0x01, 0xc0, 0x19, 0x00, 0xdc, 0x1f, 0x04, 0x46, 0x9e, 0x7f, 0xdb, 0x2d, 0x92, 
0x62, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, };

imgファイルの全文を掲載してしまうと大変なことになるので掲載はしませんが、メインのソースコードについては最後に全文掲載します。

なお、カプセルのドット絵はこちらのサイトの素材を使用させていただきました。感謝。

まとめ

以上、ムゲンカプセムのご紹介でした。間に資格試験の勉強を挟んだりしていたので、構想から完成まで2ヶ月ぐらいかかってしまいました。が、個人的にはなかなか面白いモノができたんじゃないかなと思っています。

一つやり残したこととして、レジェンドライダーのカプセムの扱いがあります。これについてはちょっと考えていることがありますので、やれそうであれば、別の作品として対応していきたいなと思っています。

ソースコード

ソースコードの全文はこちらになります。とはいえ、画像用のincludeファイル(img_capsem.h)は載せておりませんのでご了承ください。それだけで7,000行ぐらいになってしまいますので。

#include <M5Unified.h>
#include "AiEsp32RotaryEncoder.h"
#include "img_capsem.h"

#define PIN_SW_DISP 41
#define PIN_RE_SW    5
#define PIN_RE_A     6
#define PIN_RE_B     7
#define PIN_SW_MAIN  8
#define PIN_MP3_RX  38
#define PIN_MP3_TX  39 

#define ON  LOW
#define OFF HIGH

uint8_t sw_disp = OFF;
uint8_t sw_main = OFF;
uint8_t re_sw   = OFF;
uint8_t prev_sw_disp = OFF;
uint8_t prev_sw_main = OFF;
uint8_t prev_re_sw   = OFF;

#define MODE_A 0 // ガシャポンモード
#define MODE_B 1 // 自由選択モード
uint8_t mode = MODE_A;

#define MODE_CHANGE_TIME_MS 2000
bool is_sw_main_pushing = false;
unsigned long start_sw_main_pushing_point_ms = 0;

uint8_t handle_rotate_count = 0;
bool is_handle_rotated_an_eighth = false;

#define STATE_GASHAPON     0
#define STATE_CAPSULE      1
#define STATE_CAPSULE_OPEN 2
#define STATE_CAPSEM_WAIT  3
#define STATE_CAPSEM       4
#define STATE_ACTIVATE     5
#define STATE_SET_PIN      6
uint8_t state = STATE_GASHAPON;
uint8_t prev_state = STATE_GASHAPON;

#define STATE_CAPSULE_OPEN_TIME_MS 1200
#define STATE_CAPSEM_WAIT_TIME_MS  2000
#define STATE_ACTIVATE_TIME_MS     6800
unsigned long state_change_point_ms = 0;

void change_state(uint8_t new_state){
  state = new_state;
  Serial.print(F("STATE: "));
  switch(state){
  case STATE_GASHAPON:     Serial.println(F("GASHAPON"));     break;
  case STATE_CAPSULE:      Serial.println(F("CAPSULE"));      break;
  case STATE_CAPSULE_OPEN: Serial.println(F("CAPSULE_OPEN")); break;
  case STATE_CAPSEM_WAIT:  Serial.println(F("CAPSEM_WAIT"));  break;
  case STATE_CAPSEM:       Serial.println(F("CAPSEM"));       break;
  case STATE_ACTIVATE:     Serial.println(F("ACTIVATE"));     break;
  case STATE_SET_PIN:      Serial.println(F("SET_PIN"));      break;
  default: ;
  }
}

#define ROTARY_ENCODER_CYCLE 28
#define ROTARY_ENCODER_STEPS  2

int16_t re_start_value = 0;

AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(PIN_RE_A, PIN_RE_B, PIN_RE_SW, -1, ROTARY_ENCODER_STEPS);

void IRAM_ATTR readEncoderISR(){
    rotaryEncoder.readEncoder_ISR();
}

M5GFX display;
M5Canvas sprite_1(&display);
M5Canvas sprite_2(&display);
M5Canvas sprite_3(&display);
M5Canvas sprite_4(&display);
M5Canvas sprite_5(&display); // ピン配置用

#define DISP_WIDTH  128
#define DISP_HEIGHT 128

#define ANIMATION_SPEED_MS 100

int8_t capsem_id_list[] = {1,2,3,4,5,6,7,8,9,10,56};
int8_t capsem_max_items = sizeof(capsem_id_list) / sizeof(capsem_id_list[0]);
int8_t capsem_index = 0;

uint8_t frame_index = 0;
unsigned long prev_frame_change_point_ms = 0;

void set_gashapon_img(){
  sprite_1.fillSprite(TFT_BLACK);
  sprite_2.fillSprite(TFT_BLACK);
  sprite_3.fillSprite(TFT_BLACK);
  sprite_4.fillSprite(TFT_WHITE); // カプセルオープン時の間

  sprite_1.drawPng((std::uint8_t*)handle, 797, 0, 0);
  sprite_2.drawPng((std::uint8_t*)capsule, 601,22, 19);
  sprite_3.drawPng((std::uint8_t*)capsule_top, 430, 22, 0);
  sprite_3.drawPng((std::uint8_t*)capsule_bottom, 449, 22, 68);
}

void draw_set_pins(int8_t campsem_id){
  for(uint8_t i=0;i<6;i++){
    uint8_t bit_value = (campsem_id >> i) & 0x01;
    if(bit_value == 1){
      sprite_5.fillCircle(14+20*i, 64, 10, TFT_WHITE);
    }else{
      sprite_5.drawCircle(14+20*i, 64, 10, TFT_WHITE);
    }
  }
}

void set_capsem_img(int8_t capsem_id){
  sprite_1.fillSprite(TFT_BLACK);
  sprite_2.fillSprite(TFT_BLACK);
  sprite_3.fillSprite(TFT_BLACK);
  sprite_4.fillSprite(TFT_BLACK);
  sprite_5.fillSprite(TFT_BLACK);
  switch(capsem_id){
  case 1:
    sprite_1.drawPng((std::uint8_t*)impact_1, 2909, 0, 0);
    sprite_2.drawPng((std::uint8_t*)impact_2, 2270, 0, 0);
    sprite_3.drawPng((std::uint8_t*)impact_3, 2219, 0, 0);
    sprite_4.drawPng((std::uint8_t*)impact_4, 2219, 0, 0);
    break;
  case 2:
    sprite_1.drawPng((std::uint8_t*)transform_1, 2969, 0, 0);
    sprite_2.drawPng((std::uint8_t*)transform_2, 1407, 0, 0);
    sprite_3.drawPng((std::uint8_t*)transform_3,  501, 0, 0);
    sprite_4.drawPng((std::uint8_t*)transform_4, 1407, 0, 0);
    break;
  case 3:
    sprite_1.drawPng((std::uint8_t*)wing_1, 2928, 0, 0);
    sprite_2.drawPng((std::uint8_t*)wing_2, 2407, 0, 0);
    sprite_3.drawPng((std::uint8_t*)wing_3, 2165, 0, 0);
    sprite_4.drawPng((std::uint8_t*)wing_4, 2303, 0, 0);
    break;
  case 4:
    sprite_1.drawPng((std::uint8_t*)stream_1, 1747, 0, 0);
    sprite_2.drawPng((std::uint8_t*)stream_2, 1881, 0, 0);
    sprite_3.drawPng((std::uint8_t*)stream_3, 1856, 0, 0);
    sprite_4.drawPng((std::uint8_t*)stream_4, 1855, 0, 0);
    break;
  case 5:
    sprite_1.drawPng((std::uint8_t*)machinery_1, 3231, 0, 15);
    sprite_2.drawPng((std::uint8_t*)machinery_2, 3006, 0, 15);
    sprite_3.drawPng((std::uint8_t*)machinery_3, 3007, 0, 15);
    sprite_4.drawPng((std::uint8_t*)machinery_4, 3231, 0, 15);
    break;
  case 6:
    sprite_1.drawPng((std::uint8_t*)projection_1, 1810, 0, 17);
    sprite_2.drawPng((std::uint8_t*)projection_2, 3067, 0, 17);
    sprite_3.drawPng((std::uint8_t*)projection_3, 2961, 0, 17);
    sprite_4.drawPng((std::uint8_t*)projection_4, 1810, 0, 17);
    break;
  case 7:
    sprite_1.drawPng((std::uint8_t*)recovery_1, 2426, 0, 0);
    sprite_2.drawPng((std::uint8_t*)recovery_2, 2493, 0, 0);
    sprite_3.drawPng((std::uint8_t*)recovery_3, 2976, 0, 0);
    sprite_4.drawPng((std::uint8_t*)recovery_4, 2892, 0, 0);
    break;
  case 8:
    sprite_1.drawPng((std::uint8_t*)barrier_1, 3246, 0, 17);
    sprite_2.drawPng((std::uint8_t*)barrier_2, 2815, 0, 17);
    sprite_3.drawPng((std::uint8_t*)barrier_3, 2536, 0, 17);
    sprite_4.drawPng((std::uint8_t*)barrier_4, 2612, 0, 17);
    break;
  case 9:
    sprite_1.drawPng((std::uint8_t*)wonder_1, 1568, 0, 33);
    sprite_2.drawPng((std::uint8_t*)wonder_2, 2282, 0, 33);
    sprite_3.drawPng((std::uint8_t*)wonder_3, 3166, 0, 33);
    sprite_4.drawPng((std::uint8_t*)wonder_4, 3372, 0, 33);
    break;
  case 10:
    sprite_1.drawPng((std::uint8_t*)gravity_1, 3265, 0, 17);
    sprite_2.drawPng((std::uint8_t*)gravity_2, 3295, 0, 17);
    sprite_3.drawPng((std::uint8_t*)gravity_3, 3079, 0, 17);
    sprite_4.drawPng((std::uint8_t*)gravity_4, 3243, 0, 17);
    break;
  case 56:
    sprite_1.drawPng((std::uint8_t*)code_1, 1028, 0, 3);
    sprite_2.drawPng((std::uint8_t*)code_2, 1122, 0, 3);
    sprite_3.drawPng((std::uint8_t*)code_3, 1138, 0, 3);
    sprite_4.drawPng((std::uint8_t*)code_4, 2504, 0, 3);
    break;
  default:
    ;
  }
  draw_set_pins(capsem_id);
}

void animate_capsem(unsigned long now_ms, int inverval_ms){
  if(prev_frame_change_point_ms == 0){
    prev_frame_change_point_ms = now_ms;
  }

  if(now_ms - prev_frame_change_point_ms >= inverval_ms){
    switch(frame_index){
    case 0: sprite_1.pushSprite(0,0); break;
    case 1: sprite_2.pushSprite(0,0); break;
    case 2: sprite_3.pushSprite(0,0); break;
    case 3: sprite_4.pushSprite(0,0); break;
    default: ;
    }
    frame_index++;
    if(frame_index > 3){
      frame_index = 0;
    }
    prev_frame_change_point_ms = now_ms;
  }
}

void control_animation(unsigned long now_ms){
  // 状態遷移時処理
  switch(state){
  case STATE_GASHAPON:
    switch(prev_state){
    case STATE_CAPSULE:
    case STATE_CAPSULE_OPEN:
    case STATE_CAPSEM_WAIT:
    case STATE_CAPSEM:
    case STATE_ACTIVATE:
    case STATE_SET_PIN:
      set_gashapon_img();
      sprite_1.pushSprite(0,0); // ハンドル描画
    default:
      ;
    }
    break;
  case STATE_CAPSULE:
    if(prev_state == STATE_GASHAPON){
      sprite_2.pushSprite(0,0); // カプセル描画
    }
    break;
  case STATE_CAPSULE_OPEN:
    if(prev_state == STATE_CAPSULE){
      sprite_3.pushSprite(0,0); // カプセルオープン描画
    }
    break;
  case STATE_CAPSEM_WAIT:
    if(prev_state == STATE_CAPSULE_OPEN){
      sprite_4.pushSprite(0,0); // カプセルオープン時の間の描画
      set_capsem_img(capsem_id_list[capsem_index]); // カブセムの画像準備
    }
    break;
  case STATE_CAPSEM:
    switch(prev_state){
    case STATE_GASHAPON:
    case STATE_CAPSULE:
    case STATE_CAPSULE_OPEN:
    case STATE_CAPSEM_WAIT:
    case STATE_ACTIVATE:
    case STATE_SET_PIN:
      sprite_1.pushSprite(0,0); // カブセム・メインビジュアル描画
      break;
    default:
      ;
    }
    break;
  case STATE_ACTIVATE:
    switch(state){
    case STATE_CAPSEM:
    case STATE_SET_PIN:
      // アニメーション用変数の初期化。メインのアニメーション処理は永続処理の方で記述
      prev_frame_change_point_ms = 0;
      frame_index = 0;
    default:
      ;
    }
    break;
  case STATE_SET_PIN:
    if(prev_state == STATE_CAPSEM){
      sprite_5.pushSprite(0,0); // ピン配置描画
    }
    break;
  }

  // ハンドルが1/8回転したときの描画
  if(is_handle_rotated_an_eighth){
    switch(state){
    case STATE_GASHAPON:
      sprite_1.pushRotateZoom(&display, DISP_WIDTH/2, DISP_HEIGHT/2,45*handle_rotate_count,1,1); // ハンドルを回転させて描画
      break;
    case STATE_CAPSEM:
      sprite_1.pushSprite(0,0); // カブセム・メインビジュアル描画
      break;
    default:
      ;
    }
  }

  // 永続処理
  if(state == STATE_ACTIVATE){
    animate_capsem(now_ms, ANIMATION_SPEED_MS);
  }
}

//////////////////////////////////////////////////////////////////////

#include <DFPlayerMini_Fast.h>
#include <SoftwareSerial.h>

#define VOLUME 28 // 0〜30

SoftwareSerial ss_mp3_player(PIN_MP3_RX, PIN_MP3_TX);
DFPlayerMini_Fast mp3_player;

unsigned long sound_wait_start_point_ms = 0;
bool has_next_sound = false;

#define SOUND_FOLDER_COMMON 1
#define SOUND_FOLDER_CAPSEM 2

#define SOUND_POWER_ON      1
#define SOUND_HANDLE_ROTATE 2
#define SOUND_HANDLE_FINISH 3
#define SOUND_CAPSULE_OPEN  4

void play_sound(uint8_t folder_num, uint8_t track_num){
  mp3_player.playFolder(folder_num, track_num);
  Serial.print(F("Play Folder: "));
  Serial.print(folder_num);
  Serial.print(F(", Track: "));
  Serial.println(track_num);
}

void pause_sound(){
  mp3_player.pause();
}

void control_sound(unsigned long now_ms){
  ///////////// 状態変化直後の音声処理 /////////////
  switch(state){
  case STATE_GASHAPON:
    break;
  case STATE_CAPSULE:
    if(prev_state == STATE_GASHAPON){
      play_sound(SOUND_FOLDER_COMMON, SOUND_HANDLE_FINISH);
    }
    break;
  case STATE_CAPSULE_OPEN:
    if(prev_state == STATE_CAPSULE){
      play_sound(SOUND_FOLDER_COMMON, SOUND_CAPSULE_OPEN);
    }
    break;
  case STATE_CAPSEM_WAIT:
    if(prev_state == STATE_CAPSULE_OPEN){
      play_sound(SOUND_FOLDER_CAPSEM, capsem_id_list[capsem_index]);
    }
    break;
  case STATE_CAPSEM:
  case STATE_ACTIVATE:
  case STATE_SET_PIN :
  default:
    ;
  }

  // ハンドルが1/3回転したときの音声
  if(state == STATE_GASHAPON && is_handle_rotated_an_eighth){
    if(handle_rotate_count % 4 == 3){
      play_sound(SOUND_FOLDER_COMMON, SOUND_HANDLE_ROTATE);
    }
  }
}

//////////////////////////////////////////////////////////////////////

void setup(void){

  Serial.begin(115200);

  // 省電力化のため、CPU周波数を少し落とす場合に有効化。最大240MHz。
  setCpuFrequencyMhz(160);

  pinMode(PIN_SW_DISP, INPUT_PULLUP);
  pinMode(PIN_SW_MAIN, INPUT_PULLUP);
  pinMode(PIN_RE_A,  INPUT_PULLUP);
  pinMode(PIN_RE_B,  INPUT_PULLUP);
  pinMode(PIN_RE_SW, INPUT_PULLUP);

  rotaryEncoder.begin();
  rotaryEncoder.setup(readEncoderISR);
  rotaryEncoder.setBoundaries(-32768, 32767, false); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
  rotaryEncoder.setAcceleration(0);
  re_start_value = rotaryEncoder.readEncoder();

  // 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);
  }
  Serial.println(F("dfplayer online."));
  delay(800); // 間を開けるのが短すぎるとコマンドが有効にならないので注意
  mp3_player.volume(VOLUME);
  delay(500);
  play_sound(SOUND_FOLDER_COMMON, SOUND_POWER_ON);

  display.begin();
  display.fillScreen(TFT_BLACK);
  
  sprite_1.createSprite(DISP_WIDTH, DISP_HEIGHT);
  sprite_2.createSprite(DISP_WIDTH, DISP_HEIGHT);
  sprite_3.createSprite(DISP_WIDTH, DISP_HEIGHT);
  sprite_4.createSprite(DISP_WIDTH, DISP_HEIGHT);
  sprite_5.createSprite(DISP_WIDTH, DISP_HEIGHT);

  set_gashapon_img();
  sprite_1.pushSprite(0,0);
}

void loop(){
  sw_disp = digitalRead(PIN_SW_DISP);
  sw_main = digitalRead(PIN_SW_MAIN);
  re_sw   = digitalRead(PIN_RE_SW);

  unsigned long now_ms = millis();

  // スイッチ操作遷移
  switch(state){
  case STATE_GASHAPON:
    if(rotaryEncoder.encoderChanged()){
      //Serial.println(rotaryEncoder.readEncoder());
      int16_t re_value = rotaryEncoder.readEncoder();
      if(re_value - re_start_value >= ROTARY_ENCODER_CYCLE/8 || re_value - re_start_value <= ROTARY_ENCODER_CYCLE/8 * (-1)){
        re_start_value = re_value;
        is_handle_rotated_an_eighth = true;
        handle_rotate_count++;
      }
      
      if(handle_rotate_count == 9){
        // およそ一周したらカブセムを変更する
        capsem_index = random(capsem_max_items); 
        handle_rotate_count = 0;
        change_state(STATE_CAPSULE);
      }
    }
    break;
  case STATE_CAPSULE:
    if(prev_sw_disp == OFF && sw_disp == ON){
      change_state(STATE_CAPSULE_OPEN);
      state_change_point_ms = now_ms;
    }
    break;
  case STATE_CAPSULE_OPEN:
    break;
  case STATE_CAPSEM_WAIT:
    break;
  case STATE_CAPSEM:
    if(prev_sw_disp == OFF && sw_disp == ON){
      change_state(STATE_SET_PIN);
    }
    if(prev_sw_main == OFF && sw_main == ON){
      change_state(STATE_ACTIVATE);
      state_change_point_ms = now_ms;
    }
    if(rotaryEncoder.encoderChanged()){
      int16_t re_value = rotaryEncoder.readEncoder();

      if(re_value - re_start_value >= ROTARY_ENCODER_CYCLE/8){
        if(mode == MODE_A){
          // 1/8週でガシャポン状態に遷移する
          change_state(STATE_GASHAPON);
        }else{
          // 1/8周ごとにカブセムを変更する
          capsem_index++;
          if(capsem_index >= capsem_max_items){
            capsem_index = 0;
          }
          set_capsem_img(capsem_id_list[capsem_index]);
        }
        re_start_value = re_value;
        is_handle_rotated_an_eighth = true;
      }else if(re_value - re_start_value <= ROTARY_ENCODER_CYCLE/8 * (-1)){
        if(mode == MODE_A){
          // 1/8週でガシャポン状態に遷移する
          change_state(STATE_GASHAPON);
        }else{
          // 1/8周ごとにカブセムを変更する
          capsem_index--;
          if(capsem_index < 0){
            capsem_index = capsem_max_items - 1;
          }
        }
        set_capsem_img(capsem_id_list[capsem_index]);
        re_start_value = re_value;
        is_handle_rotated_an_eighth = true;
      }
    }
    break;
  case STATE_ACTIVATE:
    break;
  case STATE_SET_PIN:
    if(prev_sw_disp == OFF && sw_disp == ON){
      change_state(STATE_CAPSEM);
    }
    if(prev_sw_main == OFF && sw_main == ON){
      change_state(STATE_ACTIVATE);
      state_change_point_ms = now_ms;
    }
    break;
  default:
    ;
  }

  // 時間経過遷移
  if(state == STATE_CAPSULE_OPEN && now_ms - state_change_point_ms >= STATE_CAPSULE_OPEN_TIME_MS){
    change_state(STATE_CAPSEM_WAIT);
    state_change_point_ms = now_ms;
  }

  if(state == STATE_CAPSEM_WAIT && now_ms - state_change_point_ms >= STATE_CAPSEM_WAIT_TIME_MS){
    re_start_value = rotaryEncoder.readEncoder();; // 状態遷移前に値をリセットしておく
    change_state(STATE_CAPSEM);
  }

  if(state == STATE_ACTIVATE && now_ms - state_change_point_ms >= STATE_ACTIVATE_TIME_MS){
    change_state(STATE_CAPSEM);
  }
  
  // モードチェンジ
  if(prev_sw_main == OFF && sw_main == ON){
    is_sw_main_pushing = true;
    start_sw_main_pushing_point_ms = now_ms;
  }

  if(sw_main == OFF){
    is_sw_main_pushing = false;
  }

  if(is_sw_main_pushing && now_ms - start_sw_main_pushing_point_ms >= MODE_CHANGE_TIME_MS){
    if(mode == MODE_A){
      mode = MODE_B;
      Serial.println(F("MODE: B"));
      change_state(STATE_CAPSEM);
      set_capsem_img(capsem_id_list[capsem_index]);
    }else{
      mode = MODE_A;
      Serial.println(F("MODE: A"));
      change_state(STATE_GASHAPON);
    }
    is_sw_main_pushing = false;
  }

  ///////////////////////////////////////////

  control_animation(now_ms);
  control_sound(now_ms);

  if(is_handle_rotated_an_eighth){
    is_handle_rotated_an_eighth = false;
  }

  ///////////////////////////////////////////

  prev_sw_disp = sw_disp;
  prev_sw_main = sw_main;
  prev_re_sw   = re_sw;

  prev_state = state;

  delay(10);
}

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次