この文書の目的
例えば、3次元ベクトルオブジェクト v を使用するとき、X,Y,Z など座標軸を意識して書く場合は v.X などと書くのが自然だと思います。しかし、その一方で、行列との掛け算を行う場合は、メンバ変数 X,Y,Z をひとつの配列のように取り扱うことができると便利です。
本文章では、ベクトル型の変数 v に対し、メンバ変数 X を v[1] としても取り扱う方法について説明します(…とはいえ、実際には、ベクトル型以外に使用される場面は少ないかもしれませんが、Lua でも実現できる、ということで記しておきます)。
動作確認した Lua のバージョン
Lua-5.3.4。メタテーブルが使える Lua のバージョンであれば、多分使えると思います。
しくみの概略
Lua におけるオブジェクトのインスタンス(といっても単なるテーブルですが)に対し、添字のオペレータを適用する場合、そのテーブルのメタテーブルに設定された関数で添字の情報を取得することができます。
本文書では、その仕組みを活用して、v[2] と書けば、v.Y を表すような仕組みを実現すします(ちなみに v.X は v[1] を、v.Z は v[3]と表すこととします)。
メタテーブル __index と __newindex の仕様
テーブルに対し、指定されたメンバを持っていない場合、__index や __newindex が参照されます。この仕組を活用し、型定義のためのテーブルをこれらのフィールドに指定することにより Lua のオブジェクト指向が実現されるのは周知の通りです。
__index や __newindex にはテーブルしか設定できないのか?といえば、そうではありません。関数を設定することも可能です。その場合、テーブルに存在しないメンバにアクセスするとき、設定した関数が呼び出されます。
これらの関数に渡される引数ですが、__index では第 1 引数に対象となっているテーブルが、第 2 引数には見つからなかったメンバ名がそれぞれ入っています。
t={}
setmetatable(t,{__index=function(inSelf,inIndex)
print("index="..inIndex)
end})
t.A -- "index=A" と表示される
t[3] -- "index=3" と表示される
__newindex では、上記の引数に加え、第 3 引数として設定しようとしている値が渡されます。
t={}
setmetatable(t,{__newindex=function(inSelf,inIndex,inValue)
print("index="..inIndex)
print("value="..inValue)
end})
t.A=1 -- "index=A" および "value=1" と表示される
t[3]=10 -- "index=3" および "value=10" と表示される
複数のメンバ変数を配列のように扱う方法
サンプルで使用するオブジェクトの仕様
本文書では、X,Y,Z という 3 つのメンバ変数を持つ Vector3 というオブジェクトを用いて説明を行います。それぞれのメンバ変数には、v[1]〜v[3] としてもアクセスできることとします。もちろん、メンバ変数を直接指定する場合でも、配列として扱う場合でも、どちらでも読み書きできるものとします。
-- Vector3 の使用例
v=Vector3.new(1,2,3) -- メンバ変数 X,Y,Z にそれぞれ 1,2,3 を設定
print(v.X) -- 1 と表示される
v.X=10
v[2]=20
for i=1,3 do print(v[i]) end --- 10 20 3 とそれぞれ改行して出力される
Vector3 型の定義
例えば、以下のようなコードを書くことにより、上で定義した振る舞いをする Vector3 型が実現できます。
Vector3={}
Vector3.new=function(inX,inY,inZ)
if inX==nil or inY==nil or inZ==nil then
error("invalid argument.\n"
.."Vector3.new needs 3 arguments. ex: v=Vector3.new(1,2,3)")
end
local ret={}
ret.X=inX
ret.Y=inY
ret.Z=inZ
ret.Dump=function(inSelf)
print("{X="..inSelf.X..", Y="..inSelf.Y..", Z="..inSelf.Z.."}")
end
setmetatable(ret,{
__index=function(inSelf,inIndex)
if inIndex==1 then
return inSelf.X
elseif inIndex==2 then
return inSelf.Y
elseif inIndex==3 then
return inSelf.Z
else
error("getter: invalid index="..inIndex)
end
end
,
__newindex=function(inSelf,inIndex,inValue)
if inIndex==1 then
inSelf.X=inValue
elseif inIndex==2 then
inSelf.Y=inValue
elseif inIndex==3 then
inSelf.Z=inValue
else
error("setter: invalid index="..inIndex)
end
end
})
return ret
end
応用例
Vector3 型のメンバに配列として読み書きできるようになっているので、行列との掛け算などもループを使って書くことができます。
Matrix3x3={}
Matrix3x3.new=function(inA11,inA12,inA13,
inA21,inA22,inA23,
inA31,inA32,inA33)
local ret={{inA11,inA12,inA13},
{inA21,inA22,inA23},
{inA31,inA32,inA33}}
ret.Apply=function(inSelf,inVec)
local v=Vector3.new(0,0,0)
for i=1,3 do
local t=0
for j=1,3 do
t=t+inSelf[i][j]*inVec[j] -- 配列として読みとり
end
v[i]=t -- 配列として代入(書きこみ)
end
return v
end
ret.Dump=function(inSelf)
for i=1,3 do
print(inSelf[i][1].." "..inSelf[i][2].." "..inSelf[i][3])
end
end
return ret
end
上記で定義した Matrix3x3 と Vector3 は、以下のように使用することができます。
v=Vector3.new(1,2,3)
v:Dump() -- {X=1, Y=2, Z=3} と表示される
-- m に X 要素と Z 要素を入れ替える行列を設定する
m=Matrix3x3.new(0,0,1,
0,1,0,
1,0,0)
t=m:Apply(v)
t.Dump() -- {X=3, Y=2, Z=1} と表示され、正しく計算できていることが確認できる
まとめ
本文書では、メタテーブルの __index および __newindex に関数が指定できることを紹介し、その応用として複数のメンバ変数を配列のように扱う方法を説明しました。
同様の仕組みで、Lua でも C# のプロパティのようなものを定義することができると思います。こちらについても、機会があれば別途文章を書いてみたく思います。
もし、この記事が役に立ったとか、面白いと思われましたら、「いいね」やストック、フォローしていただければ嬉しいです。Qiita で文章を公開する励みになりますので、よろしくお願いします。