きかいや。

機械といいつつだいたいプログラミングのはなし

WiiBalanceBoardをVRでコントローラにする話、とか

この記事はOculusRiftAdventCalendar2016 11日目の記事です。
qiita.com

はじめに

「確かにルームスケールVRいいんだけど」
「僕の部屋6畳1間で冷蔵庫とかベッドとか食器棚とかあるんで移動できるようなスペースないんだ。」
「なのでWiiのバランスボード的な体重移動式の移動コントローラ、狭い部屋ではいいと思うんだよね」
「そこに乗ってる限りは何かにぶつかる心配とかないし。」
という感じで半年ほど前に作って公開した奴
machinemaker.hatenablog.com
を使ってセ○ウェイ的な移動コントローラのサンプルを作ってみました。
ソース、アプリはこちら。
BalanceBoardTest(Unity).zip - Google ドライブ

※注意:

2バージョンほど制御方式を試しましたが、
PD速度制御(PDControllフォルダのほうの)、直接速度変更(VelocityControllフォルダのほう)
正直どちらもVRで使うとVR酔いして倒れると思うので、各自酔わない使い方を模索していただければと思います。僕が数分で酔って倒れてるのでまだまだ制御方法を考えなくてはいけません…。あくまで使ってみたサンプルとして読んでください。

検証環境:UnityPro4.5.1 (64bit)/Windows10 Home/Oculus Rift CV1

つかいかた:

[共通準備]

・PCとWiiBalanceBoardを接続しておく。
PCとバランスボードのペアリングをします。
通常の方法でペアリングをしても、ボードの電源を切るたびにペアリングが切れてやり直し。
しかしこちらを参考にペアリングをすると再接続もできるようにしてくれます。ありがたや。。。
Wiiリモコン(バランスボード)とWindowsの再接続の話
認識されたバランスボードはHIDデバイスとして認識されています。

[ビルド済みアプリを使ってみる]

0.ディレクトリ名に日本語が含まれないことを確認する
Oculusは英語のことしか考えていないので、
アプリのパスにディレクトリ名にうっかり日本語を使うとVRモードが使えません(!)。
この場合非VRモードで起動します。
(UnityEditor上ではVRで動いてるのにビルドするとVRで動かない、なんで!?ってときはこいつのせいのことが多い。)

なので半角英数にしましょう。

1.Unity製アプリの起動と同時に、別プロセスでTCPサーバアプリをたちあげるので、
「お前怪しいことしてないか?」とアンチウィルスなどの警告が出ることがあります。
(僕の記事を信用するのであれば)ホワイトリストに入れて実行させてあげてください。
TCPサーバアプリがバランスボードとの通信をしています。
UnityアプリはTCPサーバのクライアントとして、サーバ経由でデータをもらっています。)

※サーバ・クライアント間はTCP/IP通信をしていて、中で
public string DistIPAddress = "127.0.0.1"; //自分のPCを指すアドレス
public int portNum = 8888; //ポート番号
としています。

2.接続がうまくいけば、乗り物や荷重中心を表す球が動くはずです。
体重を左右に移動すると左右に旋回、
体重を前後に移動すると前後に移動となっています。
中身としては前後左右位置に適当なゲインをかけたものを移動/旋回速度目標として設定し、

適当なPD速度制御をかけてやっているだけです。

中身の解説

サーバランチャ

TCPServerLancher.cs:

サーバアプリケーションをフォア/バックグラウンドプロセスとして起動しています。
hideServerApplicationWindowのフラグをTrueにするとバックグラウンド起動します。

using UnityEngine;
using System.Collections;
using System;

// 2016/03/17 written by meka
// 本コード内では
// dobon!!様のDOBON.NET > プログラミング道 > .NET Tips より、
//	外部プロセスの実行系のコード(MITライセンス)を改変して使用しています。
//(Topページへのリンク http://dobon.net/)
//http://dobon.net/vb/dotnet/process/openfile.html				(外部プロセスの実行、イベントハンドラの登録)
//http://dobon.net/vb/dotnet/process/processwindowstyle.html	(ウィンドウを非アクティブにして実行)
//http://dobon.net/vb/dotnet/process/shell.html					(StartInfoの使い方)
//http://dobon.net/vb/dotnet/process/killprocesse.html			(外部プロセスを終了させる)
// 以下、dobon!!様のコードのライセンス許諾文表示
/*The MIT License (MIT)
Copyright (c) 2016 DOBON! <http://dobon.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

//Todo::多重起動対策
//Wiimoteサーバーアプリを起動するランチャ

public class WiimoteServerLancher : MonoBehaviour
{
	//外部プロセスのプロセスオブジェクト
	System.Diagnostics.Process wiimoteServerProcess;

	//サーバアプリをバックグラウンドで起動するか
	public bool hideServerApplicationWindow = false;

	//サーバアプリを自動で起動するか サーバアプリの開発時などにfalseにして使う
	public bool lunchTheServerProcess = true;

	//与える引数(現在未使用)
	public string argments = "";

	//このフォルダと.exeはあらかじめ作っておく
	public string applicationPath = "/WiimoteServer/WiimoteTest.exe";

	//クライアントが(Start関数で)起動する前にサーバを起動させたいので、
	//Awakeでサーバを起動する
	void Awake ()
	{
		if(!lunchTheServerProcess)
			return;
		
		string dir = "";
		string filepath = "";				//起動したいアプリケーション
		//エディター時とビルド済み実行ファイルで同じ場所を見るようにする
		//アプリと同じ階層をさがすように設定
		if (Application.isEditor) {
			dir = Application.dataPath + "/..";
		} else {
			dir = Application.dataPath + "/..";
		}
		//起動するファイルのフルパス
		filepath = dir + applicationPath;	

		Debug.Log (filepath);

		//Processオブジェクトを作成する
		wiimoteServerProcess = new System.Diagnostics.Process ();
		//起動するファイルを指定する
		wiimoteServerProcess.StartInfo.FileName = filepath;
		//イベントハンドラの追加
		wiimoteServerProcess.Exited += new EventHandler (wiimoteServerProcess_Exited);
		//プロセスが終了したときに Exited イベントを発生させるように設定
		wiimoteServerProcess.EnableRaisingEvents = true;
		//引数を指定する
		wiimoteServerProcess.StartInfo.Arguments = argments;
		//(フラグを見て)外部プロセスをバックグラウンドで起動するように設定
		if (hideServerApplicationWindow) {
			wiimoteServerProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
		}
		//起動する
		try {
			wiimoteServerProcess.Start ();
		} catch (Exception e) {
			//外部プロセスをスタートできなかったらエラー表示
			Debug.LogError ("Failed to start process." + e.Message);
		}

	}


	//実行終了時に外部プロセスが生きているなら自動終了させる
	void OnApplicationQuit ()
	{
		if(!lunchTheServerProcess)
			return;
		
		if (wiimoteServerProcess.HasExited) {
			Debug.Log ("外部プロセスは終了済みです");
			return;
		}

		//まだ外部プロセスが動いていれば終了させる。
		try {
			//メインウィンドウを閉じる
			wiimoteServerProcess.CloseMainWindow ();
			//プロセスが終了するまで最大2秒待機する
			wiimoteServerProcess.WaitForExit (2);
			//プロセスが終了したか確認する
			if (wiimoteServerProcess.HasExited) {
				Debug.Log ("外部プロセスが終了しました");
			} else {
				Debug.LogError ("外部プロセスが終了しませんでした。強制終了させます。");
				wiimoteServerProcess.Kill ();		///プロセスを強制終了
			}
		} catch (Exception e) {
			Debug.LogError (e.Message);
		}

	}



	//外部プロセス終了を検知して呼び出される関数
	void wiimoteServerProcess_Exited (object sender, System.EventArgs e)
	{
		if(!lunchTheServerProcess)
			return;
		
		UnityEngine.Debug.Log ("WiimoteProssess_ExitEvent");
		wiimoteServerProcess.Dispose();						//プロセスを破棄
	}




}

通信クライアント系

WiiBalanceBoardCliant.cs

サーバアプリケーションとの間でTCP通信を行い、センサデータを受け取っています。
受け取ったデータはBalanceBoardDatalist.parseMessage(resMsg)で区切って意味のあるデータに変換しています。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;

// 2016/03/17 written by meka 
// 本コード内では
// dobon!!様のDOBON.NET > プログラミング道 > .NET Tips より、
// TCP/IP通信系のコード(MITライセンス)を改変して使用しています。
// http://dobon.net/vb/dotnet/internet/tcpclientserver.html 
//(Topページへのリンク http://dobon.net/)

// 以下ライセンス許諾文表示
/*The MIT License (MIT)
Copyright (c) 2016 DOBON! <http://dobon.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/



[System.Serializable]
public class WiiBalanceBoardCliant : MonoBehaviour
{
	TcpClient tcpClient;
	Thread readThread;
	bool flg_continue = true;
	public string DistIPAddress = "127.0.0.1";					//自分のPCを指すアドレス
	public int portNum = 8888;									//ポート番号
	public int timeout = 2000;

	//通信データ格納用データリスト
	//Wiiバランスボードのインスタンス別に管理
	public BalanceBoardDataList recvBalanceBoardDatalist = new BalanceBoardDataList();

	// Use this for initialization
	void Start ()
	{
		
		IPAddress serverIP = IPAddress.Parse (DistIPAddress);

		if (tcpClient != null)
			return;
		
		tcpClient = new TcpClient ();
		tcpClient.ReceiveTimeout = 2000;	//2秒でタイムアウト
		tcpClient.SendTimeout = 2000;		//2秒でタイムアウト
		tcpClient.Connect (serverIP, portNum);
		Debug.Log ("init client");

		readThread = new Thread (new ThreadStart (recvTask));		//スレッドで呼び出す関数を登録
		readThread.IsBackground = true;
		readThread.Start ();

	}


	void OnDestroy ()
	{
		flg_continue = false;
		//readThreadの終了待ち
		if (readThread != null) {
			Debug.Log ("waiting for read thread kill.");
			readThread.Join ();
			Debug.Log ("read thread killed");
		}
		//TCPクライアント(ソケット)を閉じる
		if (tcpClient != null) {
			Debug.Log ("tcpClient Closing...");
			tcpClient.Close ();
			Debug.Log ("tcpClient Closed.");
			
		}

		
	}

	//受信メッセージを処理
	private void recvTask ()
	{
		//
		NetworkStream ns = tcpClient.GetStream ();
		if(ns == null ){
			Debug.LogError("tcpCliant.Getstream() is failed.");
			return;
		}
		Debug.Log ("GetStream Success");
		//networkStream setup
		ns.ReadTimeout = 2000;
		ns.WriteTimeout = 2000;
		System.Text.Encoding enc = System.Text.Encoding.UTF8;


		byte[] resBytes = new byte[1];									//一文字づつ読ませるため1個ぶん

		//スレッド内で回し続ける
		while (flg_continue) {
			//読み取り、書き込みのタイムアウトを10秒にする
			//デフォルトはInfiniteで、タイムアウトしない
			//(.NET Framework 2.0以上が必要)

			//サーバーから送られたデータを受信する
			System.IO.MemoryStream ms = new System.IO.MemoryStream ();

			int resSize = 0;
			do {
				//データの一部を受信する
				//					Debug.Log ("ns.Read()");
				resSize = ns.Read (resBytes, 0, resBytes.Length);
				//					Debug.Log ("readed... " + resSize + " byte");

				if (!(resSize > 0))
					break;													//通信に返答なし

				//受信したデータをmemoryStreamへ蓄積する
				ms.Write (resBytes, 0, resSize);
				//まだ読み取れるデータがあり、データの最後が\n(改行コード)でない時は受信を続ける
				//改行コードがきたら、それがデータの区切りなので切り出す
			} while(resBytes [resSize - 1] != '\n'&& ns.DataAvailable);

			//受信したデータをmemoryStreamから吐き出す
			string resMsg = enc.GetString (ms.GetBuffer (), 0, (int)ms.Length);
			recvBalanceBoardDatalist.parseMessage(resMsg);
			//holdMessage(resMsg);
			ms.Close ();

		}
		Thread.Sleep (1);
	}


}
BalanceBoardInfo.cs

enum BalanceBoardInfo:通信して流れてきたデータの何番目が何のデータか表す。
class BalanceBoardDataList:複数のバランスボードのデータを保持するリスト.データの解釈・変換もこいつが行う

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

///データ番号定義
///この順番で区切られたメッセージが来る
enum BalanceBoardInfo
{
	index,					//コントローラ番号
	Weight,					//重さ[kg]
	CoPPosX,				//位置[cm]
	CoPPosY,				//位置[cm]
	SensorLoadTopRight,		//センサ値
	SensorLoadTopLeft,		//センサ値
	SenserLoadBottomRight,	//センサ値
	SensorLoadBottomLeft,		//センサ値
	EnumMaxNumber			//個数チェック用
}

//複数のバランスボードのデータを保持するリスト
public class BalanceBoardDataList
{

	public List <BalanceBoardData> balanceBoardData = new List<BalanceBoardData>();
	public string message;	//デバッグ用に保持させる

	//string型のmessageを区切り、数値データとして変換する。
	public bool parseMessage(string message_in)
	{
		//メッセージ異常
		if(message_in == null){
			Debug.LogError("TCP message is null");
			return false;
		}
			
		message = message_in;

		string[] stArryData = message_in.Split(',');
		//Debug.LogWarning(stArryData[0]);
		//Debug.Log("個数:"+ stArryData.Length+"要素数:"+(int)WiiBalanceBoardInfo.EnumMaxNumber);
		if(stArryData.Length < (int)BalanceBoardInfo.EnumMaxNumber){
			Debug.LogError("メッセージのデータ個数が不正です。データを破棄します。 個数:"+ stArryData.Length+"正しい要素数:"+(int)BalanceBoardInfo.EnumMaxNumber);
			//データ個数が不正
			return false;
		}

		int _index;
		//コントローラ番号を見分ける
		if(!int.TryParse(stArryData[(int)BalanceBoardInfo.index].ToString(),out _index))
			Debug.LogWarning("failed to parse.index:" + stArryData[(int)BalanceBoardInfo.index]);

		//新しいコントローラを追加
		while(balanceBoardData.Count < _index+1 ){
			balanceBoardData.Add(new BalanceBoardData());
			Debug.Log("adding / data.Count:" + balanceBoardData.Count +" index");
		}


		//各値を変換
		//weight[kg]
		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.Weight].ToString(),out balanceBoardData[_index].weight))
			Debug.LogWarning("failed to parse.wight:" + stArryData[(int)BalanceBoardInfo.Weight]);

		//Center of Pressure X[cm]
		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.CoPPosX].ToString(),out balanceBoardData[_index].copPos.x))
			Debug.LogWarning("failed to parse.copX:" + stArryData[(int)BalanceBoardInfo.CoPPosX]);

		//Center of Pressure Y[cm]
		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.CoPPosY].ToString(),out balanceBoardData[_index].copPos.y))
			Debug.LogWarning("failed to parse.copY:" + stArryData[(int)BalanceBoardInfo.CoPPosY]);

		//sensor weight data[]

		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.SensorLoadTopRight].ToString(),out balanceBoardData[_index].sensorLoad.TopRight))
			Debug.LogWarning("failed to parse.SensorKgTopRight:" + stArryData[(int)BalanceBoardInfo.SensorLoadTopRight]);

		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.SensorLoadTopLeft].ToString(),out balanceBoardData[_index].sensorLoad.TopLeft))
			Debug.LogWarning("failed to parse.SensorKgTopLeft:" + stArryData[(int)BalanceBoardInfo.SensorLoadTopLeft]);

		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.SenserLoadBottomRight].ToString(),out balanceBoardData[_index].sensorLoad.BottomRight))
			Debug.LogWarning("failed to parse.SensorKgBottomRight:" + stArryData[(int)BalanceBoardInfo.SenserLoadBottomRight]);

		if(!float.TryParse(stArryData[(int)BalanceBoardInfo.SensorLoadBottomLeft].ToString(),out balanceBoardData[_index].sensorLoad.BottomLeft))
			Debug.LogWarning("failed to parse.SensorKgBottomLeft:" + stArryData[(int)BalanceBoardInfo.SensorLoadBottomLeft]);



		return true;
	}

}




/// <summary>
/// 受信データを意味のある形にしたもの.
/// </summary>
[System.Serializable]
public class BalanceBoardData
{
	/// <summary>
	/// WiiBalanceBoardの各センサにかかっている重量
	/// </summary>
	[System.Serializable]
	public struct SensorLoad
	{
		public float TopRight;
		public float TopLeft;
		public float BottomRight;
		public float BottomLeft;
	}

	public float weight;		//重量[kgf]
	public Vector2 copPos;		//荷重中心[cm]
	public SensorLoad sensorLoad;		//WiiBalanceBoard各センサーにかかっている重量(4か所)
}

表示・移動コントロールなどセンサデータ利用系

WiimoteInfoDisplayBase.cs

WiiBalanceBoardCliantにアクセスしてデータを取得し、加工して表示するための親クラスです。
これの子クラスでOutput関数をオーバーライドして実装する形でセンサデータを利用します。
ボードの複数接続に対応しているので、indexでどのバランスボードの値を使うかを決めています。

using UnityEngine;
using System.Collections;

//情報取得(input)と表示など(Output)の基本クラス
public class WiimoteInfoDisplayBase: MonoBehaviour
{
	///コントローラ番号
	public int index = 0;
	/// <summary>
	//(コントローラの情報をもっている)サーバと通信してるTCPクライアント
	[SerializeField]
	protected WiiBalanceBoardCliant wiiBalanceBoardCliant;
	//バランスボードのステータスを格納する構造体
	[SerializeField]
	protected BalanceBoardData balanceBoardDataNowFlame;		//現在のセンサ情報

	protected BalanceBoardData balanceBoardDataZeroFlame;		//初期センサ情報(オフセット用)


	//[Wii Controller informations]
//	protected BalanceBoardData 

	// Use this for initialization
	protected void Start ()
	{
		//通信クライアントコンポーネントへアクセスできるようにする
//		wiiBalanceBoardCliant = wiiBalanceBoardCliantObject.GetComponent<WiiBalanceBoardCliant> ();
		if (wiiBalanceBoardCliant == null) {
			Debug.LogError ("WiiBalanceBoardCliant is null");
		}
	}

	// Update is called once per frame
	void Update ()
	{
		//WiiバランスボードのInputを取得する
		if (!GetInputData ()) {
			//データが取得できなかったら表示せず次のフレームへ
			return;
		}

		//表示やコントローラとしての利用など。
		//中身は子クラスで実装して下さい
		Output ();
	}

	//
	/// <summary>
	/// Gets the data.
	/// </summary>
	/// <returns><c>true</c>, if data was gotten, <c>false</c> otherwise.</returns>
	bool GetInputData ()
	{
		//通信クライアントにアクセスできない
		if (wiiBalanceBoardCliant == null) {
			Debug.LogError ("BalanceBoardCliant is null");
			return false;
		}
		//バランスボードが接続されていない
		if (wiiBalanceBoardCliant.recvBalanceBoardDatalist.balanceBoardData == null) {
			Debug.LogError ("リストがありません");
			return false;
		}
		//表示対象のコントローラがない
		if (!(index < wiiBalanceBoardCliant.recvBalanceBoardDatalist.balanceBoardData.Count)) {
			Debug.LogWarning ("デバイスの数が足りません index(コントローラ番号[0始まり]):" 
				+ index
				+ " count(総コントローラ数):"
				+ wiiBalanceBoardCliant.recvBalanceBoardDatalist.balanceBoardData.Count);
			return false;
		}

		//データ取得部
		balanceBoardDataNowFlame = wiiBalanceBoardCliant.recvBalanceBoardDatalist.balanceBoardData [index];
		return true;
	}

	//センサオフセットのゼロ点調整//未実装
	bool calibrateZeroPoint ()
	{
		return true;
	}


	//子クラスで実装する
	protected virtual void Output ()
	{
	}

}
WiiBalanceBoardVisualDisplaySphere.cs

荷重中心の位置を、transform.localPositionに挿入することで球の位置を移動しています。
WiimoteInfoDisplayBaseの子クラスです。

using UnityEngine;
using System.Collections;
/// <summary>
/// 荷重中心の位置を使って軌跡を表示
/// </summary>
public class WiiBalanceBoardVisualDisplaySphere : WiimoteInfoDisplayBase 
{
	protected float gainScale = 0.01f;
	//距離の単位変換ゲイン[cm-> m なので1/100]
	public bool xzInverse = false;			//xz入れ替えて表示

	Vector3 localPos;

	override protected void Output()
	{
		//荷重中心の位置を使ってオブジェクトの位置を上書きする
		if(xzInverse){
			localPos = new Vector3
				(balanceBoardDataNowFlame.copPos.y * gainScale ,
					gameObject.transform.localPosition.y,
					balanceBoardDataNowFlame.copPos.x * gainScale); 
		}else{
			localPos = new Vector3
				( balanceBoardDataNowFlame.copPos.x * gainScale,
					gameObject.transform.localPosition.y,
					-balanceBoardDataNowFlame.copPos.y * gainScale); 

		}

		gameObject.transform.localPosition = localPos;

	


	}
}
VehicleController.cs

セ○ウェイ的コントローラの実装です。物理演算しています。
重心座標に応じて目標速度・目標角速度を決めています。
それに対して速度PD制御することで移動速度・旋回速度を与えています。

using UnityEngine;
using System.Collections;

public class VehicleController : WiimoteInfoDisplayBase {

	///距離の単位変換ゲイン[cm-> m なので1/100]
	protected float gainScale = 0.01f;
	/// <summary>
	/// The speed gain.
	/// </summary>
 
	Rigidbody rigid;

	void Start()
	{
		base.Start();
		if((rigid = GetComponent<Rigidbody>()) == null){
			Debug.LogError("Controller has no rigidbody!");
		}
	}

	/// <summary> </summary>
	public FilteredInput filteredInput = 
		new FilteredInput(
		new Vector3(0.1f,0.1f,0.1f) ,
		new Vector3(0.1f,0.1f,0.1f),
		new Vector3(0.315f , 0.1f, 0.511f) );

	Vector3 velocityTarget;
	Vector3 angVelocityTarget;

	public float forceScale = 10f;
	public float torqueScale = 10f;

	public float PGain;
	public float DGain;
//	public float Igain;
	Vector3 filteredValue;

	/// <summary>
	/// Output this instance.
	/// </summary>
	override protected void Output()
	{
		////入力のフィルタリング
		//荷重中心位置[m]
		Vector3 localPos = new Vector3
			(balanceBoardDataNowFlame.copPos.y * gainScale ,
				gameObject.transform.localPosition.y,
				balanceBoardDataNowFlame.copPos.x * gainScale); 

		//不感帯フィルタ

		filteredValue = filteredInput.GetOutput(localPos);
	}

	Vector3 force = Vector3.zero;
	Vector3 torque = Vector3.zero;

	/// <summary>
	/// 物理演算更新
	/// </summary>
	void FixedUpdate()
	{
		//エラー処理
		if(rigid==null){
			Debug.LogWarning("Controller has no rigidbody!");
			return;
		}

		////乗り物オブジェクトの制御
		/// 
		///////////速度PD制御
		/// 力
		float x;
		float y;
		float z;

		velocityTarget =  Vector3.forward * -filteredValue.x * forceScale;	//目標力の計算

		//トルク
		x = 0f;
		y = filteredValue.z * torqueScale;
		z = 0f;

		angVelocityTarget = new Vector3(x,y,z);
		//並進
		//回転

		//計算座標系はローカルで
		force = (velocityTarget - transform.InverseTransformVector(rigid.velocity)) * PGain + (force)*DGain;
		torque = (angVelocityTarget - transform.InverseTransformDirection(rigid.angularVelocity)) * PGain + (torque)*DGain;


		//addforceはworld座標で
		rigid.AddForce(transform.TransformVector(force), ForceMode.Force);
		rigid.AddTorque(torque,ForceMode.Force);
	}

}
FilteredInput.cs

入力に対してレンジ制限、ゲインの反映、不感帯フィルタ、オフセットを反映するために用意したクラスです。

using UnityEngine;
using System.Collections;


[System.Serializable]
public class FilteredInput
{

	public Vector3 offset = Vector3.zero;
	/// <summary> 不感帯 </summary>
	public Vector3 deadZone = new Vector3(0.05f,0.05f,0.05f);
	/// <summary> 比例ゲイン </summary>
	public Vector3 pGain = new Vector3(1f,1f,1f);

	//入力範囲制限
	public Vector3 range = new Vector3(0.315f , 0.1f, 0.511f);
	//	Vector3 output;

	public FilteredInput(Vector3 deadZone_in,Vector3 pGain_in,Vector3 range_in)
	{
		deadZone = deadZone_in;
		pGain = pGain_in;
		range = range_in;
	}

	//不感帯フィルタ用
	float DeadFilter(float input_, float deadZone_)
	{
		float output_= 0f;

		if(Mathf.Abs(deadZone_) < Mathf.Abs(input_)){
			if(deadZone_ < input_){
				output_ = input_ - deadZone_;
			}else{
				output_ = input_ + deadZone_;
			}
		}else{
			output_ = 0f;
		}

		return output_;
	}

	//レンジ制限
	float RangeFilter(float input_, float range_)
	{
		float output_ = 0f;
		output_ =  Mathf.Clamp(input_, -range_ * 0.5f , range_ * 0.5f);

		return output_;
	}

	//レンジ制限
	Vector3 RangeFilter(Vector3 input_)
	{
		Vector3 output_ = new Vector3(
			RangeFilter(input_.x,range.x),
			RangeFilter(input_.y,range.y),
			RangeFilter(input_.z,range.z));

		return output_;
	}

	//不感帯フィルタ
	Vector3 DeadFilter(Vector3 input_)
	{
		Vector3 output_ = 
			new Vector3(
				DeadFilter(input_.x,deadZone.x),
				DeadFilter(input_.y,deadZone.y),
				DeadFilter(input_.z,deadZone.z));

		return output_;
	}

	//ゲイン反映
	Vector3 GainFilter(Vector3 input_)
	{
		Vector3 output_ = new Vector3(pGain.x * input_.x,pGain.y * input_.y,pGain.z * input_.z);
		return output_;
	}


	//オフセット設定
	public bool SetOffset(Vector3 offset_in)
	{
		offset = offset_in;
		return true;
	}

	Vector3 OffsetFilter(Vector3 input_)
	{
		Vector3 output_ = input_ - offset;

		return output_;
	}

	//出力取得
	public Vector3 GetOutput(Vector3 input_)
	{

		Vector3 output_ = input_;
		output_ = OffsetFilter(output_);
		output_ = RangeFilter(output_);
		output_ = DeadFilter(output_);
		output_ = GainFilter(output_);
		return 	output_;
	}
}

ScriptableObject継承クラスを作ったときにやったミス

ScriptableObjectのAssetを作成して、

UnityEditorを再起動すると中身が破損して読み込めなくなる症状があり、
(Inspectorで見ると The associated script can not be loadedといわれる。)

一年以上放置してたんですが、解決したのでメモ。

(Airfoil.cs)
[CreateAssetMenu]
public class Airfoil:ScriptableObject{
}

[CreateAssetMenu]
public class AirfoilMinimum:ScriptableObject{
}

のようにひとつの.csファイルの中にふたつのScriptableObject継承クラスを入れていたのが間違い。

UnityのScriptableObjectやMonoBeheviourを継承したクラスは、
.csファイルの名前とクラス名を一致させなければいけないのです、ハイ。

(Airfoil.cs)
[CreateAssetMenu]
public class Airfoil:ScriptableObject{
}




(AirfoilMinimum.cs)
public class AirfoilMinimum:ScriptableObject{
}
のようにソースを分割すると治りました。

↓みたいなところちゃんと読もう
docs.unity3d.com

Unity+WindowsでWiiのBalanceBoardを使う

Unityで任天堂バランスWiiボードのデータを取れるようになったのでメモです。

VR用の移動系コントローラとして使用予定。

追記:サーバ、クライアント(Unity)のプロジェクト全体をアップロードしました。ライセンスはMs-PL。

使い方は後日。

 

drive.google.com

 

動画


WiiBalanceBoardCOPTest


WiiBalanceBoardTestCoPTrail

 

動作確認した構成:

PC:Windows 10 Home
サーバ:C#(VisualStudio2013)+WiimoteLib1.7

クライアント:Unity5.3.5f

バランスWiiボード

 

背景:
MacだとUnityとバランスボードをつなげるアセットが売ってるので楽らしいのですが、Windows非対応。

・よく使われているWiimoteLibというライブラリはUnity上ではうまく動かない 。せっかくC#なのに!※

 

手法:

・UnityとTCP通信するサーバを立てる。このサーバはWiimoteLibを使ってbalanceboardと通信してセンサ値を取得しており、センサ値をUnityに送信する仲介役。(WiimoteLibのサンプルを改造して、C#で書いた)

・Unityアプリ側はサーバへTCPで接続し、センサデータを受け取る(クライアント)

・Unityアプリの起動時にサーバアプリを起動するようにした、Editor実行時に勝手にサーバも立ち上がってくれるので便利です。

 

何の情報が取れるか:

いまのところ、Unity側では

・重量

・圧力中心の位置情報

・各センサの生値

・コントローラ番号

の取得が可能です。

 

その他:

・マルチバランスボード対応です。

・サーバアプリはUnityアプリ(クライアント)から自動起動できます。(デフォルトではバックグラウンド起動しませんが、フラグを立てるとバックグラウンド起動します)

 

参考:

外部アプリケーションを起動して終了まで待機する: .NET Tips: C#, VB.NET

 

 

※UnityアプリとBalanceBoard間で直接通信したかったのですが、WiimoteLibはC#向けのLibがあるにもかかわらずUnityでは使えません。(どうもWin32APIあたりのSafeHandlesを中で使ってるのが原因っぽい。動かない。)そこで一段間に別のアプリとしてかませています。

※PC-バランスボード間はBluetoothで接続するため、あらかじめペアリングが必要になります。通常のやり方だとbalanceboardを再起動するたびにペアリングが必要になるので、以下のサイトを参考にしてください。

Wiiリモコン(バランスボード)とWindowsの再接続の話

-----------------

いまのところTCP通信はfloat型の数値をString型で通信してfloat.TryParse()して数値に戻すという無駄なことをやってるのですが、TCPでbinary通信する方法が良く分かりませんでした…(教えて下さると幸いです)
あとTryParseがミスる確率がだいぶ高いのですが、原因が良く分かりません

 --------------------

参考リンク:この辺を参考にしています

wiimotelib.codeplex.com

 

TCPクライアント・サーバープログラムを作成する: .NET Tips: C#, VB.NET

 

 

d.hatena.ne.jp

----------------

追記:リンクのセキュリティアップデートで共有できなくなっていたので再掲しました。

 

Oculus + UnityVRModeでの開発Tips

自分用のメモです。

随時追記予定

 

・カメラの移動

カメラの位置を強制的に動かしたい場合、Cameraコンポーネントが付いているGameObjectのTransformを変更しても、センサーによる値で上書きされてしまう

->カメラに親Objectを用意し、親ObjectのTransformを変更すると、カメラを強制的に動かせる

 

・VRSupportはDirectX9では働かないので、DirectX11にする。(PlayerSettings)

 

HPASimの配布(非Oculus版)

人力飛行機用のフライトシミュレータ
HPASimについてです。

Teitterだけではどこで配布してるのか良く分からん、

という話を聞いたので、記事としてUPしました。


Oculus版は現在改修中ですが

・Unity版(新):横風練習用のTFモード

・DXライブラリ版(旧):海っぽいところで飛ぶモード、リプレイ可

(作った時期の違いにより、機体特性や操作法が一部異なります)

を以下のリンク(OneDrive)からDLできます。

https://onedrive.live.com/redir?resid=DA038CC57FD44635!57416&authkey=!AFcEXKrUVTa3kus&ithint=folder%2czip

 

使ってやってください。
こんな感じです。

f:id:machinemaker:20150921184316p:plain

Unity:場所によって風方向が変わるテストフライトモードを搭載。地上発進

 

f:id:machinemaker:20150921184749p:plain

DirectX版:計器をちゃんと作ってある。リプレイあり。上空発進

 

追記(2016/11/04):揚抗力可視化版Uploadしました

(すぐできる高速化?)ヘボPCでもOculusがしたい

HPASimを

OculusDK2 SDK0.4.4 Unity4.6.1f'(Pro)
で75FPSを出すために設定でできることをやったことのメモです。
75FPSでない悲しいPCで頑張るための忘備録。(いいパソコンを買え。)

経緯:
頭を回すと視界が妙にブレ、FPSが足りていないことが原因と判明。

(回転運動の予測演算がずれて制御的に振動している?)

 

-----クオリティ設定-------
・Disable Shadows 影を切る

(影は残したいけど…というときは、その下の項目を画像のように弄るとクオリティは落ちるが影は残る)

・Anti Ailiasing アンチエイリアスなし

先ずこのふたつが大きく効きます。

あと、異方性テクスチャとやらも切ってみた

f:id:machinemaker:20150304211654p:plain

f:id:machinemaker:20150304211915p:plain

 

---------------オブジェクトの設定

水面オブジェクト(Daylight Water)の設定

・Refractive(屈折の再現)にはしない

・Refrective(反射とかを描画する)テクスチャサイズを256->64へ

割と効く。水面の反射だけは残したかった。(水面に近いものがあるとチラつくので、調整する)

f:id:machinemaker:20150304212630p:plain

 

・OVRManager(CameraRigについてるスクリプト)の設定

f:id:machinemaker:20150304210743p:plain

Native Texture Scaleを下げる。めちゃめちゃ効く。最後の手段。
樽型変形の際に使うテクスチャサイズを下げる。代わりに表示は荒くなる。
が、ある程度荒いほうがかえってモデルの荒が気にならなくなったりする。。。


PCはASUS U24A
CPU IntelCorei5-3230M 2.6GHz

Memory 8GB

VideoCard:Intel4000HD(OnBoard)

---------------

そのほか

この辺をまず読もう。

izmさんとこ。いい話だ。

Unity+OculusRiftコンテンツのパフォーマンスチューニング - izm_11's blog

引用:

  • Occlusion Cullingと固定物のstatic化はしている
  • 毎フレームDebug.Logを呼ぶような恐ろしい無駄は削除済み

 :というあたりはすぐにできるので、まずやる。


フレームセンシスさんとこ


Unity+Oculus Rift開発メモ(DK2、SDK 0.4.4対応) - フレームシンセシス 技術ブログ

:樽型shaderのテクスチャサイズの話はここ読んで知りました。

MFT2014でHPASim(OculusRift対応版)を出した話

この記事はOculus Rift Advent Calendar 2014 の7日目の記事になります。

Maker Faire Tokyo 2014 | Maker Faire Tokyo 2014 | Make: Japan

に、「はかるひと」という出展内の一企画として、人力飛行機シミュレータ HPASimを出展してきました。(はかるひと、というのは慣性センサをロケットや人力飛行機や鳥にくっつけて、飛びもののセンサ計測を趣味や仕事でやっているひとたちの集まりです。)

今回はこのレポートというか、ここに至るまでの忘備録です。何を考えてこんなものを作っているか、という話になります。Oculusのカレンダーですが、人力飛行機の話や筐体側を作るうえでの話、体験会での注意点的なものが多くなっています。適宜飛ばして読んでいただければ。

 

1.HPASimって何?なんで作ったの?(人力飛行機の話)

人力飛行機シミュレータ(Human Powered Airplane Simulator)といいます。
人力飛行機はペダルをこぐ力でプロペラを回し、空を飛ぶというものです。傍から見ると漕ぐのが大変そうなのはわかるのですが、その操縦性が難しいことはあまり知られていないと思います。慣性が大きく応答が悪いため、飛行機の動きを予測して早めに舵を切らないといけないのです。たとえるなら、ハンドルを切ってから2秒後にやっと曲がり始める車みたいなモノです。(パイロットは筋肉だけ鍛えればいいというものではない)

 昔コレの設計と製作をしていたのですが、その操縦性の悪さにもかかわらず、操縦練習できるシミュレータがこれといって公開されていませんでした(滑空機のシミュレータはいくつかあったのですが、プロペラ機のモノがない。あっても研究室のものだったり、あそこが持っているらしいという話くらい)。パイロットはラジコン飛行機を飛ばしたり、グライダーに乗せてもらったり、市販のフライトシミュのパラメータをいじったりしてそれっぽい動きの飛行機をつくり練習をした後、最後は実機で操作を体得するという流れでした。人力飛行機は出来上がったら荷重試験や重心調整をしたのち、自家用飛行機の滑走路などを使ってテストフライトを行っています)。FlightGear,MSFS,RealFlightのような一般機のフライトシミュの特性を書き換えるのは割と面倒で、真面目に合わせようと思うと飛行機の運動特性を自前で計算してやることになり、この計算が面倒です。スタートボタンを押したらすぐ飛べて練習できる、そんなシミュレータがあればなぁ、という思いからDXライブラリでシミュレータを作りました。そしてOculusRiftを知り、コレを使えばとてもいい練習用のシミュレータになるだろう、と考えて作りました。

あと、アレって基本的にパイロットの専用機なので、作ってるほかの人は乗れないんですよ。せっかく作ったら自分も乗ってみたくなるじゃないですか、アレ。(本音)

f:id:machinemaker:20141207200652j:plain

( こんな筐体に乗ってペダルをこぐと、画面内の飛行機が飛びます)


流れとしては
DXライブラリ+MMDモデルで作成、twitterで公開 -> MFT2013で展示 -> OculusRift(DK1)購入 -> Unityで作り直す ->Oculus Rift(DK1)対応 -> Ocufes(秋葉原大好き祭り)展示 -> ペダル連動するよう筐体作成 -> 裏オキュフェス2で発表 -> OculusRift(DK2)購入->mbed祭り2014@夏の東銀座【一般参加枠】 - mbed祭り | Doorkeeperで飛び入り発表 -> ポジショントラッキング対応、筐体作り直し -> MFT2014で展示

と変遷をたどっています。

 

2.設計コンセプト
設計コンセプトは以下のようにしています。

  • 没入感を高め、また視界を忠実に再現するため、パイロットのアバターを表示する( ポジショントラッキング対応とアバターのIK対応
  • 操縦系/操縦方法を人力飛行機のものに合わせることで、実機への乗り換えをシームレスに行う。
  • デモンストレーションで持ち運び可能なように設計する。(解体・組立・輸送)
  • 特殊な工作具を使用しないでも作れる(というか使えない)

 

3.機器構成

MFTやOcufesでは、体験してもらってハイ次のひとー、という形になってしまったので、あまり技術面の話をじっくりできませんでしたので、ここに。(mbed祭りや裏Ocufesではその辺の話もしていたので、特色が出ますね。)

  • 筐体構成
    フレーム:棚を作るためのL字アングルで構成
    椅子:座椅子を分解してフレームを取り出し、金具でフレームに固定。園芸用のアルミの台を上において、下にクッションゴムを敷き詰めて荷重を床に逃がしています。(MFT2014はビッグサイトで行われ、防火法のため装飾の布類に防炎証明が必要でした。展示物は法律上別扱いとも聞いたのですが、筐体はできるだけ金属や塩ビなど燃えないもの、自己消化性をもつもので構成しました)

    操縦:
    ペダル入力…ペダルをこぐとプロペラが回ります。ペダルにはコレ

    Amazon.co.jp | ながら エクササイズ フィットネス 足 こぎ ペダル ローラー | スポーツ&アウトドア 通販

    を使っています。右ペダルにフォトインタラプタと回転盤、左ペダルに磁石とホールセンサがついています。回路の項で後述。
    尾翼・補助翼(ラダー・エレベータ・エルロン):飛行機の水平尾翼垂直尾翼主翼についている補助翼を操作することで機体のロール・ピッチ・ヨーを制御できます。ここはジョイパッドですが、操縦桿にしたいですね。

  • 回路
    マイコン:mbedマイコンのコレ


    mbed NXP LPC1768 + Starborad orange 【きばん本舗SHOP】

    を使用しています。今回はシリアル通信とセンサのOn/OFFしか見ていないので、ここまで高級なマイコンとボードを使うこともないのですが、扱いやすさや今後の拡張性からコレを使用しています。あと、実機も最近はフライバイワイヤの機体はサーボで尾翼やエルロンを動かし、エンコーダで足元の回転数を見ていたりするので、その基板をそのまま使えるというのが理想的です。車でもそうですが、シミュレータは操縦桿やペダルなどをその操縦系と同じものを使うのが理想だ、という考えが根底にあります。(あとArduinoやH8ではなくmbed推しなのは、AVRは遅い、H8はレジスタのセットアップが面倒という研究室時代にうえつけられた悪夢イメージと、開発環境のインストールが不要でmbedのクラウド開発環境にソースもコンパイラも置いてあるというのが非常に便利なためです。あとCではなくC++で書けるのと、レジスタ周りをみなくても簡単に使えるので良い。ほかの人が作ったクラスやコードのインポートがボタン一発で終わって、ボタンを押せば実行ファイルがダウンロードできて、USBでD&Dで書き込まれるの、楽すぎます。あと、mbedクラスタtwitterにいるので、割と人に聞きやすいです。mbed派増えないかなぁ、Oculusクラスタ。)

    センサ:

     

    f:id:machinemaker:20141207210051j:plain


    ホールセンサ…回転位置のリセット(キャリブレーション)用です。左のペダルに磁石(画像にある、ペダルのパイプについてる黒いやつ)がついており、磁石の通過を検知すると回転の絶対値をリセットします。これがあることで、お客さんはキャリブレーション用の動作なく、知らないうちにリアルの脚の回転とVRの脚の回転が一致します。(今見たら、ディスコンになってる…)

    f:id:machinemaker:20141207205333j:plain

    f:id:machinemaker:20141207211600j:plain


    フォトインタラプタ…画像の黒いやつです。光がさえぎられてのを検出し、回転角度に変換します。空間分解能は2°です。VRでは時間・空間的分解能が低いとアニメーションががくがくして、思いのほか体験の質が低下します。鋏とカッタで作れる中でできるだ歯(スリット)を増やそうとしてこのような直径300mmの大円盤になっています。また、分解能が高ければ人力飛行機的には脈動も見ることができるので、漕ぎ方自体の練習にもなるのではないかな、とも。
    (90パルス2逓倍で分解能2°、A層、Z層のみのロータリエンコーダとなっているので、逆回転させても正転しているように認識しますが、自転車を逆さに漕ぐこともあまりないこと、A層に対して1/4周期ずらしたB層用の回転盤をうまく固定できないし壊れたら直せないだろうということから、このようになっています。ホットメルトで固定してあったのですが、展示中数回この円盤は外れました。最後はガムテ固定でどうにかしました)
  • ソフト
    Untiy4.5.4+Oculus SDK 0.4.2で開発しました。PCがASUS


    ノートパソコン - U24A - ASUS
    で、グラフィックオンボードチップ(Intel4000HD)の、DK2には心もとないスペックです。そのためエフェクトの類を極力きり、DirectモードでFPS75を維持しています。Oculus Riftの体験の質を上げているのは何よりも、人の動きに対する画面の追従速度で、立体視や解像度などおまけに過ぎません。なので、最近のデスクトップPCなら問題なく動くはずです。
    Oculus Riftの体験の質を上げているのは何よりも、人の動きに対する画面の追従速度で、立体視や解像度などおまけに過ぎません。大事なことなので二回書きました。これだけ覚えておけばOKです。

4.これまで展示をしてきて感じたことなど

  • A.筐体と回路は頑丈に、きれいに作ろう
    mbed祭りに参加するまで、ブレッドボード上で回路を組んでいたのですが、mbed祭りでは輸送中に壊れました。また体験会中にDK2の配線が絡みまくりました。ペダルもフレームなしでおいていただけだったので、ガタンガタンと前後左右にズレて漕ぎづらそうでした。この時MFTへの参加が決まっていたころだったので、「このままだとあの規模の体験会だと確実に壊れるぞ」と感じて基板、筐体ともにちゃんと作りました。またフレームの角でけがをしないよう、各所養生やゴムで保護などをしています。
  • 無線と有線
    裏オキュフェスまでは、mbedとPCはbluetooth接続だったのですが、春のVR/AR祭りに出そうとした際、PCとの相性(デモ用PCがほかの人のだった)、会場の電波の込み具合(ワイヤレスLANだらけだったらしい)からか、接続できず封印されていたということがあったので、MFTでは有線にしています。電波が込み合う会場の場合、無線で通信しているものはこの辺リスクになります。
  • 運び込むことを考えよう
    デモ用に作っているため、自分で輸送できることが大切です。そのため車に運び込めるサイズに抑えること、分解組立が可能なようにしています。大きいですが、できるだけ軽く作ったので一人で持ち上げることもできます。ビッグサイトに運ぶ際は、コレをもって階段を上っていたので、周りの人に心配されたり奇異の目で見られたりしました…
    あと、平地移動用にキャスターが付け外しできるようにしています。
  • ゲーブルが引っかからないようにしよう

    f:id:machinemaker:20141207212341j:plain

    f:id:machinemaker:20141207212332j:plain
    Oculus Rift DK2はケーブルお化けです。ワイヤーが絡んだり、人がひっかけたりして大参事になります。あと今回のモノは人の脚で動かす非電動器具とはいえ回転モノな ので、巻き込みに気を付けなければなりません(本当は回転部を露出しないようにフルカバーしたかったところです。巻き込みそうな服装でないかは一応チェッ クしてました)。なので、ケーブル類はスパイラルケーブルでまとめ、フレームにケーブルタイやテープなどで固定しています。センサ系のワイヤーはコレで固定しています。USBハブを使ってPCと接続 するようにすると、ハブのケーブルだけを扱えばいいのでセットアップが楽です。

  • 展開性
    組みあがり状態で車に積めるので、現地ではUSBハブを二個PCとつなぎ、カメラの脚立を置くだけでセットアップ完了です。運び込める時間が決まっているので、展開・撤収が速いと楽です。
  • 説明書をタイトルで表示
    説明書をタイトル画面で表示していました。が、一人5分とかの体験だと、そもそも操作を複雑にしないことが肝心だろうなぁ…。説明だけで1分かかったりするので。展示会によっては「漕ぐだけ」のような形でもありじゃないかなと思っています。
  • 難易度調整
    本物のパラメータに比べ、思いっきり操縦をしやすくしています。1月のocufesでは5割程度のクリアだったのですが、7-8割くらいの人はクリアできるようでした。1月のocufesで展示するまえ、昔人力飛行機を一緒に作っていた人たち(パイロット、非パイロット)に使ってもらったところ、パイロットには「本物より操作しやすい」と言われたものの、それ以外の人たちはほとんどすぐに墜落しました。なので、体験会にもっていくたびに機体パラメータを甘めに書き換えていっています(具体的には、尾翼の位置を本物よりだいぶ後ろにあるように計算させている。また、上反角を実際よりかなり大きくしている)。やはり展示会によっては「漕ぐだけ」のような形でもありじゃないかなと思っています。難易度を選択できるようにしておくべきかな。
  • 制限時間など
    制限時間制、リトライN回まで、みたいにしてあらかじめ説明すると、買い移転率良く回せると思います。
  • キャリブレーション
    カメラの位置のリセット機能を必ずつけましょう。最新版でのやり方はこちらで紹介されています。SDK0.4.2でもCameraConrollerの中にRecenterPose()があるので、それを呼ぶとカメラの位置姿勢がリセットされます。

4.今後

まずは操縦桿が付けたいですね。それから、滑走路を走っているときの振動をフィードバックして離陸時にフッと振動が消える感じを出したり、ペダルの負荷をシミュレーションと連動させてやりたいです。ものが大きいので、展示機会が限られるのですが、そのうちまた何かに出したいと思います。
シミュレータソフトのアップロードは準備中なので、ちょっと待っていていただければ…SDKのアップデートが激しすぎてついていけない!