JavaScriptであのアトラクションを作ってみた

JavaScript

こんにちは。
ECF Techブログ
担当 Michiharu.Tです。

マイクロビットでブラウザとBluetooth通信を行ない、とあるゲームを作ってみました。まずは下の動画をご覧ください。

少しわかりづらいのですが、マイクロビットの傾きに合わせて画面上の地面が動きます。ボールを下の箱に入れることが目的です。

お気づきの方も多いかと思いますが、人気TV番組のアトラクションを再現してみました。(^ ^;)

今回は、こちらのゲームプログラムの紹介・解説をしていきたいと思います。よろしくお願いします。

対象読者

本記事は次のような方を読者として想定しています。

  • JavaScriptの基本的なプログラミングを知っている方
  • Web BLEのしくみに興味のある方
  • 2Dゲームに興味のある方。2Dゲームフレームワークに興味のある方

プログラム

まずは、HTMLファイルにJavaScriptのプログラムを含めたプログラム全文をご確認ください。コピー&ペーストでファイルを作成すれば、実際に動作確認することも可能です。

<!DOCTYPE html>
<htm>
<head>
    <script src="https://cdn.jsdelivr.net/npm/phaser@3.15.1/dist/phaser.min.js"></script>
    <meta charset="utf-8">
    <style>
        #myCanvas{
            margin:auto;
            width:810px;
            height:610px;
        }
    </style>
</head>
<body>
    <input type="button" value="スタート" onclick="pushStart()" />
    <input type="button" value="ストップ" onclick="pushStop()" />
    <div id="myCanvas" name="myCanvas">
    </div>
    <script>

    //ゲーム情報設定
    var config = {
        type: Phaser.AUTO,
        width: 800,
        height: 600,
        parent: 'myCanvas',
        physics: {
            default: "matter",
            matter:{
                debug:false
            }
        },
        scene: {
            preload: preload,
            create: create,
            update: update
        }
    };
    //変数準備
    var dir = 1;

    function preload ()
    {
      //背景の画像
      this.load.image('back', 'assets/back.png');
      this.load.image('ground', 'assets/ground.png');
      this.load.image('ball', 'assets/ball.png');
      this.load.image('box_bottom', 'assets/box_bottom.png');
      this.load.image('box_side', 'assets/box_side.png');
    }

    function create ()
    {
      //背景の設定
      this.add.image(400,300,'back');
      //地面画像を物理エンジン塔載でスプライト化
      ground = [];
      for( let i = 0; i < 4; i++){
          let wk;
          if( i % 2 == 0)
              wk = this.matter.add.image(350, 100 + i*120, 'ground');
          else
              wk = this.matter.add.image(450, 100 + i*120, 'ground');
          //物理演算を行わない
          wk.setStatic(true);
          //X,Yのサイズを倍率で指定
          wk.setScale(1.5, 0.5);
          //摩擦係数を0
          wk.body.friction = 0;
          //角度を0にする。
          wk.setAngle(0);
          ground.push(wk);
      }
      //ボールの生成と設定
      ball = this.matter.add.image(400, 0, 'ball');
      ball.setStatic(true);

      //ゴール箱の作成
      box1 = this.matter.add.image(100,590,'box_bottom');
      box2 = this.matter.add.image(55,555,'box_side');
      box3 = this.matter.add.image(145,555,'box_side');
      box1.setStatic(true);
      box2.setStatic(true);
      box3.setStatic(true);
    }

    function update ()
    {
        //ゴール箱の移動
        box1.x += dir;
        box2.x += dir;
        box3.x += dir;
        //左に折り返す
        if( box1.x >= 250 ){
            dir = -1;
        }
        //右に折り返す
        if( box1.x <= 100 ){
            dir = 1;
        }
    }
    //ゲームの開始
    var game = new Phaser.Game(config);

    var charaA;
    function pushStart(){
        console.log("pushStart");
        //microbit 加速度サービスのUUID
        let acceleUuid = "E95D0753-251D-470A-A062-FA1922DFA9A8";
        //取得用characteristics
        let acceleChar = "E95DCA4B-251D-470A-A062-FA1922DFA9A8";
        //デバイスの取得
        navigator.bluetooth.requestDevice(
            //デバイスにフィルターかけて、マイクロビットだけが見えるようにできる
            {
                filters:[ {namePrefix:'BBC micro:bit'} ],
                optionalServices:[acceleUuid.toLowerCase()]
            }
        ).then(         //デバイス取得できたら
            device => {
                console.log('Connecting micro:bit');
                return device.gatt.connect();
            }
        ).then(         //接続できたら
            server => {
                console.log('Getting Service');
                return server.getPrimaryService(acceleUuid.toLowerCase());
            }
        ).then(         //サービスが取得できたら
            service => {
                console.log('Getting Characteristics');
                //ボールの準備
                ball.setStatic(false);
                ball.setCircle(30);
                ball.setBounce(0.5);
                ball.body.friction = 0;
                //加速度のcharacteristicsの取得を待つ
                return Promise.all([
                    //characteristicsの取得
                    service.getCharacteristic(acceleChar.toLowerCase())
                    .then(chara => {
                        //停止できるようにグローバルに保持
                        charaA = chara;                                
                        //通知サービスを開始
                        chara.startNotifications();
                        //リスナー関数の設定
                        chara.addEventListener('characteristicvaluechanged',listenerAccele);
                    })
                ]);
            }
        )
        .catch(
            error => {
                //途中でエラー発生したらエラー出力
                console.log('sorry Error!');
                console.log(error.code);
                console.log(error.message);
                console.log(error.name);
            }
        )
    }
    //通知停止
    function pushStop(){
        if( charaA ){
            //通知の停止
            charaA.stopNotifications().then(() => {
                //リスナーの解放
                charaA.removeEventListener('characteristicvaluechanged',listenerAccele);
                console.log("stop notification");
            });
        }
    }

    //加速度情報リスナ
    function listenerAccele(event){
        let chara = event.target;
        //valueがDataViewオブジェクトになっている。microbit仕様によると値は
        //Signed INT16(リトルエンディアン)で提供されるので、その情報で取得
        let x_ax = (chara.value.getInt16(0,true));
        //取得値は-1024~1023の間で示されるため、おおよその角度に変換
        x_ax = parseInt(x_ax/12);
        if( x_ax >= 15 )x_ax = 15;
        if( x_ax <= -15 )x_ax = -15;
        for( let i = 0; i < 4; i++){
            ground[i].setAngle(x_ax);
        }
    }
</script>
</body>
</html>

動作について

使用している画像

今回のプログラムに使われている画像を提供いたします。ご自由にご利用ください。
背景

ボール

箱の底

箱の横

地面

Webサーバー上で動作する

本プログラムを動作させるには、Webサーバー上で実行する必要があります。ブラウザでHTMLファイルを開くだけでは動作しません。

Windows環境で簡単にWebサーバーを立ち上げるにはXAMPPがオススメです。GUIも充実していて、簡単に構築ができます。下記のサイトからダウンロードできます。

Windowsのプライバシー設定

Windows10では、プライバシー設定によりブラウザからの無線通信機能が利用できない場合がありますので、次のように設定を行ないます。

Windowsの設定から、プライバシーを選びます。

左メニューから無線を選び、「アプリがデバイスの無線を利用できるようにする」をONにします。

マイクロビット内のプログラム

マイクロビット内のプログラムを作る際は、Microsoft MakeCodeを使って、次のようにして作成しておきます。MakeCodeへのアクセスは下のリンクからご利用ください。

Microsoft MakeCode for micro:bit
A Blocks / JavaScript code editor for the micro:bit powered by Microsoft MakeCode.

新規プロジェクトを作成したら、ブロックのグループから高度なブロックをクリックします。

拡張機能をクリックします。

Bluetoothをクリックします。

ピンクのボタンを選びます。

Bluetoothのグループが使えるようになります。

Bluetoothグループのブロックを使って、次のようにプログラムを組み立てます。

右上の歯車からプロジェクトの設定を選びます。

「No Pairing・・・」をONにしておきます。

ペアリング

ブラウザでゲームを起動後、マイクロビットの電源をオンにします。続いて、ゲーム画面のスタートボタンをクリックします。

デバイスを選択する画面が表示されますので、BBC micro: bitを選択します。

ブラウザとマイクロビットの接続が確立すると、ボールが落ちてきて、ゲームスタートです。

コード解説

前置きが長くなりましたが、プログラムの説明に入りたいと思います。命令ごとの細かい説明はコード内のコメントとして記述しています。

ゲーム部分の解説

ゲームプログラムの部分には、JavaScript用の2Dゲームフレームワーク「Phaser」を利用しています。
Phaserの基本については、本ブログ内の「JavaScriptゲームライブラリPhaserを使ってみた。」の記事にまとめています。Phaserの基本部分についての簡単な解説とポイントとなる部分を見ていきます。

物理演算ライブラリの選択

物理演算を行なうにあたり、MatterJSというライブラリを使用します。defaultプロパティに"matter"を設定すると、このMatterJSを使うモードとなります。

physics: {
    default: "matter",
    matter:{
        debug:false
    }
}

このdefaultプロパティに初期状態で設定されているarcadeでは、地面の傾きなどを物理演算に反映できません。

ゴール箱の作成

ゴール箱の作成は次の部分です。

//ゴール箱の作成
box1 = this.matter.add.image(100,590,'box_bottom');
box2 = this.matter.add.image(55,555,'box_side');
box3 = this.matter.add.image(145,555,'box_side');
box1.setStatic(true);
box2.setStatic(true);
box3.setStatic(true);

ゴール箱はbox1,box2,box33つのオブジェクトを組み合わせて出来ています。

また物理演算は行わないため、setStatic(true)を実行します。

ゴール箱の移動

ゴール箱は左右移動をします。これを担っているのがupdate文内の下記のプログラムです。update文はphaserから呼び出されるゲームループを担う部分です。デフォルトでは1秒に60回呼び出しが行われます。

//ゴール箱の移動
box1.x += dir;
box2.x += dir;
box3.x += dir;
if( box1.x >= 250 ){
    dir = -1;
}
if( box1.x <= 100 ){
    dir = 1;
}

dirは向きを表す変数です。右端(x = 250)までいったら、向きは左方向dir = -1となり、左端(x = 100)までいったら右方向dir = 1となります。

ここでポイントとなるのが、各スプライトのプロパティxを直接操作している点です。phaserが提供するスプライトには、setVelocityXなどの移動を行なえるメソッドがあるのですが、物理演算無効の命令setStatic(true)があると機能しないため、この方法を取っています。

無線通信部分の解説

次にBLE(Bluetooth Low Energy)による無線通信について解説します。

Web BLEの基本は本ブログの「ブラウザ(JavaScript)でマイクロビットとBLE!」で詳説しています。ご興味のある方はご覧ください。BLE通信とゲームをつなげる部分は、下のlistenerAcceleメソッドです。このメソッドはマイクロビットからの加速度情報の通知を受け取った都度動作します。

function listenerAccele(event){
    let chara = event.target;
    //valueがDataViewオブジェクトになっている。microbit仕様によると値は
    //Signed INT16(リトルエンディアン)で提供されるので、その情報で取得
    let x_ax = (chara.value.getInt16(0,true));
    //取得値は-1024~1023の間で示されるため、おおよその角度に変換
    x_ax = parseInt(x_ax/12);
    if( x_ax >= 15 )x_ax = 15;
    if( x_ax <= -15 )x_ax = -15;
    for( let i = 0; i < 4; i++){
        ground[i].setAngle(x_ax);
    }
}

加速度情報の取得

マイクロビットからの加速度通知は、下のように6バイトの領域に2バイトずつ格納されます。

今回はXの傾き(加速度)の情報を取得したいので、chara.value.getInt16(0,true)の記述で取得します。getInt16メソッドは、バイト配列から16ビット(2バイト)の符号つき数値として値を取り出してくれるメソッドです。

最初の引数0は、0バイト目から値を取得することを示します。2つ目の引数はエンディアンの指定です。マイクロビットの加速度情報はリトルエンディアンでデータが提供されるため、引数をtrueにします。

INFO:
エンディアンはメモリへの格納方式を表すものです。メモリに下位バイトから書き込む方式をリトルエンディアン、上位バイトから書き込む方式をビッグエンディアンと言います。

角度への反映

角度への反映は少し適当ですが、取得できる範囲の値を12で割っただけです。-1024~1023までを-90°~90°の範囲に表現し直すだけなので、1024÷90のおおよその値が12です。

また、この計算で求められた角度の値は15°以上にならないように制御しています。傾きすぎると地面同士がぶつかるためです。

ゲームスタートのタイミング

本ゲームではマイクロビットとの無線接続が確立されると、下のプログラムによって上空に停止していたボールが落下します。

//ボールの準備
//物理演算を有効化
ball.setStatic(false);
//ボールの半径
ball.setCircle(30);
//ボールの弾み具合
ball.setBounce(0.5);
//ボールの摩擦
ball.body.friction = 0;

おわりに

本日は以上とさせていただきます。最後までありがとうございました。Web BLEと2Dゲームフレームワークを組み合わせた内容で、やや盛りだくさんな内容になりましたが、マイクロビットが画面上のゲームとつながる楽しさを実感してもらえたら幸いです。


合同会社イー・シー・エフでは、子ども向けプログラミングなどの教育講座を実施しています。プログラミング教室の案内や教育教材の情報、また関連するご相談・問い合わせにつきましては下記よりご確認ください。

ECFエデュケーション
キッズも大人も。沖縄からITのマナビを応援します
タイトルとURLをコピーしました