こんにちは、技術開発ユニットの三浦です。
2月になりました。年度末に向けてプロジェクトをどうやって収束させようかとか、来年度はどんなテーマに取り組もうかとか、色々と考えないといけない大事な時期なのですが、まもなく花粉症がやってきます。花粉症と付き合いながらこの大事な時期を乗り越えるのが、私にとって毎年度の最後の山場になっています。
さて、Minecraft(マイクラ)というゲームが好きなのですが、マイクラの世界で色々な建物を作ると、実際にこの世界を歩いたり走ったりしてみたいなぁと考えることがあります。そんな想いを形にした、夢のような装置を今回作りました。
Minecraft:Fit・・・通称「マイクラ:フィット」です。
Raspberry PiではMinecraft Pi Editionというゲームを動かすことが出来ます。マイクラの他のエディションと比べると内容は少ないものの、フリーでインストールすることが出来、APIを通じてプログラムから操作することも出来ます。以前Raspberry PiとMinecraft Pi Editionを使ってこんな装置を作った話を記事にしました。
Raspberry Piに加え、今回はmicro:bitという教育用のマイコンを使って、自分が走ったりジャンプした動作に合わせてマイクラの世界を進むことが出来るようにしました。楽しいだけでなく、運動不足も解消できて一石二鳥です。
マイクラ:フィットのプレイの様子
ゲームスタート。マイクラの世界が広がります。
自分が走り出すと、マイクラの世界でも進むことが出来ます。
途中で高い壁にぶつかってしまいました。
その場でジャンプすると、目の前の壁が消えて進めるようになります。
海に飛び込んでしまいそうですが・・・
足場が自動的に作られるので大丈夫です。
ゲームを終了すると、運動の結果が確認できます。
では、この装置の作り方をこれからご紹介します。
マイクラ:フィットの構成
ハードウエア
今回使用したハードウェアです。
- micro:bit v2(+電池ケース/ケーブルなど)
- Raspberry Pi Model B+(+キーボード/ディスプレイなど)
Raspberry Piは何度か使ったことがありますが、micro:bitは今回初めて使いました。
想像以上にいろいろな機能が備わっていて、かつそれらを簡単に利用できることがわかりました。 これからどんどん使っていきたいなと思いました。
ソフトウエア
micro:bitはMicrosoft MakeCodeというオンラインエディタでプログラムの作成や書き込みが出来ます。micro:bitでは搭載された加速度センサーの値をBLEで送信するプログラムを動かします。
Raspberry PiはフォワグラウンドでMinecraft Pi Editionを動かし、一方バックグラウンドでPythonのプログラムを動かしmicro:bitのBLEの受信や APIを利用してマイクラのプレイヤーの操作を行います。Pythonは3系を利用し、以下のライブラリを使用しました。
- bluepy PythonでBLE(Bluetooth Low Energy)接続を行う。
- mcpy Minecraft Pi EditionのAPIをPythonde扱う。
- keyboard キーボード入力をPythonで受け取る。テスト用。
構成を一枚の絵にすると以下のようになります。
作り方
それでは具体的にプログラムの作り方などを紹介していきます。
micro:bit
micro:bitはLEDやボタン、様々なセンサーやBLEを含む通信機能を持っていて、付属していた電池ケースを使って電池で稼働させることが出来ました。これらの機能はmicro:bitにプログラムを書き込んで利用するのですが、そのプログラムはWebブラウザで利用できるMicrosoft MakeCodeというエディタで開発することが出来ます。開発はブロックタイプ/Python/Javascriptを使って行うことが出来るようです。今回micro:bitに必要な機能はブロックエディタで簡単に実装することが出来ます。
BLEのセットアップ
こちらがmicro:bitのブロックエディタです。
まずBLEの機能を利用するための拡張機能を追加します。「高度なブロック」の中にある「拡張機能」を選びます。
拡張機能の検索ボックスに「bluetooth」と入力すると、「bluetooth」の拡張機能が見つかります。無線機能(Radio)と一緒に使うことが出来ないため、その機能が取り除かれるという旨のメッセージが表示されるので、そちらを承認するとBluetoothのブロックが追加されます。
続いて右上の歯車アイコンからプロジェクトの設定を選び、一番上の「No Pairing Required: Anyone can connect via Bluetooth.」を有効にします。
後は以下のようなブロックを組んでプログラムを作ります。ここでは加速度センサーとmicro:bitで押されたボタンのステータスをBLEで送信するようなプログラムを作りました。
最後にmicro:bitにプログラムを書き込めば、micro:bitの準備は完了です。
Raspberry Pi
Minecraft Pi Editionのセットアップ
こちらのTopicにもあるように最新のRaspberry Pi OSである「Debian ‘Bullseye’」ではMinecraft Pi Editionは実行できなくなったようです。
以前のOS「Debian ‘Buster‘」はRaspberry Pi OS (Legacy)として利用できるので、SDカードにはこのOSを書き込みます。OSを書き込んでRaspberry Piを立ち上げたらメニューの「Recommended Software」からMinecraft Pi Editionをインストールすることが出来ます。
bluepyのセットアップ
次にPythonのBLEライブラリbluepyをインストールします。
$ sudo apt-get install libglib2.0-dev $ sudo pip3 install bluepy
micro:bitとの接続
さきほどのmicro:bitのセットアップでmicro:bitからは加速度センサーの測定値がBLEで送信されている状態です。これをRaspberry Piで受信できるかを試してみます。
周囲のBLEデバイスからmicro:bitを特定する
身の回りには様々なBLEデバイスがあり、電波が発信されています。その中から目的のmicro:bitのものを見つけます。
以下のコードをroot権限で実行すると、周囲のBLEデバイスの情報が取得できます。その中でComplete Local Name
がBBC micro:bit
で始まるものがmicro:bitから発信されているものです。このデバイスのdevice_addr
とaddr_type
が接続に必要になるので控えておきます。
from bluepy.btle import Scanner scanner = Scanner() devices = scanner.scan(10.0) for dev in devices: print('-------------------------') print(f'device_addr:{dev.addr}, addr_type:{dev.addrType}') for data in dev.getScanData(): if 'Complete Local Name' in data[1]: print(data)
実行コマンド例:
sudo python3 ble_scanner.py
結果:
------------------------- device_addr:xx:xx:xx:xx:xx:xx, addr_type:random (9, 'Complete Local Name', 'BBC micro:bit [zuzuz]') -------------------------
micro:bitの該当のセンサーのUUIDを調べる
次にmicro:bitが発信するBLEから該当のデータを読み取るために、対応するSERVICEとCHARACTERISTICSのUUIDを調べます。 以下で調べることが出来ました。
lancaster-university.github.io
センサーの値を読み取るプログラム
最後に以下のプログラムを実行して、micro:bitの加速度センサーの値がRaspberry PiのTerminalに表示されることを確認します。また、micro:bitのAボタンを長押しすると、プログラムを終了することが出来ます。
from bluepy.btle import DefaultDelegate, Peripheral,ADDR_TYPE_RANDOM MAC_ADDRESS = 'xx:xx:xx:xx:xx:xx' #ACCELEROMETER SERVICE/CHARACTERISTICS UUID ACC_SERVICE_UUID = 'E95D0753251D470AA062FA1922DFA9A8' ACC_CHARACTERISTICS_UUID = 'E95DCA4B251D470AA062FA1922DFA9A8' #BUTTON SERVICE/BUTTON A CHARACTERISTICS UUID BTN_SERVICE_UUID = 'E95D9882251D470AA062FA1922DFA9A8' BTN_A_CHARACTERISTICS_UUID = 'E95DDA90251D470AA062FA1922DFA9A8' # 接続設定 peripheral = Peripheral(MAC_ADDRESS, ADDR_TYPE_RANDOM) acc_service = peripheral.getServiceByUUID(ACC_SERVICE_UUID) acc_characteristic = peripheral.getCharacteristics(uuid=ACC_CHARACTERISTICS_UUID) btn_service = peripheral.getServiceByUUID(BTN_SERVICE_UUID) btn_A_characteristic = peripheral.getCharacteristics(uuid=BTN_A_CHARACTERISTICS_UUID) while True: # 値の読み取り acc_read_data = acc_characteristic[0].read() btn_read_data =btn_A_characteristic[0].read() # 加速度センサー x = int.from_bytes(acc_read_data[0:2], byteorder='little', signed=True) y = int.from_bytes(acc_read_data[2:4], byteorder='little', signed=True) z = int.from_bytes(acc_read_data[4:6], byteorder='little', signed=True) # 加速度の大きさの計算 print((x**2 + y**2 + z**2)**0.5) # ボタンが長押しされたことを検知したら終了 if int(btn_read_data[0]) == 2: break
Minecraft Pi Editionの操作
Minecraft Pi Editionではゲーム中のプレイヤーの方向調整がAPIで指定できないようなので、とりあえずプレイヤーはx軸の正方向に限定して移動することにします。移動中に障害物が現れることを想定し、加速度センサーの値によって以下のようにプレイヤーを操作できるようにしました。
- 加速度センサーの値が一定の値を超えたら走り出す
- 一定の値を超えている間は走り続ける
- 1ブロック分の段差は乗り越えられる
- 2ブロック以上の高さの壁にぶつかった場合はその場で停止する
- 停止中に加速度センサーの値が一定の値を超えたら「ジャンプをした」と判定して目の前のブロックを消して走行可能にする
- 移動場所の足場が水なら土にブロックを変更して走行可能にする
- micro:bitのAボタンが長押しされたら結果を表示してゲームを終了する
加速度センサーの値の観察
加速度センサーの値がどの程度になったら走ったりジャンプしたのかを判定するために、実際の測定データを見てそのしきい値を判定します。
micro:bitではシリアル出力が可能なので、PCにUSBで接続した状態でmicro:bitの測定値をエディタで可視化することが出来ます。
結果用のカロリー計算
走行状態の時の秒数とジャンプした回数をゲーム中変数に控えておき、ゲーム終了時にその値に基づいて消費カロリーを計算するようにしました。 カロリー計算はこちらのサイトを参考にしました。
プログラム全体
こちらがRaspberry Piで動かすプログラムの全体です。ちょこちょこ調整不足な点もありますが・・・。
from mcpi import minecraft from bluepy.btle import DefaultDelegate, Peripheral,ADDR_TYPE_RANDOM import time #BLE Setting MAC_ADDRESS = 'xx:xx:xx:xx:xx:xx' #ACCELEROMETER SERVICE/CHARACTERISTICS UUID ACC_SERVICE_UUID = 'E95D0753251D470AA062FA1922DFA9A8' ACC_CHARACTERISTICS_UUID = 'E95DCA4B251D470AA062FA1922DFA9A8' #BUTTON SERVICE/BUTTON A CHARACTERISTICS UUID BTN_SERVICE_UUID = 'E95D9882251D470AA062FA1922DFA9A8' BTN_A_CHARACTERISTICS_UUID = 'E95DDA90251D470AA062FA1922DFA9A8' # 接続設定 peripheral = Peripheral(MAC_ADDRESS, ADDR_TYPE_RANDOM) acc_service = peripheral.getServiceByUUID(ACC_SERVICE_UUID) acc_characteristic = peripheral.getCharacteristics(uuid=ACC_CHARACTERISTICS_UUID) btn_service = peripheral.getServiceByUUID(BTN_SERVICE_UUID) btn_A_characteristic = peripheral.getCharacteristics(uuid=BTN_A_CHARACTERISTICS_UUID) #加速度センサーの動作判定しきい値 RUN_TH = 1500 #走行中 JUMP_TH = 2000 #ジャンプ KCAL_PRE_SEC = 0.15 #ジョギング1秒あたりの消費カロリー(kcal) KCAL_PRE_JUMP = 1.5 #ジャンプ1回あたりの消費カロリー(kcal) #Minecraft Setting mc = minecraft.Minecraft.create() status = 'NOTRUN' run_score = 0 jump_score = 0 start_time = time.time() #移動場所に高さ2以上の壁があるかを判定する def check_passable(next_pos): for dy in range(0, 3): #空気以外のブロックがあるかを判定 if mc.getBlock(next_pos.x, next_pos.y+dy, next_pos.z) != 0: return False else: return True #移動場所の高さ2以上の壁を消して通行可能にする def make_passable(next_pos): for dy in range(0,3): mc.setBlock(next_pos.x, next_pos.y+dy, next_pos.z,0) #走行中か確認する def check_run(acc_characteristic): acc_read_data = acc_characteristic[0].read() x = int.from_bytes(acc_read_data[0:2], byteorder='little', signed=True) y = int.from_bytes(acc_read_data[2:4], byteorder='little', signed=True) z = int.from_bytes(acc_read_data[4:6], byteorder='little', signed=True) strength = (x**2 + y**2 + z**2)**0.5 print(strength) return True if strength > RUN_TH else False #ジャンプしたか確認する def check_jump(acc_characteristic): acc_read_data = acc_characteristic[0].read() x = int.from_bytes(acc_read_data[0:2], byteorder='little', signed=True) y = int.from_bytes(acc_read_data[2:4], byteorder='little', signed=True) z = int.from_bytes(acc_read_data[4:6], byteorder='little', signed=True) strength = (x**2 + y**2 + z**2)**0.5 print(strength) return True if strength > JUMP_TH else False #メインのループ処理 while True: if status != 'STOP': if check_run(acc_characteristic): if status == 'NOTRUN': start_time = time.time() status = 'RUN' else: if status == 'RUN': run_score += time.time() - start_time status = 'NOTRUN' if status == 'RUN': pos = mc.player.getPos() pos.x = pos.x + 0.3 if not check_passable(pos): pos.y = pos.y + 1.0 if not check_passable(pos): run_score += time.time() - start_time status = 'STOP' continue if mc.getBlock(pos.x, pos.y-1,pos.z) in [8, 9]: mc.setBlock(pos.x, pos.y-1,pos.z,3) mc.player.setPos(pos) if status == 'STOP': if check_jump(acc_characteristic): #test jump_score += 1 pos = mc.player.getPos() pos.x = pos.x + 0.3 make_passable(pos) status = 'NOTRUN' btn_read_data =btn_A_characteristic[0].read() # Aボタンが長押しされたら終了処理をする if int(btn_read_data[0]) == 2: kcal = KCAL_PRE_SEC * run_score + KCAL_PRE_JUMP * jump_score victory_str = f'Victory! ' mc.postToChat('Victory!\n') mc.postToChat(f'RUNNING TIME:{run_score:.2f}s JUMP_COUNT:{jump_score}count\n') mc.postToChat(f'CALORIE:{kcal:.2f}kcal!\n') break time.sleep(0.1)
まとめ
ということで、今回はmicro:bitとRaspberry Piを使ってゲームを作ってみた話をご紹介しました。 今度は下半身だけでなく、上半身も鍛えられるようなゲームにしたいと思いました。
今回初めて触ったmicro:bitはとても楽しいデバイスで、子供と色々作るのにも最適です。 これからも活用していきたいです!