スキン中のランタイムLuaスクリプトの記述方法 - exch-bms2/beatoraja GitHub Wiki
Luaスキンの記述方法 で説明しているようにLuaスキンのスクリプト全体はロード時に実行されますが、画面に表示する数値を指定するところやオブジェクトの表示条件を指定するところにLuaの関数を書くことができ、スキン動作時に評価させることができます。これにより、プレイ状況に応じて柔軟に表示を変えることができます。
できることとできないこと
バージョン0.7.9現在、以下の要素をLuaスクリプトに置き換えることができます。各要素がスキンのどの部分に出現するかはソースコードの JsonSkinクラス を参照してください。
型 | 従来の指定内容 | 書けるLuaスクリプト | 主な使い道 |
---|---|---|---|
BooleanProperty |
オプションID(OPTION_* ) |
真偽値を返す関数 | 表示条件の制御 |
IntegerProperty |
数値ID(NUMBER_* ) |
整数値を返す関数 | 表示する数値の制御 |
FloatProperty |
スライダーID(SLIDER_* )または棒グラフID(BARGRAPH_* ) |
数値を返す関数 | スライダーや棒グラフの制御 |
StringProperty |
文字列ID(STRING_* ) |
文字列を返す関数 | 表示する文字列の制御 |
TimerProperty |
タイマーID(TIMER_* ) ※1 |
タイマー時刻を返す関数 | アニメーションの進み方や表示条件の制御 |
FloatWriter |
スライダーID(SLIDER_* ) |
数値を受け取る関数 | スライダーを操作したときの挙動の制御 |
Event |
ボタンID(BUTTON_* )またはカスタムイベントID ※2 |
関数 | 画像パーツをクリックしたときの処理の制御 |
※1: このページで説明するカスタムタイマーを指定可能。
※2: カスタムイベントはカスタムタイマーと同様ランタイムLuaの利用が前提となります。
ランタイムLuaスクリプトでできることとできないことは大まかに以下のようになります。
できること
- 各オブジェクトの表示条件をスクリプトで柔軟に制御する
- 表示する数値、グラフの値、文字列などをスクリプトで柔軟に制御する
- オブジェクトをクリックしたときにスクリプトを実行し、上記の表示条件制御や表示内容制御と連動させる
できないこと
- オブジェクトの座標や色をスクリプトで柔軟に制御する
- 直接はできませんが、今後対応予定です。
- 大量の
Destination
を用意しておき、それらの表示条件やタイマーを制御することで間接的に行うことは不可能ではありません。
- スクリプトから動的に
Destination
を生成する- 実装コスト、処理コストともに高いため。
- 固定数用意しておき使い回すことで疑似的には対応可能です。
- GUIを簡単に作成する
- ボタンクリック時の動作など自由度は広がりますが、レイアウトの仕組みを支援する機能などは特にありません。
- 新種のスキンオブジェクトを作成したり、画面以外のバッファに描画したりする
- スキン描画の仕組み自体はカスタマイズできません。
- バッファへの描画など面白い需要があればLuaスキンとは関係なく対応しても良いかもしれません。
- 画面遷移の流れをカスタマイズしたり、譜面内容を変更したりする
- ゲームシステムへの干渉はできません。
基本の記述方法
最も基本的な、オプションIDの代わりに関数を書いて表示条件をカスタマイズする例を紹介します。0.7.9現在の Destination
クラスの定義は以下のようになっているため、 draw
フィールドに「オプションIDを表す整数値」と「真偽値を返すLuaの関数」のどちらも書くことができます。
public static class Destination {
public String id;
public int blend;
public int filter;
public TimerProperty timer;
public int loop;
public int center;
public int offset;
public int[] offsets = new int[0];
public int stretch = -1;
public int[] op = new int[0];
public BooleanProperty draw;
public Animation[] dst = new Animation[0];
public Rect mouseRect;
}
オプションIDの少し複雑な事情として、従来オプションIDを指定していたのは draw
ではなく op
でした。 op
には複数のオプションIDを入れることができますが、諸々の事情からLuaの関数を入れることはできません。 draw
と op
で役割が被っているわけですが、両方指定した場合は draw
が優先されます。
例えば draw
にオプションIDを入れる書き方と、関数を入れる書き方はそれぞれ以下のようになります。
-- オプションID 80(ロード中のみ表示される)
{id = "load-progress", loop = 0, draw = 80, dst = {
{time = 0, x = 40, y = 440, w = 200, h = 4},
{time = 500, a = 192, r = 0},
{time = 1000, a = 128, r = 255, g = 0},
{time = 1500, a = 192, g = 255, b = 0},
{time = 2000, a = 255, b = 255}
}},
-- 常に true を返す関数(常に表示されるので無意味)
{id = "load-progress", loop = 0,
draw = function()
return true
end,
dst = {
{time = 0, x = 40, y = 440, w = 200, h = 4},
{time = 500, a = 192, r = 0},
{time = 1000, a = 128, r = 255, g = 0},
{time = 1500, a = 192, g = 255, b = 0},
{time = 2000, a = 255, b = 255}
}},
BooleanProperty
である draw
に関数を入れた場合、その関数は毎フレーム評価されて返り値によって表示状態が決まります。したがって、この関数の中でゲーム状態を参照して判定することで、例えば「ゲージが80%以上のときのみ表示される」のような挙動を実現できます。
ゲーム状態の参照
動作時にLuaスクリプトからゲーム状態を参照するには、beatoraja 本体から提供される main_state
というモジュールを使います。まず、スキン定義の最初のほうに
local main_state = require("main_state")
と書きます。モジュールのロード結果は様々な関数や値が入ったテーブルとなっており、これが main_state
変数に代入されます。あとは、動作時に実行される関数からこのテーブル内の関数を呼び出すことでゲーム状態を取得できます。例えば、ゲージが80%以上のときのみ表示されるオブジェクトは次のように書くことができます。
{
id = "background",
draw = function()
return main_state.gauge() >= 80
end,
dst = {{x=0, y=0, w=1280, h=720}}
},
当然ですが、main_state
内の関数は動作時に呼び出される部分から使用してください。スキン読み込み時に main_state
内の関数を呼び出してもあまり役に立つ結果は得られないと思います。また、スキンヘッダ読み込み時は main_state
モジュールの読み込み結果そのものが空っぽになります。(スキンヘッダ読み込み時は skin_config
が使えないのと同様です。詳しくは Luaスキンの記述方法#スキン設定へのアクセス を参照してください。)
main_state
モジュールで提供される関数などの一覧はソースコードの MainStateAccessor で確認することができます。以下で代表的なものを説明します。
ID指定でプロパティの値を取得する関数
main_state.option(id)
: オプションIDid
の値(オンかどうか)main_state.number(id)
: 数値IDid
の値main_state.float_number(id)
: スライダーまたはグラフIDid
の値main_state.text(id)
: 文字列IDid
の値main_state.offset(id)
: オフセットIDid
の値- 結果は
{ x = x, y = y, w = w, h = h, r = r, a = a }
形式のテーブルです
- 結果は
main_state.timer(id)
: タイマーIDid
の値(マイクロ秒単位)- タイマーがオフの場合に返ってくる値はJavaの
Long.MIN_VALUE
となりますが、これはmain_state.timer_off_value
(関数ではありません)で与えられています。つまり、タイマーがオンかどうかは返り値をmain_state.timer_off_value
と比較すればわかります。
- タイマーがオフの場合に返ってくる値はJavaの
main_state.event_index(id)
: イベントIDid
の値(イベントの現在の状態を表すインデックス番号)(0.8.4以降)
特定の値を直接取得する関数
類似のプロパティは適宜省略するので MainStateAccessor で確認してください。
main_state.time()
: 現在時刻(ゲーム状態が遷移してからの経過時間)をマイクロ秒単位で取得します。main_state.rate()
: 現在のスコアレートを取得します。main_state.exscore()
: 現在のEXスコアを取得します。main_state.volume_sys()
: 現在のシステム音量を取得します。main_state.judge(judge)
: 判定judge
の現在のカウントを取得します。judge
の値: 0:PG, 1:GR, 2:GD, 3:BD, 4:PR, 5:MS
main_state.gauge()
: 現在のゲージの値を取得します。main_state.gauge_type()
: ゲージの種類を取得します。- 0:ASSIST EASY, 1:EASY, 2:NORMAL, 3:HARD, 4:EX-HARD, 5:HAZARD, 6:GRADE, 7:EX GRADE, 8:EXHARD GRADE
各プロパティの仕様
BooleanProperty
引数なしで真偽値を返す関数を書くことができます。例:
function()
-- ゲージが80%以上かどうか
return main_state.gauge() >= 80
end
Destination
のdraw
フィールドに記述すると毎フレーム評価されて結果がtrue
のときのみ表示されます。同じ役割を持つop
フィールドはオプションID専用です。CustomEvent
のcondition
フィールドに記述するとイベントを実行しようとしているときに評価されて結果がtrue
のときのみイベントが実行されます。
IntegerProperty
引数なしで整数値(Luaには実数型との区別がないので実数値でもOK)を返す関数を書くことができます。例:
function()
-- ゲージの10の位(//は切り捨て除算)
return main_state.gauge() // 10
end
ImageSet
のvalue
フィールドに記述すると毎フレーム評価されて結果が何番目の画像が表示されるかに使われます。同じ役割を持つref
フィールドは数値ID専用です。Value
のvalue
フィールドに記述すると毎フレーム評価されて結果の数値が表示されます。同じ役割を持つref
フィールドは数値ID専用です。
FloatProperty
引数なしで実数値を返す関数を書くことができます。例:
function()
-- ゲージの割合(最大100の場合)
return main_state.gauge() / 100
end
Slider
のvalue
フィールドに記述すると毎フレーム評価されて結果がスライダーの位置に反映されます。- スライダーを操作したときの動作
FloatWriter event
も同時に設定することになると思います。 type
フィールドにスライダーIDを指定するとvalue
とevent
の両方指定したのと同じ結果になります。
- スライダーを操作したときの動作
Graph
のvalue
フィールドに記述すると毎フレーム評価されて結果が棒グラフの長さ(割合)に反映されます。同じ役割を持つtype
フィールドはグラフID専用です。
StringProperty
引数なしで文字列を返す関数を書くことができます。例:
function()
return "Hello"
end
Text
のvalue
フィールドに記述すると毎フレーム評価されて結果の文字列が表示されます。同じ役割を持つref
フィールドは文字列ID専用です。
TimerProperty
引数なしで整数値を返す関数を書くことができます。結果が Long.MIN_VALUE
すなわち main_state.timer_off_value
の場合はタイマーがオフであることを、正の数のときはタイマーがオンになった時刻を表します。例:
function()
-- KEY1 と KEY2 の両方がオンのときオンになるタイマー
local key1 = main_state.timer(101)
local key2 = main_state.timer(102)
if key1 == main_state.timer_off_value or key2 == main_state.timer_off_value then
return main_state.timer_off_value
elseif key1 < key2 then
return key2
else
return key1
end
end
Image
,Value
,Slider
,Graph
,HiddenCover
,LiftCover
のtimer
フィールドに記述すると毎フレーム評価されて結果が分割アニメーションに用いられます。Destination
のtimer
フィールドに記述すると毎フレーム評価されて結果がdst
のアニメーションに用いられます。CustomTimer
のtimer
フィールドに記述するとカスタムタイマーの挙動として用いられ、毎フレーム評価されて結果がタイマーの値となります。
発展的な例として、オプション値を常に観測してオンになったらタイマーもオン、オフになったらタイマーもオフにするというのが次のように実装できます。(これは補助関数として timer_util
モジュールで提供されているので、自分で作る必要はありません。)
(function()
-- クロージャに保持する内部状態
local state = main_state.timer_off_value
return function()
-- ここからが実際に毎フレーム実行される
local on = main_state.option(241)
if on and state == main_state.timer_off_value then
-- オフからオンに変化
state = main_state.time()
elseif not on and state ~= main_state.timer_off_value then
-- オンからオフに変化
state = main_state.timer_off_value
end
return state
end
end)()
FloatWriter
実数値を受け取る関数を書くことができます。
Slider
のevent
フィールドに記述するとスライダーを動かしたときにスライダーの位置を引数として呼び出されます。普通のスライダーとして動作させるにはvalue
と同時に設定することになります。
Event
引数なしの関数を書くことができます。
Image
,ImageSet
のact
フィールドに記述するとオブジェクトをクリックしたときに呼び出されます。CustomEvent
のaction
フィールドに記述するとカスタムイベント実行時に呼び出されます。
ここまでの応用例
クリックした回数を表示する
各プロパティにLuaスクリプトを記述できるということは、単に表示条件や値の制御を柔軟に行えるだけでなく、変数を介してそれぞれの挙動を連動させることができることを意味します。わかりやすい例として、ボタンクリック時の挙動として変数の値を増加させ、一方でその変数の値を表示するというものを考えてみます。
実装例は以下のようになります。Destination 側は省略していますが button
と click_count
に対応したものを作ってください。
local click_count = 0
skin.image = {
...
{
id = "button", src = 1, x = 0, y = 0, w = 100, h = 100, act = function()
click_count = click_count + 1
end
},
...
}
skin.value = {
...
{
id = "click_count", src = 2, x = 0, y = 0, w = 240, h = 24, divx = 10, digit = 4, value = function()
return click_count
end
},
...
}
click_count
変数が2つの関数から見える位置に宣言されるように注意してください。Luaでは変数がどれを指しているのかが構文的に決定されます(参考: 可視性ルール )。内側から外側のスコープに向かって順番にローカル変数宣言を探していき、見つからなければグローバル変数として扱われてしまいます。ボタン側ではローカル変数だけど数値表示側ではグローバル変数なんてことになると最悪にわかりにくいバグとなってしまうので気を付けましょう。次のような書き方にするとミスが起こりにくいかもしれません。(何をやっているのかわからなければ無視してください。)
local function make_counter()
local click_count = 0
return {
increment = function()
click_count = click_count + 1
end,
get = function()
return click_count
end
}
end
local counter_1 = make_counter()
skin.image = {
...
{
id = "button", src = 1, x = 0, y = 0, w = 100, h = 100, act = counter_1.increment
},
...
}
skin.value = {
...
{
id = "click_count", src = 2, x = 0, y = 0, w = 240, h = 24, divx = 10, digit = 4, value = counter_1.get
},
...
}
ハーフレーンのプレイスキンで重なったキービームが2重に描画されないようにする
マニアックなネタですが、5鍵や24鍵のハーフレーンのスキンで隣接した鍵盤のキービームの重なった部分が2重に描画されないようにするにはどうすればいいだろうと考えたことがある方もいるのではないでしょうか。
キービームオブジェクトを「1鍵が占有する部分」「1鍵と2鍵が重なる部分」「2鍵が占有する部分」「2鍵と3鍵が重なる部分」…と分割していき、重なる部分のタイマーをスクリプトで定義すればこれを実現できます。
1鍵と2鍵が重なる部分のキーONタイマー関数は次のようになります。2つのタイマーのうち早い方のタイマー値が採用され、両方OFFのときのみ結果がOFFになります。(キービームをアニメーションさせない場合はON/OFFだけ区別できれば大丈夫ですが)
function()
local key1 = main_state.timer(101)
local key2 = main_state.timer(102)
if key1 == main_state.timer_off_value then
if key2 == main_state.timer_off_value then
return main_state.timer_off_value
else
return key2
end
else
if key2 == main_state.timer_off_value then
return key1
elseif key1 < key2 then
return key1
else
return key2
end
end
end
キーOFFタイマーはキーが離されたときにセットされるタイマーです。1鍵と2鍵が重なる部分は次のように、両方離されたとき初めて重なった部分のタイマーがONになるようにします。
function()
local key1 = main_state.timer(121)
local key2 = main_state.timer(122)
if key1 == main_state.timer_off_value or key2 == main_state.timer_off_value then
return main_state.timer_off_value
elseif key1 < key2 then
return key2
else
return key1
end
end
ボムを多重に再生する
ボムタイマーは鍵盤ごとに1個割り当てられているだけなので、普通に作ったのでは同一鍵盤で同時に1個しか再生することができません。豪華なボムを作ろうとしても頻繁に時刻がリセットされてしまうのでいまいちな見た目になってしまい、諦めた方も多いのではないでしょうか。
やはりタイマーのスクリプト制御によってボムを多重に再生することができるようになります。やり方は色々考えられますが、ここでは鍵盤ごとに最大N個のボムを再生する仕組みを作ります。
次の関数 create_cascade_timers
では、タイマーID timer
を多重化した max_count
個のタイマー関数を作成します。current_value
変数には元のタイマーの前回の値が、current_index
には何番目のタイマーが次に更新されるかが格納されています。タイマーの値が更新されたとき、自身が更新対象(current_index == i
)なら自身の値を更新し current_index
を進めるというわけです。
local function create_cascade_timers(timer, max_count)
local current_value = main_state.timer_off_value
local current_index = 0;
local timers = {}
for i = 0, max_count - 1 do
local self_value = main_state.timer_off_value
table.insert(timers, function()
local value = main_state.timer(timer)
if value ~= current_value and current_index == i then
current_index = (current_index + 1) % max_count
current_value = value
self_value = value
end
return self_value
end)
end
return timers
end
使用例は次のようになります。Destination側だけでなく、Image側も多重化したタイマーを参照するように注意しましょう。
local bomb_position_x = {...} -- ボムのX座標
for i = 1, key_count do
local timers = create_cascade_timers(49 + i, 4)
for j = 1, 4 do
local id = "bomb_" .. i .. "_" .. j
table.insert(skin.image, {
id = id, timer = timers[j], cycle = 800,
src = 10, x = 0, y = 0, w = 1810, h = 192, divx = 10
})
table.insert(skin.destination, {
id = id, timer = timers[j], loop = -1,
blend = 2, dst = {
{ time = 0, x = bomb_position_x[i], y = 28, w = 180, h = 192 },
{ time = 800 }
}
})
end
end
カスタムタイマーとカスタムイベントの定義
自分でLuaスクリプトによって定義したタイマー(TimerProperty
)やイベント(Event
)は、もちろん timer
や act
フィールドに直接入れれば使えるのですが、既存のタイマーやイベントのようなIDを振ることができます。これをカスタムタイマーやカスタムイベントと呼びます。複数人でスキンを制作するときはタイマーの実装者がカスタムタイマーとしてIDを割り当て、他の作者にタイマーID参照で使ってもらうのがわかりやすいかもしれません。
カスタムタイマーやカスタムイベントを定義するには、スキンの customTimers
、customEvents
フィールドにデータを入れます。詳しくは JsonSkinクラス を参照してください。
カスタムタイマーでは、本来スクリプトを入れるべき timer
フィールドを空にすることで、「受動的なタイマー」を作成することができます。受動的なタイマーとは一体何かというと、自身の毎フレームの更新処理は行わず、外部からの操作(main_state.set_timer(id, value)
)によって値をセットすることだけができるというものです。ぶっちゃけただの変数といえますが、IDの振られたカスタムタイマーとしては役に立つ場面もあると思います。
カスタムIDとして割り当て可能な値の範囲については SkinPropertyクラス を参照してください。現在タイマーIDは 10000~19999 が、イベントIDは 1000~1999 が利用可能です。
スキンから利用可能な補助関数
本体からスキンに対して main_state
以外にも timer_util
と event_util
の2つのモジュールを公開しています。それぞれの内容については TimerUtilityクラス と EventUtilityクラス を参照してください。