AviUtlでフレームで初期化されない変数が欲しい
はじめに
AviUtlにおける一般のスクリプトでは変数はフレームが進むごとに都度 初期化されます。
例えば、オブジェクトのx座標を時間と共に大きくしたい場合、フレームごとにx座標用の変数に固定値を足し合わせるという次のようなスクリプトが思いつくかもしれません。
code:script1.lua
s = 0
s = s + 100
obj.draw(s)
しかしこのようなスクリプトを書くと、フレームごとに変数sがs=0として初期化されてしまいsは不変、オブジェクトは動きません。
上の例ではs = obj.frame * 100とするなどの対処法が考えられます。しかしながら、全てのスクリプトでそのような書き換えが出来るとは限らず、どうしてもフレームを読み込んでも変数を 初期化したくないときがあります。特に、以前のフレームにおける画面や音の情報を用いて何かしらの処理をする場合は必須となります。このような処理の例には下図のような音量メータの最大値を表すオブジェクトなどで見られます。
https://gyazo.com/c391a6cb789234633e2cc108018f6d26
白い正方形は初期化したくない変数を使って動いている。(灰色の長方形はデフォルトの音声波形)
この処理では
正方形の高さを超える音量が何フレーム前に来たか
そのときの音量はいくつだったか
という情報を前フレームから受け継いで使用しています。そのため、この情報を格納する変数は初期化したくありません。(初期化するとそのような情報がすべて消えるため)
このように、フレームが進んでも初期化されない変数が処理によっては必要になることがあります。そこで今回はそんなフレームが進んでも初期化されない方法を述べます。もう既に誰かに書かれていそうな気もしますが、個人の備忘録用として書いていきます。
AviUtlのグローバル変数の範囲
そもそもの話として、スクリプトで定義した変数がローカルなのかグローバルなのか、変数のスコープはどこまであるのかを把握しておいた方が仕組みを理解するうえで楽になりますので説明します。
まず、スクリプトの言語であるLuaでは基本的に変数はグローバル変数となります。これをローカル変数にするには変数の宣言時にlocalを付け加える必要があります。
code:script2.lua
a = 10 --グローバル変数
local b = 20 --ローカル変数
スクリプト内で変数がローカル変数として宣言されると、その変数はそのフレームでの同じオブジェクト内でしか使えません。例えば1フレーム目のレイヤー1の図形オブジェクトで変数Aがローカル変数で宣言された場合、変数Aは2フレーム目の同じ図形オブジェクトでは宣言なしに使うことは出来ませんし、レイヤー2の別のオブジェクトでも当然使うことが出来ません。
一方、変数がグローバル変数として宣言されると、その変数はAviUtl上のほとんど何処でも呼び出せるようになります。
例えば、1フレーム目のレイヤー1の図形オブジェクトで変数Bがグローバル変数で宣言されたとすると、変数Bは30フレーム目の同じ図形オブジェクトやレイヤー2の動画オブジェクトでも値をそのままに利用、変更することができます。(そのため、通常スクリプトを書く際は変数は全てローカル変数にするのが好ましいです。)
このような特性を持つグローバル変数と複数のオブジェクトを用いることで次のようなフレームが更新されても初期化されない変数を宣言することが出来ます。
code:script3.lua
@最初のフレームで使用
B = 0
obj.draw(B)
@最初以降のフレームで使用
B = B + 10
obj.draw(B)
この2つのスクリプトを下のように配置した2つオブジェクトに適切に適用すると、変数Bは初期化されずオブジェクトのx座標が1フレームあたり10ずつ増えていきます。
https://gyazo.com/6db0964de9ed9e9b6f9ef7d6b4684459
このような方法はシンプルで分かりやすい半面、複数のオブジェクトを使う必要がありまた、宣言する変数をオブジェクトごとに変えなくてはなりません (変えないと全てのオブジェクトで1つの変数を共有することになってしまい処理によっては破綻します。)そこで、次に1つのスクリプトで済み、他のオブジェクトと干渉しない方法について説明します。
オブジェクトグローバル変数
結論から述べると、1つのスクリプトで済み、他のオブジェクトと干渉しない変数─オブジェクトグローバル変数─を使用する方法とは
1. 変数が初期化されていないときは初期化し、初期化されている場合は初期化しない処理をする
2. グローバル変数を配列として宣言する
3. 配列の各インデックスが各オブジェクトで使う変数に対応する
このようなものになります。早速、実際のスクリプトをお見せします。
code:script4.lua
function undef_array_check(x)
end
function undef_number_check(x)
return x+1
end
if not(pcall(undef_array_check, B)) then
B = {0}
end
if not(pcall(undef_number_check, Bobj.layer))then end
変数Bが件のオブジェクトグローバル変数です。最初の2つの関数undef_array_checkとundef_number_checkは変数が初期化されているかどうかを確かめるための関数です。実際に確かめているのは下の2つのif文となります。if文の条件式で肝心なのはpcall関数です。このpcall関数は1番目の引数に入れられた関数が2番目以降に入れられた値を用いて実行したときにエラーが起こるかどうかを確認します。エラーが出てない場合はtrueをエラーが出た場合はfalseを返します。undef_array_check関数とundef_number_check関数はこのpcall関数を利用できるように、初期化された変数が入力された場合は問題なく実行し、初期化されてない変数が入力された時だけエラーが起こるような処理(return #xおよびreturn x+1)をしています。こうすることで、Bが初期化されてない時だけ1つ目のif文の条件式がtrueとなり、B={0}を実行して変数Bを初期化します。2つ目のif文も同様で、Bのobj.layer番目の要素が初期化されているかどうかを確認し、初期化されてない場合はB[obj.layer] = 0を実行して要素B[obj.layer]を初期化します。こうすることで、最初の一回だけ変数を宣言し、その後は変数の初期化がされない状態が実現されます。
また、変数を配列化することで異なるオブジェクトに対しそれぞれ処理をすることが可能となります。つまり、レイヤー1にあるオブジェクトはB[1]の値を、レイヤー2にあるオブジェクトはB[2]にある値を利用する訳です。逆に言えば、この方法ではオブジェクトグローバル変数を使う際にはほとんどの場合でB[obj.layer]と書く必要があることを示唆します。(専用のクラスを定義すればこのわずらわしさは多少改善されるかもしれません。)
それから注意点として、用いる変数の型に注意する必要があります。例えば、B[obj.layer]を配列として扱いたい場合に
code:script5.lua
function undef_array_check(x)
end
function undef_number_check(x)
return x+1
end
if not(pcall(undef_array_check, B)) then
B = {0}
end
if not(pcall(undef_number_check, Bobj.layer))then end
このようなことを行うと常にB[obj.layer]が初期化されてしまいます。これは配列であるB[obj.layer]を関数undef_number_checkに入れることでB[obj.layer]+1が行われエラーが発生するためです。よってこの場合は
code:script6.lua
function undef_array_check(x)
end
if not(pcall(undef_array_check, B)) then
B = {0}
end
if not(pcall(undef_array_check, Bobj.layer))then --undef_number_check を undef_array_check に変更 end
とすることで所望の動作が得られます。このように使用するオブジェクトグローバル変数の型に適したundef_check関数が必要になります。
実用上の細かな部分
上記のスクリプトのままではオブジェクトグローバル変数はレイヤーごとに管理されています。そのため、オブジェクトが切り替わっても同じレイヤーであれば変数が初期化されず変数の値が保持されたまま処理されることになります。そのため、実際には次のスクリプトのようにオブジェクトの1フレーム目でも初期化するのが望ましい場合が多いでしょう。
code:script7.lua
function undef_array_check(x)
end
function undef_number_check(x)
return x+1
end
if not(pcall(undef_array_check, B)) then
B = {0}
end
if not(pcall(undef_number_check, Bobj.layer)) or (obj.frame == 1) then end
また、この状態では一度宣言したオブジェクトグローバル変数を初期化するにはスクリプトを再起動する他ないので、手動で初期化するためのチェックボックスを用意すると使用者に易しいと思います。
code:script8.lua
--check0:初期化,0
function undef_array_check(x)
end
function undef_number_check(x)
return x+1
end
if not(pcall(undef_array_check, B)) then
B = {0}
end
if not(pcall(undef_number_check, Bobj.layer)) or (obj.frame == 1) or obj.check0 then end
チェックボックスがオンのままだとずっと初期化され、望んだ結果が得られないのでその旨はreadme等で伝えるべきだと思います。
おわり
ということで、AviUtlのスクリプトでフレームが更新されても初期化されないオブジェクト毎の変数を使う方法について説明しました。このオブジェクトグローバル変数を使う機会はそう多くはないと思われますが、読者様の何かしらのお役にたてれば幸いです。では、さようなら。
おまけ
使用例として はじめに にて紹介した音量メータのスクリプトを載せます。
code:volume_meter.obj
--track0:横幅,0,3840,100
--track1:待機時間,0,120,15,1
--track2:下降速度,0,100,10
--track3:音量,0,500,20
--check0:変数をクリア,0
function undef_array_check(x)
end
function undef_number_check(x)
return x+1
end
if not(pcall(undef_array_check, timer1)) then
timer1 = {0}
end
if not(pcall(undef_number_check, timer1obj.layer)) or (obj.frame == 1) or obj.check0 then end
if not(pcall(undef_array_check, y_pos)) then
y_pos = {0}
end
if not(pcall(undef_number_check, y_posobj.layer)) or (obj.frame == 1) or obj.check0 then y_posobj.layer = obj.screen_h/2 - obj.track0/2 end
local buf = {}
obj.getaudio(buf, "audiobuffer", "spectrum", 1)
local max_height = -obj.track3 * buf1 / 100 + obj.screen_h/2 obj.load("figure", "四角形", 0x808080, 1)
obj.drawpoly(-obj.track0/2, max_height, 0, obj.track0/2, max_height, 0, obj.track0/2, obj.screen_h/2, 0, -obj.track0/2, obj.screen_h/2, 0)
if (y_posobj.layer + obj.track0/2 + obj.track2 > max_height) then else
end
obj.load("figure", "四角形", 0xffffff, obj.track0)