DCS WorldをLuaスクリプトで楽しむ #01 着弾位置測定

# DCS Worldで使えるLua Script Engineの仕様について

↓ここ読むと必要な情報は全部書いてあります。

https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation

と言って済ませるには不親切なので、今回は「爆弾の着弾地点が目標からどのくらい外れたか計算する」スクリプトを例として作成してみます。

# 便利なエディタを用意しよう
せっかくプログラミングするんですからメモ帳よりはもうちょっとかっこいいものを入れてみましょう

https://azure.microsoft.com/ja-jp/products/visual-studio-code/

VSCODEを上のURLからダウンロードしてインストールします。

# Luaの実行環境を構築しよう

DCS上でLuaスクリプトを実行する上では必要ないですが、「Luaってこういうときどう書くの？」って実験しながら確かめたい時がいずれ出てくると思うので、深くスクリプトに触れたいならやっておくといいです。

下のURLからLuaのビルド版を入手してください。64bit版のWindowsを使用しているなら`lua-5.4.2_Win64_bin.zip`をダウンロードします。

https://joedf.ahkscript.org/LuaBuilds/

ダウンロードしたら解凍してlua.exeの保存されたフォルダ名を「Lua」にリネームしてCドライブ直下にコピーしちゃいましょう。`C:\Lua\Lua.exe`となるようにしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/e43dab00-6449-30a1-cead-a8a5bda5ec03.png)

Windowsの検索ボックスに「システム環境変数の編集」と打ち込んでENTERします(検索ボックスが表示されていない場合はツールバーを右クリックして「検索(H)」→「検索ボックスを表示(B)」)。

「システムのプロパティ」→「詳細設定」タブ→「環境変数(N)」を選択、「環境変数」ウィンドウの「システム環境変数(S)」から変数:Pathを選択して「編集(I)」を押し、「環境変数名の編集」ウィンドウにて「新規(N)」ボタンを押して、新しい行に「C:\Lua」と書いて「OK」します。

コマンドプロンプトを起動して`Lua`と打ち込んで`Lua 5.4.2  Copyright (C) 1994-2020 Lua.org, PUC-Rio`の文字が表示されたら成功です。

## LuaでHello Worldを書こう

VSCODEでファイルを新規作成して次のように記述します。

```Lua:HelloWorld.lua
print("Hello World")
```

`HelloWorld.lua`という名前でファイルを保存して、`C:\Lua`に保存します。
コマンドプロンプトを開いて次のようにコマンドを打ちます。

```cmd:コマンドプロンプト
C:\Users\Wags>cd C:\Lua

C:\Lua>Lua HelloWorld.lua
Hello World
```

Hello Worldと上の例のように表示されたら成功です。

Luaではクラスを定義できます。
クラスとは情報(変数)と情報の取り扱い方(関数)を役割ごとにまとめて固定した装置のようなものです。
次のい例では`Animal`クラスが自分の名前`name`と、自分の名前を言う役割`getName`および、何らかの動作をする`doSomething`役割を定義されており、
`Animal`のより具体的な概念である`Uma`および`Shika`概念が、名前と「何らかの動作」を自分達固有のものに上書きしています。

```Lua:DuckTyping.lua

function Animal()
    local obj = {}

    obj.name  = "Animal"   -- 名前

    function obj:getName()   -- 名前を言う
        print(self.name)
    end

    function obj:doSomething()   -- なにかする
        return
    end

    return obj
end

function Uma()
    local obj = Animal()

    obj.name = "MayanoTopGun"   -- 名前の上書き

    function obj:doSomething()   -- 動作の上書き
        print("UmapyoiUmapyoi")
    end

    return obj
end

function Shika()
    local obj = Animal()

    obj.name = "ShikaBro"   -- 名前の上書き

    function obj:doSomething()   -- 動作の上書き
        print("SenronoTetsuwoNameru")
    end

    return obj
end

function action(a)
    a:getName()
    a:doSomething()
end

u = Uma()
s = Shika()

action(u)
action(s)
```

```cmd:コマンドプロンプト
c:\Lua>lua DuckTyping.lua
MayanoTopGun
UmapyoiUmapyoi
ShikaBro
SenronoTetsuwoNameru
```

同じ動作を命令されたにも関わらず、`Uma`オブジェクト`u`と`Shika`オブジェクト`s`が別々の動作をしたことに注目してください。

# DCS上で動くスクリプトを書こう

VSCODEでファイルを新規作成して次のように記述します。

```Lua:HelloWorld.lua
trigger.action.outText("Hello I'm Matt Wagner, Welcome to DCS World.", 10, false)
```

outTextはメッセージを画面の右上に第二引数で指定した秒数(今回は十秒)表示する関数です。第三引数は以前のメッセージを削除して表示するか否かを真偽値で決定します。

https://wiki.hoggitworld.com/view/DCS_func_outText

`HelloWorld.lua`という名前でファイルを保存して、`保存したゲーム\DCS.openbeta\Missions`に保存します。

DCS Worldを起動して、MISSION EDITORから新しいミッションを作成しましょう。作成したら左列に並んだスイッチ型のアイコンを押して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/3e1f9120-c827-f015-fb20-9ab656f44cb3.png)
トリガー編集画面を開きます。

TRIGGERS欄からNEWを押してTYPE:ONCEでトリガーを作成します。

作成したトリガーに対してCONDITIONS欄でNEWを押してTYPE:TIME MOREを選択し、SECONDS: 3に設定します。

作成したCONDITIONに対してACTIONS欄からNEWを選択し、ACTION: DO SCRIPT FILEからFILE→OPENボタンを押して、先ほど作成したHeloWorld.luaを読み込みます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/2ebcf681-3fbb-5ca9-1416-8ef5648acca8.png)

これはミッション開始三秒後にHelloWorld.luaをスクリプトとして実行するという設定になります。

ミッションを保存して開始すると、三秒後に画面の右上にメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/acc04dac-aa34-563c-7f7e-5bb316a29c65.png)

よし、これでスクリプトの書き方は完璧に理解したな！終了！

# イベントを感知して関数を実行する

これだけだと面白くないので、爆弾を落とすかミサイルを発射するなりしたときにメッセージを表示する仕組みを書いてみます。

```Lua:BasicSurfaceAttack.lua
function BasicSurfaceAttack()
    local obj = {}

    function obj:onEvent(event)
        if event.id == world.event.S_EVENT_SHOT then
            trigger.action.outText("Hello I'm Matt Wagner, Welcome to DCS World.", 10, false)
        end
    end

    return obj
end

bsa = BasicSurfaceAttack()

world.addEventHandler(bsa)
```

`BasicSurfaceAttack`クラスは`onEvent`メソッドを持っています。

`world.addEventHandler`関数に引数として自作したクラスのインスタンス変数を渡すと、DCS World上でミサイルの発射やだれかの撃墜など特定のイベントが発生したときに、追加されたインスタンスの`onEvent`メソッドを実行し、イベント情報を第一引数として渡します。`onEvent`メソッドの`event`引数がそのイベント情報に当たります。

この辺で何言ってるかわからなくなってきた人は**オブジェクト指向プログラミング**とか勉強してね。

簡単に説明するとBasicSurfaceAttack関数はOnEventという名前のボタンが取り付けられたロボットです。
DCSにはイベントが発生したとき、渡されたロボットのOnEventボタンを押してイベントの情報をロボットに渡すという機能があります。
なのでロボットを自作してOnEventという名前のボタンを付けてあげれば、あとはそれをDCS側の機能に渡すだけでイベントの内容をロボットを通して取得できます。

んで、このとき渡される`event`変数はテーブルとしてイベントの情報を持っていまして、例として爆弾やミサイルの発射時に渡されるSHOTイベントの場合は次のようなテーブルが返ってきます。

```Lua:S_EVENT_SHOT
Event = {
  id        = 1,      -- イベントの種類
  time      = Time,   -- 武装が発射された時間
  initiator = Unit,   -- 何らかの武装を発射した母機のUnitインスタンス
  weapon    = Weapon  -- 発射された武装のWeaponインスタンス
}
```

イベントIDは列挙型として`world.event`で定義されています。
どんなイベントがDCS上で用意されているか確認したい場合は次を参照してください

https://wiki.hoggitworld.com/view/Category:Events

といわけで`BasicSurfaceAttack.lua`を先ほどの`HelloWorld.lua`の代わりに読み込んで、今度は爆装したF-16を発進させてみましょう。

### 爆装したＦ１６Ｊが３機、三沢を発進して南下中だ。

爆弾を投下した瞬間にメッセージが表示されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/718e7bb8-3e91-67a4-7a1a-cf7958fe926a.png)

## イベント情報を駆使してもうちょっと遊んでみる

STATIC OBJECTとして牛を置いてみます。
この牛からの着弾地点のズレを計算するスクリプトを書いてみましょう。
牛のNAMEを「DMPI」と設定しておきます。

DMPIとは"Desired Mean Point of Impact"の略で、まあ爆弾を落としたい場所という意味です。「ディンピー」って言うらしいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/7178087f-7ba4-e50a-fd3d-9cae1349c67a.png)

### ウシ娘 プリティーファーマー

この牛を爆撃したときに着弾地点と牛の位置のズレを計算するスクリプトです。
先に完成形を見せておきますね。

```Lua:BasicSurfaceAttack.lua
function Shot(event)
    local obj = {}

    obj.p = event.weapon:getPoint()       -- 爆弾の現在位置を記録 

    function obj:isExist()
        return event.weapon:isExist()     -- 爆弾がまだ存在するか確認
    end

    function obj:update()
        self.p = event.weapon:getPoint()   -- 爆弾の現在位置を更新
    end

    function obj:getPointTo(unit)
        local p1 = unit:getPoint()        -- 目標物の座標
        local p2 = self.p                 -- 現在の爆弾の座標
        return math.sqrt( (p1.x - p2.x)^2 + (p1.z - p2.z)^2 )      -- 二つの座標の距離を計算
    end

    return obj
end

function BasicSurfaceAttack()
    local obj = {}

    obj.shots = {}   -- 発射情報を記録するための変数

    function obj:onEvent(event)
        if event.id == world.event.S_EVENT_SHOT then    -- もしイベントが武装の発射だった場合
            self.shots[#self.shots + 1] = Shot(event)   -- 新しい発射情報を登録
        end
    end

    function obj:calc()
        for i,shot in pairs(self.shots) do
            if shot:isExist() then   -- 発射された武器がまだ存在しているかの確認、
                shot:update()            -- 発射された武器の現在位置の更新
            else                     -- 発射された武器が存在していなかったとしたら
                local u = StaticObject.getByName("DMPI")   -- 目標を表すインスタンスを取得
                local p = shot:getPointTo( u )             -- 最後に記録された目標までの距離を取得
                trigger.action.outText( "Bomb hit " .. string.format("%d", p) .. "meter from DMPI", 10, false )   -- 距離を表示
                table.remove(self.shots, i)   -- 消えた爆弾の発射情報を削除
            end
        end
        timer.scheduleFunction(self.calc, self, timer.getTime() + 1/360)   -- この関数を1/360秒後にもう一度実行する
    end

    return obj
end

bsa = BasicSurfaceAttack()   -- インスタンス生成

world.addEventHandler(bsa)   -- イベントハンドラにインスタンスを追加

bsa:calc()                   -- calcメソッドを開始
```

### 遠すぎた牛

こんな感じで牛と着弾地点との距離を画面右上にメッセージとして表示しています。
34mはちょっとヘタクソですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455839/349fbbb9-01a6-5620-9b04-83964bdabd93.png)

## スクリプト解説

### 全体の流れ

DCSでは何らかの目標に爆弾が命中したときのイベントは用意されていますが、適当な地形に爆弾が着弾したときのイベントが用意されていないので、爆弾の投下後毎フレーム座標を取得し続け、爆弾が消えた瞬間の直前に存在していたときの座標を着弾地点として、まあ多少の誤差は出るかもしれないが更新フレーム数が十分あれば大丈夫だろう。ということにします。

`BasicSurfaceAttack`クラスはイベントハンドラとして`OnEvent`をDCS側から実行されることで`event`情報を受け取り、新しい`Shot`オブジェクトを生成し、そのオブジェクトごとに`event`情報を受け渡します。`BasicSurfaceAttack`クラスは毎フレーム`calc`を実行し、その中で管理するすべての`Shot`オブジェクトにイベント情報として管理されている爆弾が現在も存在するか確認、存在が確認できた場合は`update`で現在の爆弾位置を取得する命令を、存在が確認できなかった場合は牛と直前の爆弾位置の距離を計算して表示し、その`Shot`オブジェクトを管理から外すという動作をします。

### Shotクラス

`Shot`クラスは投下された爆弾の現在地を記録し続けるクラスです。
最初に受け取ったイベント情報のテーブルに含まれる`Weapon`オブジェクトを参照して

1. **isExist**関数    : 爆弾がまだ存在するか確認
2. **update**関数     : 爆弾の現在位置の座標を更新
3. **getPointTo**関数 : 爆弾と目標との距離を計算

といったことが可能なメソッドを用意しています。

```Lua
function Shot(event)
    local obj = {}

    obj.p = event.weapon:getPoint()       -- 爆弾の現在位置を記録 

    function obj:isExist()
        return event.weapon:isExist()     -- 爆弾がまだ存在するか確認
    end

    function obj:update()
        self.p = event.weapon:getPoint()   -- 爆弾の現在位置を更新
    end

    function obj:getPointTo(unit)
        local p1 = unit:getPoint()        -- 目標物の座標
        local p2 = self.p                 -- 現在の爆弾の座標
        return math.sqrt( (p1.x - p2.x)^2 + (p1.z - p2.z)^2 )      -- 二つの座標の距離を計算
    end

    return obj
end
```

#### 存在の確認

Weaponオブジェクトは次の関数でまだその爆弾なりミサイルなりが存在しているか確認できます。

https://wiki.hoggitworld.com/view/DCS_func_isExist

#### 位置の確認

`getPoint`メソッドでDCS上の物体の座標が取得できます。

https://wiki.hoggitworld.com/view/DCS_func_getPoint

座標はVec3型で返ってきます。

```
Vec3 = {
  x = Distance in meter,   -- 緯度
  y = Distance in meter,   -- 高度 
  z = Distance in meter    -- 経度
}
```

### BasicSurfaceAttackクラス

`BasicSurfaceAttack`クラスはイベントを捕まえ、投下された爆弾ごとに座標を追跡するよう複数の`Shot`インスタンスを生成・管理し、爆弾が消えたという連絡をいずれかの`Shot`インスタンスから受け取ったとき、そのインスタンスを削除し、受け取った着弾地点を表示する役目を負っています。

1. **shots**変数 : Shotインスタンスを管理する
2. **onEvent**関数 : イベント情報を受け取る
3. **calc**関数 : 毎秒情報を更新し、着弾時にしかるべき処置を行う

以上の要素から成り立っています。

```Lua
function BasicSurfaceAttack()
    local obj = {}

    obj.shots = {}   -- 発射情報を記録するための変数

    function obj:onEvent(event)
        if event.id == world.event.S_EVENT_SHOT then    -- もしイベントが武装の発射だった場合
            self.shots[#self.shots + 1] = Shot(event)   -- 新しい発射情報を登録
        end
    end

    function obj:calc()
        for i,shot in pairs(self.shots) do
            if shot:isExist() then   -- 発射された武器がまだ存在しているかの確認、
                shot:update()            -- 発射された武器の現在位置の更新
            else                     -- 発射された武器が存在していなかったとしたら
                local u = StaticObject.getByName("DMPI")   -- 目標を表すインスタンスを取得
                local p = shot:getPointTo( u )             -- 最後に記録された目標までの距離を取得
                trigger.action.outText( "Bomb hit " .. string.format("%d", p) .. "meter from DMPI", 10, false )   -- 距離を表示
                table.remove(self.shots, i)   -- 消えた爆弾の発射情報を削除
            end
        end
        timer.scheduleFunction(self.calc, self, timer.getTime() + 1/360)   -- この関数を1/360秒後にもう一度実行する
    end

    return obj
end
```

#### 新しいShotオブジェクトを生成

`#変数名`はその変数の配列の長さを返します。
`変数[#変数名 + 1] = 値`で配列に新しい値を加えることが出来ます。

```Lua
self.shots[#self.shots + 1] = Shot(event)
```

#### 複数のShotオブジェクトを回して確認

これまじで便利ですね

```Lua
for i,shot in pairs(self.shots) do
    -- この中でshotはself.shots[i]と同じ意味になる
end
```

#### 牛情報の取得

DCSのミッションエディタ上でStaticObjectとして設置した物体の情報は`StaticObject.getByName(オブジェクト名)`でStaticObjectオブジェクトとして取得できます。

```Lua
local u = StaticObject.getByName("DMPI")
```

https://wiki.hoggitworld.com/view/DCS_Class_Static_Object

Unitとして設置した場合は`Unit.getByName(オブジェクト名)`でUnitオブジェクトとして同様に取得できます。

#### 無限に実行

`calc`関数は再帰的に自分自身を実行するので、1/360秒ごとに繰り返し計算されます。

```Lua
timer.scheduleFunction(self.calc, self, timer.getTime() + 1/360)
```

`timer.scheduleFunction`はDCS側で用意された任意の関数(第一引数)を第三引数で指定した秒数後に実行するよう予約する関数です。
`calc`関数から1/360秒後に`calc`関数を呼び出すことで、永遠に1/360秒ごとに`calc`が実行されます。

#### 配列から要素を削除

これだけで済むみたいです。便利～！

```Lua
table.remove(self.shots, i)
```

### 実行関数

実行関数では`BasicSurfaceAttack`クラスをインスタンス化してイベントハンドラに渡した後、`calc`メソッドを実行します。
`calc`メソッドは先ほど説明した通り毎秒360回繰り返し実行されることになります。

```lua
bsa = BasicSurfaceAttack()   -- インスタンス生成

world.addEventHandler(bsa)   -- イベントハンドラにインスタンスを追加

bsa:calc()                   -- calcメソッドを開始
```

# いかがでしたか

これだけだとどんな目標に爆弾を落としてもDMPI牛に投下された爆弾として検出されてしまうので、今度はevent情報に記された発射母機の侵入方位や距離からDMPI牛に投下された爆弾のみを検出して、パブリックサーバーなんかで運用しやすいようにしてみましょう。

次回までの課題ということで考えてみてください。

# あなたを必要としている

宣伝です

Discordでたまにいろいろゲーム遊んでます

招待リンク：

https://discord.com/invite/ADywu9B