micro:bit 平面台ロボット制御(バーチャルゲーム編)

ロボット

micro:bitで操作するPCゲーム作成

 水平台ロボットを作製する前に、micro:bitから取得される加速度の特性(追従性、誤差等)を把握する為、平面台ロボット+迷路ゲームの挙動に近い仮想的なゲームを作成する。

  • ゲーム開発環境
    • ゲーム開発環境として、UnityとUnreal Engine(両者とも個人的な利用であれば無償で使用可能)が有名であるが、個人的な嗜好(ビジュアル スクリプティングのブループリントよりC#の方が扱い易い)でUnityを選択
    • Unityは、ゲーム画面を構成するオブジェクト(2D/3D図形)に対してその性質を設定したり、動きをスクリプト(C#)で記述する
    • Unityは、簡単なカジュアルゲーム開発に使用されている例も多く、サンプルや参考になるページも多い(個人的な見解)
  • Unity ハブ インストール
  • Unity バージョン インストール
    • 必要なバージョンを選択しインストール(結構時間がかかる)
    • 最新版のUnity6も選択できるが、過去のサンプルを使用する場合に使用できない機能やパッケージの追加が必要だったりする
    • Unityも様々な開発環境同様、過去バージョンで作成したプロジェクトの互換性を保証していない
  • Unity プロジェクト作成
    • 新規作成の場合は「プロジェクト」->「新規プロジェクト」->「テンプレート選択+名称設定」
    • 追加の場合は「追加」->「ディスクから加える」又は「ロポジトリから加える」
  • プロジェクト編集(Unityエディター使用)
    • 新規の場合は、使い方を学びの「Roll-A-Ball」をカスタマイズ
    • サンプル(https://github.com/fgrehm/unity3d-roll-a-ball/tree/master 等)をベースに使用する場合は、git clone又はZip形式をダウンロードしてローカルにプロジェクトを保存し、Unity ハブでプロジェクトを読み込む(プロジェクト追加)
  • カスタマイズ(unity3d-roll-a-ballに対してUnity6で使用する場合)
    • エディター画面左の[Hierachy]のWallsにGroundを入れ、名前をBoardに変更
    • PlayerオブジェクトとPlayerController.csをの名前を、それぞれBallとBallControler.csに変更
    • Boardオブジェクトに新規スクリプトBoardController.csをアタッチ
    • 画面左上[Edit][Project Settings…][Player][Other Settings][Configuration][Api Compatibillyty Level*]を[.NET Framework]に変更(USBシリアルを使用する為)
    • USBシリアル通信用新規スクリプトSerialRead.csをBoardオブジェクトにアタッチ
    • SerialRead.cs では、シリアル受信時のデータ化け(±511の間でない場合や整数値に変換できない場合)の対応も行っている
    • 画面上部の[Window][Package Manager]から[Unity UI]を追加(Unity6ではUIパッケージを追加する必要あり)
    • Canvas内のCountText、WinTextを削除し、新規のUI/Legacy/Text(scoreText(Legacy)、winText(Legacy)、timeText(Legacy))に交換する(位置、文字サイズ、色、初期値は適当に)
    • 効果音を入れる場合は、AudioSourceとAudioClipで(今回は使用なし)
    • 経過時間測定を表示(timeText)し、12個のブロックを全て衝突されば終了(変更なし)
    • BallがBoardから落下(position.yがFALLING_HEIGHT以下)すれば終了
    • ゲーム自体の終了は[ESC]キー押し
    • micro:bitからの加速度に対するスケーリングは BoardController.cs の[FixedUpdate()]の中で行い、現状はそれぞれ 1/11.4 倍で傾き角度に変換している
    • ※本ゲームのプロジェクトを入手したい方は「コメント」にて連絡ください

ゲーム実行

  • USBケーブルでmicro:bitとPCを接続
  • Windows PCの画面下 [Windowsマーク] 右クリック [デバイス マネージャー] ポート(COMとLPT)のmicro:bitと思われるUSBシリアル デバイス(COMxx)のCOMxxをSerialRead.csの該当部に記載するか、Unityエディター画面右左[Hieracy][Board]選択時の画面右[Inspector]のアッタチされたSerialRead(Script):PortName(public文字変数)に入力する
  • Unityエディターで実行する場合は画面上部中央の ▶ をクリック
  • ビルドしてバイナリー実行する場合は画面上部左[File][Build And Run]選択で保存ディレクトリ名を適当に(buildとか)作成・選択すると、ビルド実行後ゲームが起動される
  • バイナリーの実行は build/unity-roll-a-ball.exe をクリック(unity-roll-a-ballプロジェクトを流用したので)
  • 実行ファイル名を変更するには、画面左上[Edit][Project Settings…][Player][Product Name]を変更
  • フルスクリーン実行からウインドウ実行に変更するには、画面左上[Edit][Project Settings…][Player][Resolution and Presentation][Fullscreen Mode]を[Windowed]に変更(解像度も好きに設定)
  • 理論的には他のプラットフォームでビルド可能ですが結構難易度大(Androidの場合はUSBデバイスのroot権限取得問題など)です

サンプルから新規・変更したスクリプトを下記

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public class SerialRead : MonoBehaviour
{
    // シリアル通信用定義
    public string portName = "COM16"; // micro:bitをUSB接続した時のCOMポート設定
    public int baudRate = 115200; // micro:bitをUSB接続した時のボーレート設定
    private static SerialPort serialPort;

    // シリアル通信で送られてくる加速度データをスターティックに
    public static int angleX;
    public static int angleY;
    private int angleXprev;
    private int angleYprev;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort.Open();
        angleX = 0;
        angleXprev = 0;
        angleY = 0;
        angleYprev = 0;
    }

    // Update is called once per frame
    void Update()
    {
        // シリアル読み出し
        if (serialPort != null && serialPort.IsOpen) {
            try {
                string message = serialPort.ReadLine();
                //Debug.Log(message);

                bool errorFlag = false;
                
                string[] sArr =  message.Split(',');
                if (sArr.Length == 2) {
                    try{
                        angleX = int.Parse(sArr[0]);
                        if ((angleX < -511) || (angleX > 511)) {
                            errorFlag = true;
                        }
                    }
                    catch (System.Exception) {
                        errorFlag = true;
                    };

                    try {
                        angleY = int.Parse(sArr[1]);
                        if ((angleY < -511) || (angleY > 511)) {
                            errorFlag = true;
                        }
                    }
                    catch (System.Exception) {
                        errorFlag = true;
                    };
                    if (!errorFlag) {
                        string slog = sArr[0]+' '+sArr[1];
                        //Debug.Log(slog);
                    }
                    else {
                        angleX = angleXprev;
                        angleY = angleYprev;
                    }
                }
                
            } catch (System.Exception e) {
                Debug.LogWarning(e.Message);
            }
        }
    }
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class BoardController : MonoBehaviour
{
    private float angleX;
    private float angleZ;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        angleX = 0.0f;
        angleZ = 0.0f;
        // ボードオブジェクトを回転する
        transform.rotation = Quaternion.Euler( angleX, 0f, angleZ);
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        angleX = -1.0f * SerialRead.angleY / 11.4f;
        angleZ = -1.0f * SerialRead.angleX / 11.4f;

        // ボードオブジェクトを回転する
        transform.rotation = Quaternion.Euler( angleX, 0f, angleZ);
    }
    
}
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class BallController : MonoBehaviour {
	public Text scoreText;
	public Text winText;
	public Text timeText;
	private int count;
	private const float FALLING_HEIGHT = -10.0f;
	private float elpasedTime;
	private bool endFlag;

	void Start() {
		// 初期化
		count = 0;
		scoreText.text = "";
		winText.text = "";
        elpasedTime = 0.0f;
		endFlag = false;

		SetCountText();
		DisplayTimeFormat(elpasedTime);
	}

	// Before physics calculations
	void FixedUpdate() {
		if (!endFlag) {
			// 経過時間を計測、表示
        	elpasedTime += Time.deltaTime;
			DisplayTimeFormat(elpasedTime);
		}

		DetectGameEnd();
	}

	void OnTriggerEnter(Collider other) {
		if (other.gameObject.tag == "PickUp") {
			other.gameObject.SetActive (false);
			count++;
			
			SetCountText();
		}
	}

	void SetCountText() {
		scoreText.text = "Score: " + count.ToString();
	}

	void DetectGameEnd() {
		// transformを取得
        Transform myTransform = this.transform;

		 // 座標を取得
        Vector3 pos = myTransform.position;

		if (pos.y < FALLING_HEIGHT) {
			endFlag = true;
			winText.text = "YOU LOSE!  quit: push [ESC]";

			// ゲーム終了
			EndGame();
		}
		else if (count >= 12) {
			endFlag = true;
			winText.text = "YOU WIN!  quit: push [ESC]";

			// ゲーム終了
			EndGame();
		}
	}

	//ゲーム終了
    private void EndGame() {
        //Escが押された時
        if (Input.GetKey(KeyCode.Escape))
        {

#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;//ゲームプレイ終了
#else
    		Application.Quit();//ゲームプレイ終了
#endif
        }

    }

	//floatの値をタイム表記の文字列で返す
    private void DisplayTimeFormat(float time) {
        string timeString = string.Format("{0:D2}:{1:D2}:{2:D2}",
            (int)time / 60,
            (int)time % 60,
            (int)(time * 100) % 60);
        
		timeText.text = timeString;
    }
}

/

コメント

タイトルとURLをコピーしました