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
となるようにしてください。
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でファイルを新規作成して次のように記述します。
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
概念が、名前と「何らかの動作」を自分達固有のものに上書きしています。
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
オブジェクトu
とShika
オブジェクトs
が別々の動作をしたことに注目してください。
DCS上で動くスクリプトを書こう
VSCODEでファイルを新規作成して次のように記述します。
trigger.action.outText("Hello I'm Matt Wagner, Welcome to DCS World.", 10, false)
outTextはメッセージを画面の右上に第二引数で指定した秒数(今回は十秒)表示する関数です。第三引数は以前のメッセージを削除して表示するか否かを真偽値で決定します。
HelloWorld.lua
という名前でファイルを保存して、保存したゲーム\DCS.openbeta\Missions
に保存します。
DCS Worldを起動して、MISSION EDITORから新しいミッションを作成しましょう。作成したら左列に並んだスイッチ型のアイコンを押して
トリガー編集画面を開きます。
TRIGGERS欄からNEWを押してTYPE:ONCEでトリガーを作成します。
作成したトリガーに対してCONDITIONS欄でNEWを押してTYPE:TIME MOREを選択し、SECONDS: 3に設定します。
作成したCONDITIONに対してACTIONS欄からNEWを選択し、ACTION: DO SCRIPT FILEからFILE→OPENボタンを押して、先ほど作成したHeloWorld.luaを読み込みます。
これはミッション開始三秒後にHelloWorld.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イベントの場合は次のようなテーブルが返ってきます。
Event = {
id = 1, -- イベントの種類
time = Time, -- 武装が発射された時間
initiator = Unit, -- 何らかの武装を発射した母機のUnitインスタンス
weapon = Weapon -- 発射された武装のWeaponインスタンス
}
イベントIDは列挙型としてworld.event
で定義されています。
どんなイベントがDCS上で用意されているか確認したい場合は次を参照してください
といわけでBasicSurfaceAttack.lua
を先ほどのHelloWorld.lua
の代わりに読み込んで、今度は爆装したF-16を発進させてみましょう。
爆装したF16Jが3機、三沢を発進して南下中だ。
爆弾を投下した瞬間にメッセージが表示されるようになりました。
イベント情報を駆使してもうちょっと遊んでみる
STATIC OBJECTとして牛を置いてみます。
この牛からの着弾地点のズレを計算するスクリプトを書いてみましょう。
牛のNAMEを「DMPI」と設定しておきます。
DMPIとは"Desired Mean Point of Impact"の略で、まあ爆弾を落としたい場所という意味です。「ディンピー」って言うらしいです。
ウシ娘 プリティーファーマー
この牛を爆撃したときに着弾地点と牛の位置のズレを計算するスクリプトです。
先に完成形を見せておきますね。
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はちょっとヘタクソですね。
スクリプト解説
全体の流れ
DCSでは何らかの目標に爆弾が命中したときのイベントは用意されていますが、適当な地形に爆弾が着弾したときのイベントが用意されていないので、爆弾の投下後毎フレーム座標を取得し続け、爆弾が消えた瞬間の直前に存在していたときの座標を着弾地点として、まあ多少の誤差は出るかもしれないが更新フレーム数が十分あれば大丈夫だろう。ということにします。
BasicSurfaceAttack
クラスはイベントハンドラとしてOnEvent
をDCS側から実行されることでevent
情報を受け取り、新しいShot
オブジェクトを生成し、そのオブジェクトごとにevent
情報を受け渡します。BasicSurfaceAttack
クラスは毎フレームcalc
を実行し、その中で管理するすべてのShot
オブジェクトにイベント情報として管理されている爆弾が現在も存在するか確認、存在が確認できた場合はupdate
で現在の爆弾位置を取得する命令を、存在が確認できなかった場合は牛と直前の爆弾位置の距離を計算して表示し、そのShot
オブジェクトを管理から外すという動作をします。
Shotクラス
Shot
クラスは投下された爆弾の現在地を記録し続けるクラスです。
最初に受け取ったイベント情報のテーブルに含まれるWeapon
オブジェクトを参照して
- isExist関数 : 爆弾がまだ存在するか確認
- update関数 : 爆弾の現在位置の座標を更新
- 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
インスタンスから受け取ったとき、そのインスタンスを削除し、受け取った着弾地点を表示する役目を負っています。
- shots変数 : Shotインスタンスを管理する
- onEvent関数 : イベント情報を受け取る
- 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でたまにいろいろゲーム遊んでます
招待リンク: