micro:bitで操作するPCゲーム作成
水平台ロボットを作製する前に、micro:bitから取得される加速度の特性(追従性、誤差等)を把握する為、平面台ロボット+迷路ゲームの挙動に近い仮想的なゲームを作成する。
- ゲーム開発環境
- ゲーム開発環境として、UnityとUnreal Engine(両者とも個人的な利用であれば無償で使用可能)が有名であるが、個人的な嗜好(ビジュアル スクリプティングのブループリントよりC#の方が扱い易い)でUnityを選択
- Unityは、ゲーム画面を構成するオブジェクト(2D/3D図形)に対してその性質を設定したり、動きをスクリプト(C#)で記述する
- Unityは、簡単なカジュアルゲーム開発に使用されている例も多く、サンプルや参考になるページも多い(個人的な見解)
- Unity ハブ インストール
- まず最初に、Unityの様々なバージョンやプロジェクトを管理するUnity ハブをインストール(https://unity.com/ja/download)
- 必要であれば日本語化(https://bizroad-svc.com/blog/unity-nihongoka/ を参考)
- 使い方を学んだり、コミュニティーへの参加もできる
- 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;
}
}/


コメント