レースゲーム(Assetto Corsa)の走行データをPlotlyで可視化してみた

# はじめに

私はレースゲームが好きでグランツーリスモ5/6をやり込んでいたのですが、Assetto Corsaに移ってから全く上手く走れていないことに気付きました。具体的には、AI車両（Strength=100%）に1周4秒近く離される状況です（カタルニアサーキット/TOYOTA GT86、ゲームパッド使用時)。
GT6ではABS=1/TCS=1以外のアシストOFFでオールゴールド取れているので、そんなに下手ではないと思っていたのですが…

なぜこれほど差が付くのかを分析するために、可視化ツールのお勉強も兼ねて、自分のプレイとAIのプレイの走行データをPythonで取得して、Plotlyで可視化してみました。

ちなみにですが、データ取得＆可視化するだけならMotec i2 Proというツールがあります。今回は可視化ツールの調査も兼ねているので、Motecは使いませんが。

# データ取得

Assetto Corsaには 「in-game app」(ゲーム内アプリケーション)と呼ばれる仕組みがあり、走行データをゲーム画面上に表示するアプリケーションをユーザーがPython言語を用いて独自に開発することができます。走行データを取得するためのAPIや画面上に表示を行うためのAPIなどが準備されています。

- 参考情報
    - チュートリアル：[Getting started with Assetto Corsa application development](https://github.com/ckendell/ACAppTutorial/blob/master/ACAppTutorial.md)
    - APIドキュメント：[Assetto Corsa Python documentation](https://www.assettocorsa.net/forum/index.php?threads/python-doc.517/) ※ログイン必要

これらの参考情報をもとに、以下の情報を取得するプログラムを作りました。

- 周回数
- 現在の周回の経過時間(秒)
- スタート地点からの距離(スタート地点=0～ゴール地点=1.0)
- 車速(km/h)
- アクセル開度(0.0～1.0)
- ブレーキ開度(0.0～1.0)
- ギア
- エンジン回転数(rpm:revolutions per minute)
- ステア(ハンドル)の切り角(degree)
- 車両の現在位置 (3D座標)

[ACTelemetry.py](https://github.com/abe-masanori/AC_analysis/blob/master/data_acq/ACTelemetry.py)

```ACTelemetry.py

class ACTelemetry:

(略)

    def logging(self):
        if self.outputFile == None:
            return
        
        lapCount = ac.getCarState(self.carId, acsys.CS.LapCount) + 1
        lapTime = ac.getCarState( self.carId, acsys.CS.LapTime)
        speed = ac.getCarState(self.carId, acsys.CS.SpeedKMH)
        throttle = ac.getCarState(self.carId, acsys.CS.Gas)
        brake = ac.getCarState(self.carId, acsys.CS.Brake)
        gear = ac.getCarState(self.carId, acsys.CS.Gear)
        rpm = ac.getCarState(self.carId, acsys.CS.RPM)
        distance = ac.getCarState(self.carId, acsys.CS.NormalizedSplinePosition)
        steer = ac.getCarState(self.carId, acsys.CS.Steer)
        (x, y, z) = ac.getCarState(self.carId, acsys.CS.WorldPosition)
        
        self.outputFile.write('{}\t{:.3f}\t{:.4f}\t{:.2f}\t{:.3f}\t{:.3f}\t{}\t{:.0f}\t{:.1f}\t{:.2f}\t{:.2f}\t{:.2f}\n'.format(\
        lapCount, lapTime/1000, distance, speed, throttle, brake, 
        gear, rpm, steer, x, y, z))
        
(略)

def acUpdate(deltaT):
    global telemetryInstance
    telemetryInstance.logging()

```
コードの細かい説明は割愛しますが、in-game appの仕組みでは`acUpdate(deltaT)`がグラフィック更新の度（私の環境では1秒間に60回）実行されます。`acUpdate(deltaT)`から呼び出される`ACTelemetry.logging()`においてデータ取得＆ファイル出力をしています。

このin-game appを有効にするためには、以下の手順を行います。

1. 「(Steamインストール先フォルダ)\steamapps\common\assettocorsa\apps\python\ACTelemetry」以下にACTelemetry.pyを配置
2. ゲーム内で「Options」⇒「General」⇒「UI Models」で「ACTelemetry」にチェック
3. ゲームプレイ時（リプレイ時でもOK）にマウスを画面右端に移動し、表示されたアプリの中から「ACTelemetry」を選択

以下のようなUIが表示されるので、「Next」ボタンでデータ取得対象の車両を選択し、「Start」ボタンでログ取得を開始します。
![ui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/e06040af-2b92-4a17-12e1-ed688eb4e022.png)

結果、以下のようなデータが取得できます。このデータを自分のプレイとAIのプレイの両方取得して比較したいと思います。

``` logger_20190817_1257.log
Course : ks_barcelona
Layout : layout_gp
Car Id : 0
Driver : abe.masanori
Driver : ks_toyota_gt86

lapCount	lapTime	distance	speed	throttle	brake	gear	RPM	steer	x	y	z
1	151.829	0.9399	115.7	1.00	0.00	4	6425	33	490.4	-14.6	-436.3
1	151.846	0.9400	115.8	1.00	0.00	4	6425	33	490.5	-14.6	-435.7
1	151.862	0.9401	115.8	1.00	0.00	4	6421	33	490.5	-14.7	-435.2
1	151.879	0.9402	116.0	1.00	0.00	4	6425	33	490.6	-14.7	-434.7
```

# 可視化する前のデータ整形

今回はPlotlyのJavascriptライブラリを利用してデータを可視化したいと思います。上述のヘッダ付きタブ区切りファイルのままでも扱えるのですが、多少面倒なので、事前に以下の加工・整形を加えます。

- ヘッダ（先頭5行の情報や項目の行）の削除
- 該当周回以外のデータ行削除
- 該当周回のデータにおいても先頭5行と最後5行の削除（スタート/ゴール前後に発生するおかしなデータを除去するため）
- タブ区切りからJavascript配列にフォーマット変換

以下のようなファイルになります。

``` my_data_before.js
my_data = [
    [2, 0.125, 0.0017, 155.96, 1.000, 0.000, 5, 6672, 0.0, 365.52, -18.43, -187.32],
    [2, 0.142, 0.0019, 155.96, 1.000, 0.000, 5, 6672, 0.0, 365.13, -18.43, -186.72],
    [2, 0.158, 0.0020, 156.11, 1.000, 0.000, 5, 6674, 0.0, 364.73, -18.43, -186.11],
    [2, 0.175, 0.0022, 156.11, 1.000, 0.000, 5, 6676, 0.0, 364.34, -18.44, -185.51],
    (以下略)
```

# 取得したデータの可視化

Plotlyを使って、以下のようなVizを作ってみたいと思います。（[こちら](https://abe-masanori.github.io/AC_analysis/data_viz/viz_before.html)から実際に動かせます。ちょっと重いですがアニメーションGIFは[こちら](https://abe-masanori.github.io/AC_analysis/data_viz/ui.gif)）

- 速度、アクセル/ブレーキ開度、ギア、エンジン回転数、ステアリング角度について、自分とAI(CPU)のデータをスタートからの距離を横軸にして線グラフで左側に表示する。
- 自分のコース上の走行位置を右側に表示する。
- 速度のグラフで横軸範囲指定(Zoom)すると、他のグラフもそれに追従する。

この手のデータは、通常は横軸=時刻、縦軸=メトリクスを表示させるものなのですが、今回それをやると自分とAI(CPU)のデータが比較しづらいので、横軸にはスタートからの距離を表す数値（0.0:スタート ～ 1.0:ゴール）を採用します。

![goal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/25e7e732-2429-faa7-8ea3-19f92ee78cf1.png)

## HTMLファイル作成

まず、ベースとなるHTMLファイルを作成します。

- Plotlyのライブラリを読み込みます。
- `div`要素にPlotlyでグラフを差し込むことになるので、グラフ表示の`div`に`id`を付与しておきます。

[viz_before.html](https://github.com/abe-masanori/AC_analysis/blob/master/data_viz/viz_before.html)

```viz_before.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>自分(改善前)とAIのデータ比較</title>
        <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
        <style>
            html,
            body {
                margin: 0;
                padding: 0;
                height: 100%;
                display: flex;
            }
            </style>
    </head>
    <body>
        <div>
            <div id="div-speed"></div>
            <div id="div-throttle"></div>
            <div id="div-brake"></div>
            <div id="div-gear"></div>
            <div id="div-rpm"></div>
            <div id="div-steer"></div>

        </div>
        <div id="div-position"></div>
    </body>
    <script src="data/my_data_before.js"></script>
    <script src="data/cpu_data.js"></script>
    <script src="my_viz.js"></script>
</html>
```

## 速度グラフなどの作成

速度、アクセル/ブレーキ開度、ギア、エンジン回転数、ステアリング角度についてはほぼ同じグラフになるため、自分のデータ、AI(CPU)のデータ、グラフ作成位置、縦軸タイトルを引数として渡して線グラフを作成する関数を作成します。

[my_viz.js](https://github.com/abe-masanori/AC_analysis/blob/master/data_viz/my_viz.js)

```my_viz.js
function plot_speed(my_x, my_y, cpu_x, cpu_y, divId, title_y){
    var data_me = {
        x: my_x,
        y: my_y,
        mode: 'lines',
        name: 'me'
    };

    var data_cpu = {
        x: cpu_x,
        y: cpu_y,
        mode: 'lines',
        name: 'cpu'
    };

    var layout = {
        autosize: false,
        yaxis: {title: title_y},
        width: 600,
        height: 250,
        margin: {l: 70, r: 70, b: 25, t: 25}
    };

    Plotly.newPlot(divId, [data_me, data_cpu], layout);
}

my_data_distance = Array.from(my_data, x => x[2]);
cpu_data_distance = Array.from(cpu_data, x => x[2]);

my_data_speed = Array.from(my_data, x => x[3]);
cpu_data_speed = Array.from(cpu_data, x => x[3]);
plot_speed(
    my_data_distance, my_data_speed, cpu_data_distance, cpu_data_speed, 
    'div-speed', '速度 (km/h)'
);
(以下略)
```

2次元配列をそのまま渡せないのはちょっと面倒に感じます。

## 位置データのグラフ作成

こちらもグラフ作成の関数を作成します。

- 後で距離で絞り込みを行うため、表示するデータの距離下限と上限を引数で渡し、その情報に基づきデータを絞り込みます。
- こちらのグラフはデータを入れ替えて再作成が発生するので、`Plotly.newPlot()`関数ではなく`Plotly.react()`関数を利用します（Plotlyのリファレンスによると`newPlot()`より`react()`の方が「far more efficiently」らしいのですが、なら`react()`だけ使えば良いのではと思わなくもないのですが…）
- 位置データに関して、南北(z)の値はプラス側が南、マイナス側が北となるため、`range: [600, -600]`の指定を指定して上下を逆転させています。`autorange: 'reversed'`でも上下逆転させられるのですが、データ絞り込みの際に軸の範囲が変わり縦横比が変わってしまうので、固定値を指定しています。
- データを絞り込んだ際に、コースの度の部分に該当するか分かりやすくするために、コース図をグラフの背景画像として設定します。

```my_viz.js
function plot_position(min_distance, max_distance) {
    my_x = Array.from(my_data.filter(v => (min_distance < v[2]) && (v[2] < max_distance)), x => x[9]);
    my_z = Array.from(my_data.filter(v => (min_distance < v[2]) && (v[2] < max_distance)), x => x[11]);

    my_pos = {
        x: my_x,
        y: my_z,
        mode: 'scatter',
        mode: 'line',
    };

    var layout = {
        xaxis: {autorange: false, range: [-600, 600]},
        yaxis: {autorange: false, range: [600, -600]},
        autosize: false,
        width: 300,
        height: 300,
        margin: {l: 50, r: 50, b: 50, t: 50, pad: 10},
        showlegend: false,
        images: [{
            source: 'pos_base.png',
            xref: 'x',
            yref: 'y',
            x: 500,
            y: 600,
            xanchor: 'right',
            yanchor: 'bottom',
            sizex: 1000,
            sizey: 1200,
            sizing: 'stretch',
            opacity: 0.4,
            layer: 'below'
        }]
    };

    Plotly.react('div-position', [my_pos], layout);
}
```

## グラフ間の連携

速度グラフで横軸の範囲選択(Zoom)をした場合、そのイベントをトリガーに他のグラフにもその結果を反映させます。

- Plotlyではマウスのドラッグ＆ドロップで範囲選択した場合と、グラフ上でダブルクリックするなどしてZoom解除した場合で発生するZoomイベントの内容が異なりるので、`if..else..`で処理を分けます。
- アクセル/ブレーキ開度、ギア、エンジン回転数、ステアリング角度は横軸の範囲を変えるだけなので、`Plotly.relayout()`でグラフのレイアウトを変更します。
- 位置グラフについては、表示データを絞り込む必要があるので、グラフを再作成します（上で作成した`plot_position()`関数の実行）。

```my_viz.js
document.querySelector('#div-speed').on(
    'plotly_relayout',
    function(eventdata) {
        if(eventdata['xaxis.autorange']) {
            x_start = 0.0;
            x_end = 1.0;
            option = {'xaxis.autorange': true};
        } else {
            x_start = eventdata['xaxis.range[0]'];
            x_end = eventdata['xaxis.range[1]'];
            option = {'xaxis.range': [x_start, x_end]}
        }

        Plotly.relayout('div-throttle', option);
        Plotly.relayout('div-brake', option);
        Plotly.relayout('div-gear', option);
        Plotly.relayout('div-rpm', option);
        Plotly.relayout('div-steer', option);

        plot_position(x_start, x_end);
    }
);
```

# 可視化結果の確認

これで、以下の流れで分析することができるようになりました。

1. 速度グラフを見て、自分がAI(CPU)より遅い部分を確認する。
2. 遅いと特定した部分にZoomする。
3. 他のメトリクスを確認して、遅い原因を特定する。

（速度グラフをダブルクリックすれば、Zoomは解除されます）

まぁ、私が遅い原因を確認するにはそこまでする必要はなく、単純にステアリングの角度が大き過ぎたというだけなのですが。
原因が分かってしまえば単純ですが、GT5/6ではステアリングの角度に上限が設けられており（動的に変わるらしい）、ここまでひどい状況にはならなかったようです。

![newplot (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/4f6edc13-4140-87f3-09f9-70c957ce0bc3.png)

ステアリングの切り過ぎが原因だということを意識してプレイすることで、AI(CPU)との差も4秒から1秒以下までに縮めることができました。ただ、再度データを確認すると、まだステアリングが切り過ぎ＆切り方が急なようなので、まだまだ改善しなければいけませんが。

[自分(改善後)とAIのデータ比較](http://localhost:8000/viz_after.html)

# Plotly感想

今回、データを可視化するにあたり、Plotly以外にも以下のツールを試したので、少し感想を。

- Tableau、Qlik Sense
    - 商用製品だけあり、機能も豊富で、作りたいグラフやダッシュボードがサクッと作成できる。
- Grafana
    - 横軸が時間でないグラフをどう作成するのか、よく分からなかった。（できるとは思うけど）
- Metabase、Superset、Redash
    - DB上のデータから１つグラフを作るのはとてもとても簡単。
    - ダッシュボード上で絞り込みをさせる、複数グラフを連携させるとなると、できなくはないけど、結構難しい or 使いづらいUIになりがち。
- picasso.js
    - Qlik Senseの背後で使われているJavascript可視化ライブラリということで使ってみたけど、コード量が多くなりがち & マニュアルを読んでも良く分からない。
- Plotly
    - DBからのデータ取得などはできないので、別途コーディングが必要。
    - 少ないコード量でグラフが作成できるし、Zoomや画像エクスポート、ホバーの機能がデフォルトで有効になっている。
    - ダッシュボード上での複数グラフに渡ったデータ絞り込みや連携はコーディングが必要だが、仕組みが分かれば比較的簡単。

まとめると、Plotlyとても良い。