F1 202Xで1位になるまで続くブログ

グローバル1位を目指します&たまに技術的なことをかきます

B737-800シミュレータの起動・停止方法

この記事は CBcloud Advent Calendar 2020 - Qiita の23日目です。

航空ファンのみなさん(?)、こんばんは!

業務都合と自分の興味が噛み合う時にしかブログを書かなかった結果、2年の時が流れました。 その間に会社を移り、興味の対象もF1 2018からMSFS2020へ広がりを見せています。

まぁそれは置いといて早速やっていきましょう

はじめに

CBcloud社内にはコクピットエリアと呼ばれるオープンスペースがあり、その前方には文字通りコクピットがあります。 www.wantedly.com

こちら実機のパイロットが訓練に使う機材そのままのいわゆるガチなシミュレータとなっています。 代表の松本は元航空管制官であり、実機パイロットの友達がオフィスでガチ練習をしていることもあります。 ※事情を知らずに近くで作業していたらチェックリスト読み上げてる風のガチっぽい人が飛ばしていてすごいなーと思ったら本物だったという

ここから本題なのですが、現状これを扱えるのが代表の松本だけであり、こんなすごい機材があるのに他のメンバーは触り方がわからなくてほぼ眠っています。 それはもったいないし、僕が好きに飛ばしたい(重要)のでここに起動・停止の手順と注意点などをまとめたいと思います。

シミュレータについて

松本によると f:id:jkym99:20201228175656p:plain ということです。このあたりは別途掘り下げる機会があればまとめたいと思います。

CHECKLIST(起動)

1. シミュレータPC

f:id:jkym99:20201228154431j:plain
シミュレーターを動作させるPCは向かって右端です
f:id:jkym99:20201228185115j:plain
画像の1と2を立て続けにONします

1. 電源タップ&黒いスイッチON

f:id:jkym99:20201228154716j:plain
電源タップON
f:id:jkym99:20201228154725j:plain
間髪入れずにボタンスイッチをプッシュです

2. 中央のディスプレー点灯を確認

f:id:jkym99:20201228154731j:plain
しばらく待って赤枠のモニターがこのようになればOKです

2. プロジェクター

コクピットを囲むようにスクリーンがあり、3台のプロジェクターで投影しています

1. プロジェクターON

f:id:jkym99:20201228154740j:plain
3台のプロジェクターを1つのリモコンで同時起動するので高めの位置から天に向けてONです

2. ランプ交換のメッセージがでるのでOKを2回

基本は2回押せばOKですが、3台のプロジェクターを同時に操作するので、どれか反応していない場合もあります。 その際はメッセージが消えるまでOK押下を繰り返しです。

f:id:jkym99:20201228154809j:plain
1つ目のメッセージ
f:id:jkym99:20201228154811j:plain
2つ目のメッセージ
f:id:jkym99:20201228154853j:plain
プロジェクターが起動してしばし待つ
f:id:jkym99:20201228155003j:plain
起動しましたね!

3. 副操縦士PFD、ND

コクピットの向かって右、副操縦士が使用するPFD、NDのディスプレーは別途スイッチを入れる必要があります ※PFD(プライマリー・フライト・ディスプレイ) ※ND(ナビゲーション・ディスプレイ)

f:id:jkym99:20201228155044j:plain
副操縦士側のディスプレイを起動しましょう

1. トグルスイッチ2つをON、両ディスプレイが映っていることを確認

f:id:jkym99:20201228154432j:plain
コクピットのこのあたりにあります
f:id:jkym99:20201228155128j:plain
このパネル
f:id:jkym99:20201228155101j:plain
外側のトグルスイッチをこんな感じで同時にONです
f:id:jkym99:20201228155108j:plain
PFDとNDが表示されればOKです

4. シミュレータ設定

1. Edgeを起動

f:id:jkym99:20201228155324j:plain
画面中央のショートカットからEdgeを立ち上げましょう

2. ローディング画面がしばし続くが、いったん画面更新すると消える

f:id:jkym99:20201228155232j:plain
ショートカットからEdgeを立ち上げるとグルグルしています
f:id:jkym99:20201228155242j:plain
リロードすると消えます

3. 訓練用の障害をOFFにする

デフォルトでは訓練用なのかトラブルが起きる設定が入っているのでOFFにしておきましょう。 左メニューのFailuresです

f:id:jkym99:20201228155258j:plain
3つともRemoveです
f:id:jkym99:20201228155311j:plain
この状態になればOKです

起動完了

以上でシミュレータの起動は完了です! この状態は、いわゆる「Cleared for Takeoff」をもらった状態(だと思われます)なので、パーキングブレーキを解除し、スロットルを入れれば離陸滑走を始めることができます。 シム側の設定次第で実機同様エンジン始動から始めることもできますが、デフォでは滑走路上で離陸準備完了という感じになっていそうです。

出発地の変更

以下の手順で現在地を変更できるので、好きな空港から出発することができます

f:id:jkym99:20201228155357j:plain
Position => Airportを選択
f:id:jkym99:20201228155441j:plain
RJBB(関西国際空港)にしてみましょう
f:id:jkym99:20201228155456j:plain
空港内の位置を指定します。これはRJBBのParking1です
f:id:jkym99:20201228155507j:plain
ダイアログがでるのでOKです
f:id:jkym99:20201228155522j:plain
しばし待ちます
f:id:jkym99:20201228155601j:plain
こうなれば変更完了です

シミュレータのポーズ・解除

操縦席・副操縦席共にシムをポーズする為のボタンがあります。設定変更やシミュレーター終了時に必要になります。

f:id:jkym99:20201228155717j:plain
コクピット左手、機長席は操縦桿の左手裏側、ゲームでいうとZLボタンの位置にあります
f:id:jkym99:20201228155707j:plain
向かって右側の副操縦席はZRの位置です

CHECKLIST(停止)

1. シミュレータ終了

f:id:jkym99:20201228155812j:plain
まずはシムをポーズします
f:id:jkym99:20201228155828j:plain
左メニューのMaint押下
f:id:jkym99:20201228155843j:plain
Misc => Shutdownです
f:id:jkym99:20201228155853j:plain
OKです
f:id:jkym99:20201228155938j:plain
プロジェクター画面にwin終了時のこれが出るので、上のほうの画像にあった黄色いシールのマウスで強制終了です

2. プロジェクター停止

f:id:jkym99:20201228160003j:plain
プロジェクターのリモコンで、最上段中央(親指の位置)のボタンを間隔あけて2回押下でプロジェクターOFFです
f:id:jkym99:20201228160013j:plain
1回押すとこうなるので2回目プッシュです

3. 電源タップOFF

f:id:jkym99:20201228160035j:plain
最後はこれをパチンとやって終了です

4. 終了

f:id:jkym99:20201228160221j:plain
お疲れ様でした!これで完全停止しました

F1 2018のUDPテレメトリーでオリジナルダッシュボードを作る

この記事は SmartDrive Advent Calendar 2018 - Qiita の15日目です。

F1ファンのみなさん(?)、こんばんは!

このブログの表題を実現すべく日夜タイムアタックで世界1位を目指し(いまのところ4900位あたりを彷徨って)いる id:jkym99 です。 その傍らでSmartDriveのバックエンドエンジニアとしてRubyやGoで色々していたりもします。

アドベントカレンダーをきっかけに作ったブログですが、当面はF1 2019が発売されるまでに公約を実現できるよう頑張っていきます。

さて、初回はPS4版 F1 2018に用意されているUDPテレメトリー機能で色々やるための準備編といった感じでお送りします。

F1 2018とUDPテレメトリー

F1 2018はコードマスターズが開発しているF1公式レースゲームでPC/Xbox One/PS4向けに展開されています。 毎年夏頃にその年のデータを反映した新作が発売されていて、2018年も9/20に発売されました。

www.ubisoft.co.jp

最近のレースゲーム、中でもレースシムと言われるようなカテゴリのタイトルには実車F1さながらに走行中の速度やタイム、ハンドル・アクセル・ブレーキ操作をはじめとした様々なデータをUDPプロトコルで送信する、いわゆるテレメトリー機能が搭載されています。

formula1-data.com

ユーザーはこれに対応したアプリケーションを利用する事でゲーム画面には表示されない多くの情報を得ることができます。

例えばこのRS Dashというアプリを使うと、 こんな感じのデータを簡単に確認することができます。 f:id:jkym99:20181212232159j:plain

これだけでもまぁ便利ではあるんですが、以下のような不満がでてきます

  • 走行中に見たいデータとあとで振り返る際に見たいデータの内容やそれに適した表現は異なる
  • そもそもあとからデータを振り返るような機能がない
  • レイアウトやフォントサイズの問題で視認性が悪い
  • もっとカッコいいやつがいい

現代F1はデータドリブンですから、闇雲に走り回るのはいったんやめにして、このあたりを解消しつつカッコいい感じのやつを作っていきましょう

UDPテレメトリーを受信する

まずはゲームからUDPで送信されてくるデータを受信してプログラムから扱えるようにしなくてはいけません。

パケットのデータ構造

UDPパケットをデコードするためには構造を知らなければいけませんが、そもそも公開されているのか・・・

前述のRS Dashなど3rd partyのアプリが複数存在しているので、どこかにあるはずだと思ったらCodemastersのフォーラムにありました! (下記のスレッドを読み進めるとわかりますが、内容が所々間違っていてツッコミが入ってるので注意が必要です...)

F1 2017 D-Box and UDP Output Specification — Codemasters Forums

データをJSONで出力する

まず、ゲーム側のテレメトリー設定をこのようにします (ゲームのポーズメニュー > お好みの設定 > テレメトリー設定)

テレメトリーデータをJSON文字列で表示するプログラムをGoで実装するとこんな感じです。

https://gist.github.com/jkym/cac411448ca5f5ad8e9098d2cecacc0b

これを go run main.go などとすれば60Hzで巨大なJSONがガガーっと流れてきます。

ちょっと長いですが、こんな感じ

{
    "Time": 484.2308,
    "LapTime": 10.225553,
    "LapDistance": 4689.0405,
    "TotalDistance": 9990.318,
    "X": 342.84232,
    "Y": 2.9368935,
    "Z": 679.2339,
    "Speed": 24.266415,
    "Xv": -24.263298,
    "Xy": -0.1229023,
    "Xz": -0.36903763,
    "Xr": 0.04496999,
    "Yr": -0.008158523,
    "Zr": -0.9989565,
    "Xd": -0.9989795,
    "Yd": -0.004903322,
    "Zd": -0.044930995,
    "SuspPos": [
        -3.1337132,
        6.698986,
        -1.6587093,
        5.8877153
    ],
    "SuspVel": [
        67.383484,
        138.62654,
        -5.815205,
        10.607468
    ],
    "WheelSpeed": [
        23.361992,
        24.848207,
        23.61155,
        25.07684
    ],
    "Throttle": 0.028936876,
    "Steer": 0.60996044,
    "Brake": 0,
    "Clutch": 0,
    "Gear": 3,
    "GforceIat": 2.3192554,
    "GforceIon": -0.56821126,
    "Lap": 2,
    "EngineRate": 8281.013,
    "SliProNativeSuport": 0,
    "CarPosition": 1,
    "KersLevel": 400000,
    "KersMaxLevel": 400000,
    "Drs": 0,
    "TractionControl": 0.008800052,
    "AntiLockBrakes": 1,
    "FuelInTank": 10,
    "FuelCapacity": 105,
    "InPits": 0,
    "Sector": 2,
    "Sector1Time": 0,
    "Sector2Time": 0,
    "BrakesTemp": [
        32.077118,
        32.077118,
        32.077118,
        32.077118
    ],
    "TyresPressure": [
        21.5,
        21.5,
        23,
        23
    ],
    "TeamInfo": 1,
    "TotalLaps": 1,
    "TrackSize": 5301.278,
    "LastLapTime": 91.03247,
    "MaxRPM": 13500,
    "IdleRPM": 4300,
    "MaxGears": 9,
    "SessionType": 0,
    "DrsAllowed": 0,
    "TrackNumber": 0,
    "VehicleFIAFlags": 0,
    "Era": 0,
    "EngineTemperature": 90,
    "GforceVert": 0.012075849,
    "AngVelX": -0.039477743,
    "AngVelY": 0.90246755,
    "AngVelZ": -0.025669953,
    "TyresTemperature": [
        100,
        100,
        100,
        100
    ],
    "TyresWear": [
        0,
        0,
        0,
        0
    ],
    "TyreCompound": 1,
    "FrontBrakesBias": 60,
    "FuelMix": 3,
    "CurrentLapInvalid": 0,
    "TyresDamage": [
        0,
        0,
        0,
        0
    ],
    "FrontLeftWingDamage": 0,
    "FrontRightWingDamage": 0,
    "RearWingDamage": 0,
    "EngineDamage": 0,
    "GearBoxDamage": 0,
    "ExhaustDamage": 0,
    "PitLimiterStatus": 0,
    "PitSpeedLimit": 37,
    "SessionTimeLeft": 3.4028235e+38,
    "RevLightsPercent": 0,
    "IsSpectating": 0,
    "SpectatorCarIndex": -1,
    "NumCars": 1,
    "PlayerCarIndex": 0,
    "CarData": [
        {
            "WordPosition": [
                342.84232,
                2.9368935,
                679.2339
            ],
            "LastLapTime": 91.03247,
            "CurrentLapTime": 10.225553,
            "BestLapTime": 91.03247,
            "Sector1LapTime": 0,
            "Sector2LapTime": 0,
            "LapDistance": 4689.0405,
            "DriverID": 13,
            "TeamID": 1,
            "CarPosition": 1,
            "CurrentLapNum": 3,
            "TyreCompound": 1,
            "InPits": 0,
            "Sector": 0,
            "CurrentLapInvalid": 0,
            "Penalties": 0
        }
    ],
    "Yam": -1.6157819,
    "Pitch": -0.0045316555,
    "Roll": 0.008158601,
    "XLocalVelocity": 0.72146493,
    "YLocalVelocity": -0.0098551065,
    "ZLocalVelocity": 24.25572,
    "SuspAcceleration": [
        4349.485,
        3506.0671,
        1856.6912,
        4040.6658
    ],
    "AngAccX": -2.3219652,
    "AngAccY": -0.38878247,
    "AngAccZ": 2.6509306
}

さて、ようやくテレメトリーデータを自由に加工する準備が整いました。

カッコいいGUIを作る

ちなみに走行中にドライバーが見る為のダッシュボード(スピードメーターとかですね)ならSimHubというのがあります。 ハンドルコントローラーにLEDのシフトインジケーターをつけてピカピカさせたい場合に必要な制御などをいい感じにやってくれるようです。

SimHub, DIY Sim racing Dash - Going mobile | RaceDepartment

こんなやつです

カッコいい。

もうこれでいいじゃんと思ってしまいそうですが、SimHubは走行時のメーターなのでちょっと用途が違うのと

  • 後から任意のデータをグラフ表示したい
  • 車両やコース、天候などで検索したい

などなど色々やりたい、というか人と同じことをしていては1位はとれません的に考えるとDIYしないといけません。 この辺も実車F1同様に競争の一部ということですね。

今回は同僚におすすめされたUnityで作ってみることにします。 Unity未経験ですが、とりあえずやってきましょう。

GoからUnityにテレメトリーデータを送信する

先程のGoのプログラムを少し修正して流用します。 とりあえずUnityで速度を表示してみたいので、Goのサーバーでは

  • クライアントからwebsocket接続を受け付け
  • UDPパケットを受信してwebsocketで送信

ということをやります。

websocketサーバの実装にはこちらを利用しました

GitHub - gorilla/websocket: A WebSocket implementation for Go.

go get github.com/gorilla/websocket してから以下のコードを main.go として保存し、go run main.go でwebsocketサーバが起動します。

Decode UDP Telemetry & Websocket Server · GitHub

Unityのセットアップ

Unity初心者なので公式ドキュメントをいったん読み込みつつ進めます。

Unity マニュアル (2018.2) - Unity マニュアル

詳しい説明は公式や他のサイトに譲ってここではポイントだけ

  • Unity HubとUnityをインストール
  • 新規プロジェクトを作成する際のTemplateを2D
  • エディタが開いたら Hierarchy > CreateのドロップダウンからUI > Textをクリック
  • Hierarchy > MainScene > Canvas の下にTextが作成されるのでこれを Speed にリネーム

このSpeedという名前のTextに次で作成するC# Scriptを割り当てていきます。

Websocketライブラリ

Websocket Clientの実装にはこのライブラリを使うことにしました。

https://github.com/sta/websocket-sharp

READMEを参考にdllを作成し、プロジェクトのAssetsに配置します。

C# Scriptの作成

まずはWebsocketから受信したJSONを扱うためにGoと同じ要領でClassを定義します。

Project > Assets > Scripts > TelemetoryModel

using System;

[Serializable]
public class TelemetryModel
{
    public double AngAccX;
    public double AngAccY;
    public double AngAccZ;
    public double AngVelX;
    public double AngVelY;
    public double AngVelZ;
.
.
.
(省略)

Websocket Clientはサンプルを参考にしてこんな感じに

https://github.com/sta/websocket-sharp

Project > Assets > Scripts > WebsocketClient

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

public class WebsocketClient : MonoBehaviour {
    public WebSocket ws;
    private TelemetryModel telemetry;

    // Websocketから受信したデータを保持
    protected List<TelemetryModel> telemetryList = new List<TelemetryModel>();

    void Start() {
        ws = new WebSocket("ws://localhost:9999/ws");
        ws.OnOpen += (sender, e) => {};

        // このOnMessage内の処理はバックグラウンドスレッドから実行される
        // データを受信したらリストに追加
        ws.OnMessage += (sender, e) => {
            lock (telemetryList)
            {
                telemetryList.Add(JsonUtility.FromJson<TelemetryModel>(e.Data));
            }
        };

        ws.Connect();
    }
    // こちらはメインスレッドからフレーム毎に呼び出される
    void Update () {
        lock (telemetryList)
        {
            if (telemetryList.Count > 0)
            {
                telemetry = telemetryList[0];
                telemetryList.RemoveAt(0);
            }
        }
    }

    // メートル/秒から時速に変換
    public string Speed() {
        if (telemetry != null) {
            return Mathf.Round((float)(this.telemetry.Speed * 60 * 60 / 1000)).ToString();
        }

        return "";
    }
}

UIにアタッチするコードはフレーム毎に↑に実装した Speed() メソッドから速度の値を取得しUIに反映します。

Project > Assets > Scripts > Speed

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

public class Speed : MonoBehaviour {
    GameObject refObj;
    public Text uiText;

    void Start () {
        refObj = GameObject.Find("TelemetryModel");
        uiText = this.GetComponent<Text>();
        uiText.text = "0";
    }
    
    void Update () {
        // Websocket Clientからスピードを取得してUIのテキストにセット
        uiText.text = refObj.GetComponent<WebsocketClient>().Speed();
    }
}

このSpeedというスクリプトを同名のSpeedというTextにドラッグするとスクリプトから制御できるようになります。

これらのC#コードとUnity上に配置した2D Textの関連はこんな風になっています

f:id:jkym99:20181215172431p:plain

ちょっと雑な説明になりましたが、これでUnity側も完成です!

動かしてみる

さっそく動かしてみましょう

  • PS4側でタイムトライアルなど走行中の状態にしてポーズ
  • Goのサーバを起動
  • Unityエディタ上でPlay

この状態でゲームのポーズを解除すると・・・

カッコいいには程遠いですが、ゲーム内のUIから数フレームくらいの遅延で表示できてそうです!

まとめ

ここまででゲームのテレメトリーデータをUnityアプリで表示する基本部分が完成しました。

いったん動くようになったのはいいんですが、今回の検証ではGoのサーバを介してJSONでやり取りしているせいもあって、ゲーム画面とUnityで1〜最大10フレーム程度遅延しています。

今後はそのあたりのアーキテクチャ変更や以下のあたりをやりたいと思います

  • Simhubのようにエンジン回転数やラップタイムなどを追加してカッコいいダッシュボードにする
  • Unityアプリで直接テレメトリーデータを受信して遅延低減
  • 時系列データベースにデータを投げ込んで検索できるようにする
  • 走行中のデータと過去のデータをリアルタイムに比較してレース戦略に活かせるような何か

す、すごく色々あって走り込む時間とか仕事する時間が無くなりそうです。

おわりに

弊社では 僕と一緒にF1 2018で走ったり作ったりして遊びたい エンジニアを募集してます!

※ちなみにこういう人種は社内で少数派なので、車自体には興味がない方もぜひ!

※免許持ってないデータサイエンティストやディレクター、エンジニアも活躍してます

www.wantedly.com