CCCMKホールディングス TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

ブログタイトル

micro:bitとRaspberry PiをBLEでつなげてマイクラの世界をジョギング出来る装置「マイクラ:フィット」を作りました。

こんにちは、技術開発ユニットの三浦です。

2月になりました。年度末に向けてプロジェクトをどうやって収束させようかとか、来年度はどんなテーマに取り組もうかとか、色々と考えないといけない大事な時期なのですが、まもなく花粉症がやってきます。花粉症と付き合いながらこの大事な時期を乗り越えるのが、私にとって毎年度の最後の山場になっています。

さて、Minecraft(マイクラ)というゲームが好きなのですが、マイクラの世界で色々な建物を作ると、実際にこの世界を歩いたり走ったりしてみたいなぁと考えることがあります。そんな想いを形にした、夢のような装置を今回作りました。

Minecraft:Fit・・・通称「マイクラ:フィット」です。

Raspberry PiではMinecraft Pi Editionというゲームを動かすことが出来ます。マイクラの他のエディションと比べると内容は少ないものの、フリーでインストールすることが出来、APIを通じてプログラムから操作することも出来ます。以前Raspberry PiとMinecraft Pi Editionを使ってこんな装置を作った話を記事にしました。

techblog.cccmk.co.jp

Raspberry Piに加え、今回はmicro:bitという教育用のマイコンを使って、自分が走ったりジャンプした動作に合わせてマイクラの世界を進むことが出来るようにしました。楽しいだけでなく、運動不足も解消できて一石二鳥です。

マイクラ:フィットのプレイの様子

ゲームスタート。マイクラの世界が広がります。

f:id:miu4930:20220131180634p:plain
今日も元気に運動だ!

自分が走り出すと、マイクラの世界でも進むことが出来ます。

f:id:miu4930:20220131180800p:plain
いい汗かいてる!

途中で高い壁にぶつかってしまいました。

f:id:miu4930:20220131180847p:plain
ここでお終いか・・・

その場でジャンプすると、目の前の壁が消えて進めるようになります。

f:id:miu4930:20220131180937p:plain
自分の筋肉で道をひらく!

海に飛び込んでしまいそうですが・・・

f:id:miu4930:20220131181024p:plain
今度こそお終いか・・・

足場が自動的に作られるので大丈夫です。

f:id:miu4930:20220131181051p:plain
自分の筋肉を信じよう!

ゲームを終了すると、運動の結果が確認できます。

f:id:miu4930:20220131181131p:plain
ビクトリー!!

では、この装置の作り方をこれからご紹介します。

マイクラ:フィットの構成

ハードウエア

今回使用したハードウェアです。

  • micro:bit v2(+電池ケース/ケーブルなど)
  • Raspberry Pi Model B+(+キーボード/ディスプレイなど)

Raspberry Piは何度か使ったことがありますが、micro:bitは今回初めて使いました。

microbit.org

想像以上にいろいろな機能が備わっていて、かつそれらを簡単に利用できることがわかりました。 これからどんどん使っていきたいなと思いました。

ソフトウエア

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で受け取る。テスト用。

構成を一枚の絵にすると以下のようになります。

f:id:miu4930:20220131182230p:plain
構成図

作り方

それでは具体的にプログラムの作り方などを紹介していきます。

micro:bit

micro:bitはLEDやボタン、様々なセンサーやBLEを含む通信機能を持っていて、付属していた電池ケースを使って電池で稼働させることが出来ました。これらの機能はmicro:bitにプログラムを書き込んで利用するのですが、そのプログラムはWebブラウザで利用できるMicrosoft MakeCodeというエディタで開発することが出来ます。開発はブロックタイプ/Python/Javascriptを使って行うことが出来るようです。今回micro:bitに必要な機能はブロックエディタで簡単に実装することが出来ます。

BLEのセットアップ

こちらがmicro:bitのブロックエディタです。

f:id:miu4930:20220131182436p:plain
Makecode

まずBLEの機能を利用するための拡張機能を追加します。「高度なブロック」の中にある「拡張機能」を選びます。

拡張機能の検索ボックスに「bluetooth」と入力すると、「bluetooth」の拡張機能が見つかります。無線機能(Radio)と一緒に使うことが出来ないため、その機能が取り除かれるという旨のメッセージが表示されるので、そちらを承認するとBluetoothのブロックが追加されます。

続いて右上の歯車アイコンからプロジェクトの設定を選び、一番上の「No Pairing Required: Anyone can connect via Bluetooth.」を有効にします。

後は以下のようなブロックを組んでプログラムを作ります。ここでは加速度センサーとmicro:bitで押されたボタンのステータスをBLEで送信するようなプログラムを作りました。

f:id:miu4930:20220131182652p:plain
作ったプログラム

最後にmicro:bitにプログラムを書き込めば、micro:bitの準備は完了です。

Raspberry Pi

Minecraft Pi Editionのセットアップ

こちらのTopicにもあるように最新のRaspberry Pi OSである「Debian ‘Bullseye’」ではMinecraft Pi Editionは実行できなくなったようです。

forums.raspberrypi.com

以前の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 NameBBC micro:bitで始まるものがmicro:bitから発信されているものです。このデバイスのdevice_addraddr_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の測定値をエディタで可視化することが出来ます。

f:id:miu4930:20220131182745p:plain
静止中と走行中の加速度センサーのグラフ

結果用のカロリー計算

走行状態の時の秒数とジャンプした回数をゲーム中変数に控えておき、ゲーム終了時にその値に基づいて消費カロリーを計算するようにしました。 カロリー計算はこちらのサイトを参考にしました。

keisan.casio.jp

プログラム全体

こちらが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はとても楽しいデバイスで、子供と色々作るのにも最適です。 これからも活用していきたいです!