Luaスキンの記述方法 - exch-bms2/beatoraja GitHub Wiki
概要
beatoraja 0.6 からスキンを Lua で記述することができます。この記事では大まかな仕様やおすすめの記述方法を説明します。
Lua スキンのメリット
CSV や JSON と異なり一般的なスクリプト言語なので、条件分岐、繰り返し、算術演算などを簡単に書いたり、類似する計算をまとめたりすることができます。これにより、スキンオプションによるパーツの変化などを全パターン書き下す必要がなくなり、データの記述と修正が楽になります。
現在(0.7.9)のところ、Lua スクリプトの実行タイミングはおもにスキン読み込み時です。そのため、表示する内容を実行中のスクリプト処理によって動的に変化させるようなことはできません。(後述のように一部実行中にスクリプトを動作させることでカスタマイズできる箇所もありますが、その影響範囲は限られています。)
仕様
スキンフォルダ内にある拡張子 .luaskin
のファイルが1個のスキンと見なされ、Lua スクリプトとして実行されます。実行結果(Lua としての戻り値)は JSON スキン と同じ構造を持つテーブルとして解釈されます。
例えば、次のスクリプトは最小限の Lua スキンの例です。(プレイスキンとして必要な情報が抜けているため、実際に使おうとするとエラーが出るでしょうが。)
-- example.luaskin
return {
type = 0, -- skin type: 7keys
name = "Skin Name",
w = 1280,
h = 720,
playstart = 1000,
scene = 3600000,
input = 500,
close = 1500,
fadeout = 1000,
property = {},
filepath = {},
offset = {}
}
デフォルトスキンに含まれる例
デフォルトスキンのうち、現在(0.6.3)24key SP プレイスキン(/skin/default/play24.luaskin
)のみ Lua で記述されています。
他のデフォルトスキンはすべて JSON 形式なので、JSON スキンの仕様がわからない場合は参考にしてください。特に、何を書いていいか全くわからない場合はシンプルな 決定画面 や リザルト画面 をおすすめします。
スキンデータの仕様・JSON と Lua テーブルの対応関係
スキンデータの構造については、 JsonSkin
クラス を参照してください。 JsonSkin
クラスや、内部にある Property
などのクラスに対応したデータを与えると、自動でデシリアライズされます。
各クラスの意味はこの記事では説明しきれませんが、LR2 スキン(CSV)との簡単な対応だけ説明しておきます。例えば、LR2 スキンでの画像表示
//SRC定義,(NULL),gr,x,y,w,h,div_x,div_y,cycle,timer,op1,op2,op3,,,,,,,
#SRC_IMAGE,0,0,432,408,32,13,1,1,0,0,0,0,0,,,,,,,
//DST定義,(NULL),time,x,y,w,h,acc,a,r,g,b,blend,filter,angle,center,loop,timer,op1,op2,op3
#DST_IMAGE,0,1600,555,435,32,13,0,0,255,255,255,2,0,0,0,2000,0,202,0,0
#DST_IMAGE,0,2000,555,435,32,13,0,255,255,255,255,2,0,,,,,,,
は、Lua では次のようになります。
return {
type = 0,
-- ...
image = {
-- ...
{ id = "example", src = 0, x = 432, y = 408, w = 32, h = 13 },
-- ...
},
-- ...
destination = {
-- ...
{ id = "example", loop = 2000, blend = 2, op = {202}, dst = {
{ time = 1600, x = 555, y = 435, w = 32, h = 13, a = 0, r = 255, g = 255, b = 255 },
{ time = 2000, a = 255 }
} },
-- ...
}
-- ...
}
#SRC_
系の命令は、その種類に応じたフィールド内(今回は IMAGE
なので image
フィールド)に、対応する #DST_
系命令は種類にかかわらず destination
フィールド内に書きます。また、これらに同じ id
を与えることで SRC と DST の対応関係を表します。(id
には文字列だけでなく数値を指定することもできます。)
これ以上の仕様については、デフォルトスキンや SkinProperty
などを参考にしてください。
最後に、JSON と Lua テーブルの対応は以下のようになっています。
- 真理値、数値、文字列はそのまま
- JSON の配列 ⇔ Lua の配列(数字が添え字のテーブル)
- JSON のオブジェクト ⇔ Lua のテーブル
Lua では配列とテーブルの区別が曖昧ですが、配列を書くべき場所かテーブルを書くべき場所かは決まっているので問題ありません。参考までに、前述の SRC/DST 定義の Lua に対応する JSON は次のようになります。作成済みの JSON スキンを Lua に変換する場合はほぼ機械的な変換で事足りるかと思います。
{
"type": 0,
"image": [
{ "id": "example", "src": 0, "x": 432, "y": 408, "w": 32, "h": 13 },
],
"destination": [
{ "id": "example", "loop": 2000, "blend": 2, "op": [202], "dst": [
{ "time": 1600, "x": 555, "y": 435, "w": 32, "h": 13, "a": 0, "r": 255, "g": 255, "b": 255 },
{ "time": 2000, "a": 255 }
]},
],
}
スキン設定へのアクセス
スキン設定(カスタムオプション、カスタムファイル、カスタムオフセット)にはグローバル変数の skin_config
でアクセスできます。変数の中身については SkinLuaAccessor
を参照してください。
ただし、Lua スキンのスクリプトは読み込み時に「ヘッダ読み込み」と「本体読み込み」の2回実行され、前者では skin_config
変数が nil
となることに注意してください。「ヘッダ読み込み」はスキン名などの基本的な情報と、どのようなスキンオプションが存在するかのデータを取得するための手順です。「本体読み込み」は、ユーザーが設定したスキンオプションに基づいてスキンの実体を取得するための手順です。「ヘッダ読み込み」ではスキン設定にアクセスできない代わりに destination
などの実際に画面に表示するデータの部分が必要ないので、それらの部分は空白にして構いません。
おすすめの記述方法
最初に示した例のように .luaskin
にデータをべた書きしてしまうと Lua のメリットが活かせません。また、テキストエディタの構文ハイライトを利用するために、大部分のデータは .lua
ファイルに記述したいところです。スキン設定に柔軟に対応するために、以下のテンプレートを元に記述することをおすすめします。
-- play7.luaskin
local t = require("play7main")
if skin_config then
return t.main()
else
return t.header
end
-- play7main.lua
local header = {
type = 0,
name = "Example (7key)",
w = 1280,
h = 720,
playstart = 1000,
scene = 3600000,
input = 500,
close = 1500,
fadeout = 1000,
property = {
-- カスタムオプション
-- {name = "Judge Detail", item = {
-- {name = "Off", op = 910},
-- {name = "EARLY/LATE", op = 911},
-- {name = "+-ms", op = 912}
-- }}
},
filepath = {
-- カスタムファイル
-- {name = "Background", path = "background/*.png"}
},
offset = {
-- カスタムオフセット
}
}
local function main()
-- ヘッダ情報をスキン本体にコピーします
local skin = {}
for k, v in pairs(header) do
skin[k] = v
end
-- 以下でスキン本体のデータを定義します
skin.source = {
-- {id = 1, path = "background/*.png"},
}
skin.font = {
-- {id = 0, path = "VL-Gothic-Regular.ttf"}
}
skin.image = {
-- {id = "background", src = 1, x = 0, y = 0, w = 1280, h = 720},
}
skin.imageset = {
}
skin.value = {
-- {id = 400, src = 5, x = 0, y = 0, w = 240, h = 24, divx = 10, digit = 4, ref = 91},
}
skin.text = {
-- {id = 1000, font = 0, size = 24, align = 0, ref = 12}
}
skin.slider = {
-- {id = 1050, src = 0, x = 0, y = 289, w = 14, h = 20, angle = 2, range = 520,type = 6}
}
skin.hiddenCover = {
}
skin.graph = {
}
skin.note = {
}
skin.gauge = {
}
skin.judge = {
}
skin.bga = {
id = "bga"
}
skin.destination = {
-- {id = "background", dst = { {x = 0, y = 0, w = 1280, h = 720} }},
}
return skin
end
return {
header = header,
main = main
}
play7.luaskin
の if skin_config then
の部分で「本体読み込み」か「ヘッダ読み込み」かを判別しています。play7main.lua
では、ヘッダ(header
)と本体部分のデータを作成する関数(main
)を返しています。main
をわざわざ関数にした理由は、「ヘッダ読み込み」時に skin_config
が使えなくてエラーになるのを防ぐとともに、スキン設定画面で余分な処理を行わず高速化するためです。(スキン設定画面ではすべてのスキンのヘッダのみ読み込むため)
また、異なるスキンの共通部分をくくりだす(例:SPとDPの部品の共通化など)には、適宜ファイルを分けて require
で読み込むと良いでしょう。
Lua プログラムの応用例
【初級】変数・分岐
-- ヘッダーに次のようなプレイオプション(property)が入っているとする
-- {name = "Play Side", item = {
-- {name = "1P", op = 920},
-- {name = "2P", op = 921},
-- }}
local play_side
if skin_config.option["Play Side"] == 920 then
play_side = 1
else
play_side = 2
end
-- プレイサイドに応じてレーンの位置を設定する
local lane_origin_x
local lane_origin_y = 140
if play_side == 1 then
lane_origin_x = 20
else
lane_origin_x = 870
end
skin.destination = {
-- ...
-- 先ほど代入した変数を使ってレーン背景を所定の座標に配置する
{id = "lane-bg", loop = 1000, dst = {
{time = 0, x = lane_origin_x, y = lane_origin_y, w = 390, h = 0, a = 0},
{time = 1000, h = 580, a = 255}
}},
-- ...
}
【中級】テーブル操作・繰り返し・関数
レーンの位置をレーン幅に基づいて計算したり、レーンごとのオブジェクト定義をループでまとめて書いたりすることができます。
-- 指定されたレーン番号に対するキーオンタイマーIDを返す関数(7鍵モード専用)
local function timer_key_on(lane)
if lane == 8 then
-- 皿
return 100
else
-- 鍵盤 (lane=1~7)
return 100 + lane
end
end
-- 白鍵・黒鍵・皿のキービーム画像
skin.image = {
-- ...
{id = "keybeam-w", src = 6, x = 48, y = 0, w = 27, h = 255},
{id = "keybeam-b", src = 6, x = 76, y = 0, w = 20, h = 255},
{id = "keybeam-s", src = 6, x = 0, y = 0, w = 47, h = 255},
-- ...
}
-- 白鍵・黒鍵・皿のレーン幅
local lane_width = {
w = 50,
b = 40,
s = 70,
}
-- レーン(スキンでは1~7鍵盤→皿の順番)ごとにレーンの種類(白・黒・皿)を設定する
local lane_types = { "w", "b", "w", "b", "w", "b", "w", "s" }
-- レーンの順番(左からi番目に表示するレーンの番号、左皿の場合)
local lane_order = { 8, 1, 2, 3, 4, 5, 6, 7 }
-- 各レーンのX座標を計算する
local lane_origin_x = 20
local lane_origin_y = 140
local lane_x = {}
do
local x = lane_origin_x
-- 配列の添え字が1から始まることに注意
for i, k in ipairs(lane_order) do
-- i: 左から何番目か(1~8)、k: レーン番号
lane_x[k] = x
-- 上で定義した種類ごとのレーン幅から取得する
local width = lane_width[lane_types[k]]
x = x + width
end
end
skin.destination = {
-- ...
}
-- 各レーンのキービームを配置する
for i, k in ipairs(lane_order) do
-- 文字列の連結は .. 演算子で行う
local image_id = "keybeam-" .. lane_types[k]
-- 既存のテーブル(配列)に要素を追加するには table.insert 関数を使う
table.insert(skin.destination, {
id = image_id,
timer = timer_key_on(k),
loop = 0,
dst = {
{ time = 0, x = lane_x[k], y = lane_origin_y, w = lane_width[lane_types[k]], h = 580 },
}
})
end
【上級】カスタムファイルでスクリプトを選択できるようにする
例えばプレイスキンのフレームパーツをカスタムファイルとして選択可能にする際、単に画像を変更するのではなく、画像ごとに組み立てアニメーションなどをスクリプトで記述できるようにしたいことがあると思います。そのような場合、次のようにフォルダをカスタムファイルのパスとして指定すると良いでしょう。
ディレクトリ構造:
/ スキンファイルの置かれたディレクトリ
- play.luaskin
/ frame
/ Default
- frame.png
- skin.lua
/ Custom1
- frame.png
- skin.lua
スキン:
-- ヘッダ部分
skin.filepath = {
-- ...
{ name = "Frame", path = "frame/*", def = "Default" }
}
-- 以下、本体部分
skin.source = {
-- ...
{ id = 1, path = "frame/*/frame.png" }, -- 画像ファイルは途中に * を含んだパスを指定できる
}
-- skin_config.get_path 関数を使うと、* を含んだ(オプション依存の)パスを解決してくれる
local lua_path = skin_config.get_path("frame/*") .. "/skin.lua"
-- pcall 関数を使い、エラーが起きても止まらないようにする
-- (カスタム部分はスキンのユーザーが編集することを想定してのエラー処理ですが、
-- もちろん必須ではありません)
local frame_parts_status, frame_parts = pcall(function()
-- 指定されたパスのスクリプトを実行するには dofile 関数を使う
-- (require はディレクトリがドット区切りなので今回は使えない)
return dofile(lua_path).load(1)
end)
-- 読み込みに成功した場合
if frame_parts_status and frame_parts then
-- 実行結果の image, destination をスキン本体にマージする
for _, v in ipairs(frame_parts.image) do
table.insert(skin.image, v)
end
for _, v in ipairs(frame_parts.destination) do
table.insert(skin.destination, v)
end
end
カスタム部分のスクリプト(frame/*/skin.lua
):
local function load(source_id)
local parts = {}
parts.image = {
{id = "image1", src = source_id, x = 0, y = 0, w = 1280, h = 360},
}
parts.destination = {
{id = "image1", dst = {
{x = 0, y = 0, w = 1280, h = 360},
}},
}
return parts
end
return {
load = load
}
その他
利用上の注意
Lua からはファイル入出力その他の OS 機能が利用可能なため、怪しい入手先のスキンを使うのは危険です。信頼できるスキンのみ、自己責任で使いましょう。
スキン作成側も、危険な処理を書かないのはもちろんのこと、スクリプトに間違いがあるとアプリ全体が落ちてしまうので十分にデバッグしましょう。
デバッグ方法
TODO
スキン実行中に使える Lua
数値系の SRC で参照値にLuaの関数や文字列を与えると、実行時に関数または文字列をLuaスクリプトとして解釈したものを実行してくれます。この中でスコアなどの情報を変数で参照することができます。詳しくは スキン中のランタイムLuaスクリプトの記述方法 をご覧ください。