8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-12

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

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

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

便利なエディタを用意しよう

せっかくプログラミングするんですからメモ帳よりはもうちょっとかっこいいものを入れてみましょう

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

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

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

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

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

image.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でファイルを新規作成して次のように記述します。

HelloWorld.lua
print("Hello World")

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

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

C:\Lua>Lua HelloWorld.lua
Hello World

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

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

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)
コマンドプロンプト
c:\Lua>lua DuckTyping.lua
MayanoTopGun
UmapyoiUmapyoi
ShikaBro
SenronoTetsuwoNameru

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

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

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

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

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

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

DCS Worldを起動して、MISSION EDITORから新しいミッションを作成しましょう。作成したら左列に並んだスイッチ型のアイコンを押して
image.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

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

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

image.png

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

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

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

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イベントの場合は次のようなテーブルが返ってきます。

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

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

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

爆装したF16Jが3機、三沢を発進して南下中だ。

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

image.png

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

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

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

image.png

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

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

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

スクリプト解説

全体の流れ

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

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

Shotクラス

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

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

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

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

位置の確認

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

座標は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関数 : 毎秒情報を更新し、着弾時にしかるべき処置を行う

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

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] = 値で配列に新しい値を加えることが出来ます。

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

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

これまじで便利ですね

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

牛情報の取得

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

local u = StaticObject.getByName("DMPI")

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

無限に実行

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

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

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

配列から要素を削除

これだけで済むみたいです。便利~!

table.remove(self.shots, i)

実行関数

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

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

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

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

いかがでしたか

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

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

あなたを必要としている

宣伝です

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

招待リンク:

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?