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.luaskinif 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スクリプトの記述方法 をご覧ください。

Lua 関連の情報