LuaJITでお手軽3Dプログラミング (1) (2013/03/29)
世間では電子工作系の話題と違って、Raspberry Pi を使ったプログラミングの話題はあまり盛り上がっていないようですが、私は Raspberry Pi の GPU でずっと遊んでいます。 これまで C を使ってみたり (Raspberry Pi で OpenGL ES2 (1), (2), (3), (4), (5), (6) )、Python で試してみたりしていましたが、楽しく「コードを書いて実行する」事ができるのは LuaJIT という結論に達しました。 LuaJIT用のRaspberry Pi専用3Dライブラリが、やっと形になってきたので「 LuaJITでお手軽3Dプログラミング」という新シリーズを始めてみたいと思います。まずは使用するスクリプト言語 Lua と、私が今ものすごく惚れ込んでいる LuaJIT 処理系の紹介です。
Raspberry Pi は、超小型、省電力で普通に Linux が動作するデスクトップPCとして使えますが、デスクトップで利用する X.org のドライバがGPUを使ったハードウェアアクセラレーションに対応していないため、何をするにも遅い印象があります。一方、 Minecraft の Raspberry Pi 専用版 では3次元グラフィックのアニメーションがスムーズに動作します。おそらく、デスクトップを表示している X Window System とは独立したレイヤーにGPUを使ったOpenGLの画面を表示し、それをX上のウィンドウと同じ位置とサイズに置くことによって、デスクトップで動作しているように見せていると思われます。Raspberry Pi でもGPUを使えば3次元グラフィックのアニメーションをスムーズに動作させることができるわけです。
CやC++でプログラミングすれば最速のアプリケーションを作成できますが、前に書いた「Raspberry Pi で OpenGL ES2 (1), (2), (3), (4), (5), (6) 」ようにVideoCoreの初期化、EGLの初期化、OpenGL ES2の初期化と使い方、Cのソースのコンパイルなど、お手軽にプログラミングをしてみようという感じではありません。
「 LuaJITでお手軽3Dプログラミング」としては、もっと簡単に3Dプログラミングを始める事ができるように LjES というライブラリを作成してみました。LjES は大体下の図に示すような構成になっています。 LuaJIT の ffi を使って C の共有ライブラリをリンクして OpenGL ES2 などを Lua のコードから使えるようにしています。 Raspberry Pi の公式ディスクイメージの Raspbian には最初からLuaJIT が入っており、それ以外には何もインストールしなくても 100% LuaJITのスクリプトだけで実行できるようにしています。
冒頭のスクリーンショットは、次のコードを実行した時のものです。30行程度のコードでテクスチャを貼った物体をアニメーションできます。次回以降で解説する予定です。 今回はコードの雰囲気だけの紹介です。
package.path = "../LjES/?.lua;" .. package.path -- LjES の場所 require("Texture") -- モジュールの読み込み require("Space") require("Shape") local demo = require("demo") demo.screen(0, 0) -- スクリーンの生成 local aSpace = demo.getSpace() -- 空間の生成 local eye = aSpace:addNode(nil, "eye") -- 視点の生成 eye:setPosition(3, 10, 40) -- 視点の位置 local tex = Texture:new() tex:readImageFromFile("ljes03.png") -- テクスチャのロード local donuts = aSpace:addNode(nil, "obj") -- 空間にノードを配置 donuts:setPosition(0, 20, 0) donuts:setAttitude(90, 27, 0) local shape = Shape:new() shape:donuts(10, 3, 32, 32) -- 形状とサイズを指定 shape:endShape() shape:shaderParameter("use_texture", 1) -- 形状に質感を設定 shape:shaderParameter("texture", tex) shape:shaderParameter("ambient", 0.1) shape:shaderParameter("light", {50, 0, 50, 1}) donuts:addShape(shape) -- 形状をノードに登録 demo.backgroundColor(0.2, 0.2, 0.4) -- 背景色の設定 function draw() -- 描画内容の更新 donuts:rotateY(0.5) -- 回転 aSpace:draw(eye) -- 描画 end demo.loop(draw, true) -- アニメーションループ demo.exit() -- 終了処理
Luaについて
Lua は主にアプリケーションやゲームに組み込んで使われることの多い、高速でコンパクトなスクリプト言語です。 文法の規模が小さいので習得は非常に容易です。 オブジェクト指向プログラミングも工夫次第で可能になっています。
ある程度規模の大きいアプリケーション(ブラウザ、ワード、エクセル、ゲーム等)になると、設定や動作の変更、決まった一連の動作の登録といった便利機能を実現するために、専用の小さな言語をアプリケーションごとに用意することになります。ブラウザの JavaScript、ワードやエクセルの Visual Basic が分かりやすい例でしょうか。 いろいろなアプリケーションの開発者が、それぞれアプリケーション専用のプログラミング言語の設計や作成の技術を習得するのは大変なことです。 そこでアプリケーションに組み込むことを目的としたコンパクトな言語として、1993年頃 のブラジルで Lua は誕生しました。
LuaJIT-2.0 について
LuaJIT は Lua 5.1.5 に対応した JITコンパイラ処理系で、実行時 (Just In Time) に部分的にネイティブコードにコンパイルした後に実行します。もともと速い Lua がさらに何倍も速くなっていて、スクリプト言語で最速の処理系と言われたりします。Raspberry Pi の公式OSである Raspbian のディスクイメージには、最初からARM hard-float ABI with VFP (armhf)に対応した LuaJIT 2.0.0-beta11 (2012-10-16) が入っていて、すぐにでも使うことができます。 LuaJIT はコンパイラでもあるため、CPU と OS の色々な決め事 (Application Binary Interface) に合わせてバイナリコードを生成する必要がありますが、 Raspbian が採用している armhf に対応したバージョンが存在するのは非常にラッキーなことです。LuaJIT 自体のバイナリのサイズが365キロバイトと非常にでコンパクトで、インタプリタの手軽さとコンパイラの高速性を備えた超おすすめの処理系です。
さらに LuaJIT には標準で FFI (Foreign Function Interface) モジュール という拡張機能を持っていて、その中にCのヘッダーを理解するパーサを持っているため、Cのライブラリの宣言(関数や構造体)をほとんどそのままコード中で使うことができます。実行時には共有ライブラリを /etc/ld.so.conf から検索してきて(たぶん)、LuaJITが実行しているコードにリンクできます。 数多く存在する C の共有ライブラリを LuaJIT のコード内で使える (Python の ctypesと同様) ようになります。 LuaJIT は FFI のおかげで Lua のコードだけで C と同じような守備範囲のプログラミングが可能となり、オリジナルの Lua に比べて言語の可能性を非常に広げた処理系になっていると思います。
Lua の文法について
命令文の数は少なく、基本的に関数宣言と関数の呼び出し、代入文、if、for、while、repeat、return の使い方を覚えるだけです。何かプログラミング言語を知っていれば簡単に習得できる (見かけ上) 小さな言語ですが、手続き型、オブジェクト指向、関数型といろいろなプログラミングスタイルが可能な自由度の高い言語です。
Lua 5.1 のリファレンスマニュアルを翻訳してくださっている方がいて、 https://milkpot.sakura.ne.jp/lua/lua51_manual_ja.html で読むことができます。 リファレンスマニュアルを参照してください。 書籍では Programming in Lua の第2版の日本語訳( プログラミング言語Lua公式解説書) があります。LuaJIT は Lua 5.1 相当なので日本語訳でも大丈夫です。
Lua のコードは上から順に実行されます。実行を開始するための main のような特別な関数はありません。関数の定義も含めて、上から順に文 (statement) を実行するのが基本です。関数の定義も実行できる文となっていることに注意して下さい。 普通に実行する文と関数定義を混ぜて書くことができます。冒頭のコードの下から2行目の「demo.loop(draw, true)」の draw は関数で使う直前に「 function draw() .. end 」として定義しています。
文の種類
- 変数名 = 式
- 関数呼び出し
- do ブロック end
- while 式 do ブロック end
- repeat ブロック until 式
- if 式 then ブロック elseif 式 then ブロック else ブロック end
- for 変数名 = 開始式 , 終了式, ステップ式 do ブロック end
- for 変数名リスト in 式リスト do ブロック end
- 関数定義
- local 関数定義
- local 変数名 = 式
- return 式リスト
- break
どの文もC、Pascal、Javascript、Python など普通の言語とよく似たものとなっています。switch文やcase文はありません。 以下、注意する必要のある部分を説明します。
代入文
Lua は同時に複数の代入ができるため、代入文では要素をカンマで区切った形式で、左辺に変数リスト、右辺に式リストを書きます。 変数に関数を代入することもできます。
x, y, z = 0, 1, 2 f = function (x) return x + x end
return文とbreak文
Lua の関数は return文で関数の値を返すことができますが、返り値として複数の値を指定できます。 その返り値を受け取るには、複数の変数を指定した代入文を使います。
function f(n)
return n, n + 1, n + 2
end
a, b, c = f(2)
文 (statement) のリストをブロックと呼びますが、ブロックを「do」と「end」で囲んで 1つの文として扱うことができます。return 文とbreak 文はブロックの最後の文である必要があるため、ブロックの途中で使いたい場合は do return end、do break end のように書くことができます。
if文
if 文はブロックの範囲が if文で使うキーワードの then、elseif、else、end にはさまれた部分となります。 Lua は switch 文を持たないため、elseif をよく使います。
if i == 1 then print "one" elseif i == 2 then print "two" else print "many" end
繰り返し文
while 文
while は条件式の後ろの文を、条件式が true の間は繰り返し、false になると終了します。 普通は do .. end ブロックの中に複数の文を置きます。
local i = 1 while i <= 10 do print(i) i = i + 1 end
repeat 文
repeat は until と対になって、少なくとも1回は実行されるループを作ります。until の条件式が false の間は繰り返し、true になるとループを抜けます。 repeat と until で構成される繰り返しブロックは until キーワードのところではなく条件式の後で終わります。 until の条件式の中でブロックの内側で宣言されたローカル変数を使うことができます。
local i = 1 repeat print(i) i = i + 1 until i > 10
for 文
for 文は文の実行を指定した回数繰り返すのに使います。 3つの制御式 (開始式 , 終了式, ステップ式) はループが始まる前に一度だけ評価され、数値でなければなりません。for ループを脱出するためには break を使います。ループ変数はループ内でローカルになっていて、あらためてlocal 宣言する必要はありません。 したがって、for 文の外側ではループ変数の値にはアクセスできないため、ループ変数の値が必要な場合はループを出る前に別の変数に代入する必要があります。
for i = 1,10 do print(i) end
ローカル宣言
Luaでは local というキーワードを付けずに新規に作成した変数は、すべてグローバル変数となります。したがって、ローカル変数を使用するためには、local というキーワードを変数の前に付けて宣言します。 ローカル変数はブロック中のどこでも宣言でき、多重代入と同じ形式で初期値を代入することができます。初期値が代入されない場合は nil で初期化されます。ブロックの外側も一種のブロック (Luaではチャンク (chunk) と呼ぶ) なのでローカル変数を宣言できます。
local 文の変数のスコープは次の文から始まり、ブロックの終わりまでです。 以下の例のように、ローカル宣言する変数の初期化のために代入する右辺の値「a」のスコープはグローバルか、より外側のスコープなので正常に動作します。
local a = a
一方、次のような形式で定義されたローカル関数では関数本体の中で再帰できません。 local 宣言される変数 (この場合は関数が変数の値となる) のスコープは次の文から始まるため、関数本体の中の aFunc() はローカルに宣言したものとは異なります。 もし、外側で宣言したものがあれば、それが呼び出されることになります。
local aFunc = function() -- aFunc() -- 「local aFunc」はここでは見えない -- end
ローカルな関数で再帰呼び出しを行う場合は、ローカル変数の宣言を別の文に分ける必要があります。
local aFunc aFunc = function() -- aFunc() -- この場合は再帰呼び出しになる。「local aFunc」が可視のため -- end
演算子
Luaに特徴的な演算子は「等しくない」の「~=」、剰余の「%」 、累乗の「^」、論理演算の「and」、「or」、「not」、文字列連結の「..」です。C と Pascal を混ぜたような感じです。ビット演算用の演算子はありません。ビット演算用のライブラリを使う必要があります。
長さ演算の「#」は文字列に対してはバイト数、テーブル t の長さは t[n] が nil でなく t[n+1] が nil であるような整数 n と定義されています。テーブルの要素の値に nil を持つような場合にはテーブルの要素の数と # 演算子の返す値が異なる可能性があります。
演算子の種類
四則演算 + - * / 比較 < <= > >= == ~= 論理 and or not 累乗 ^ 剰余 % 文字列連結 .. 長さ #
演算子の優先順位
低い方から
or and < <= > >= == ~= .. + - * / % not # -(単項) ^
文字列
文字列には3種類の書き方があって、シングルクォートで囲まれた'文字列'、ダブルクォートで囲まれた"文字列"、角カッコ形式(long bracket)の文字列があります。
角カッコ形式の文字列リテラル ( [[文字列]] ) は複数行になった文字列を表現でき、文字列の中に含まれるエスケープシーケンスは解釈されず、異なる段数の閉じ長括弧は無視します。 異なる段数の長括弧を以下に示します。
- 0段: [[文字列]]
- 1段: [=[文字列]=]
- 2段: [==[文字列]==]
- n段: [=(n個)[文字列]=(n個)]
コメント
文字列の外なら 二つのハイフン (--) から行末まではコメントとなります。 もし -- の直後に開き長括弧がある場合は、対応する段数の閉じ長括弧まで複数行にわたって続きます。 つまり、複数行に渡る角カッコ形式の文字列の直前に二つのハイフンを付けると全体をコメントアウトすることができます。
-- 短いコメント --[[ 長いコメント 複数行にわたってもよい。 ]] -- --[[ コメントではないブロック --]]
最後の例は、「-- --[[」も「--]]」も短いコメントですが、最初のハイフン2つを消すと長いコメントの始まりとなって、ブロック全体をコメントアウトするのに便利です。「--]]」のように長いコメントの閉じかっこの前にハイフン2つを置くことで、長いコメントの終わりと、短いコメントの始まりの両方の解釈が可能となります。
数値
扱うことのできる数値は仕様では倍精度浮動小数点のみですが、LuaJITは内部的に整数で処理することもあるようです。
テーブル、配列
Luaには通常の配列がなく、書き方は違うものの JavaScriptのオブジェクトとほぼ同じ機能のテーブルと呼ぶデータ型を使います。 テーブルは辞書、連想配列、ハッシュとも呼ばれる「添え字として文字列も使える配列」で、キーと値のペアで構成されます。 値はどんなデータ型も格納できます。
以下の例では table1 という変数にテーブルを代入しています。数値、文字列、関数を格納しています。最後の「table1.d = 1」では、新たにキーが d という項目を作成して数値 1 を代入しています。 テーブルにデータと関数を格納できるため、オブジェクト指向プログラミングにおけるクラスやクラスのインスタンスとして使うことができます。
-- Lua のテーブル table1 = { a = 1, b = "abcdefg", c = function (x, y) print(x + y) end } table1.d = 1
テーブルの要素は、テーブルの名称がtable1の場合には table1.d でも table1["d"] の形式でも記述できます。テーブルを次のように値だけ並べて作成した場合は、配列のように数値型の添え字でアクセスできます。
-- Lua のテーブル table2 = { 100, 200, 300, "abc" } print( table2[3] ) -- 300 を表示
table2[3] が3番目の値 300 を返していることに注意して下さい。 Lua の配列の添え字は 1 から始ります。
Luaのテーブルとの比較のために、JavaScriptのオブジェクトを載せておきます。ほとんど同じですね。
// JavaScriptのオブジェクト var obj = { a : 1, b : "abcdefg", c : function (x, y) { print(x + y); } }; obj.d = 1;
テーブル操作
テーブルを操作する関数として、table.concat、table.insert、table.maxn、table.remove、table.sort がありますが、使い道が多いとは言えず、テーブルの末尾に追加する table.insert ぐらいしか使いません。 テーブルが変数に代入されている場合に table.insert(変数名, 値) のように使います。テーブル名.insert(値) ではありません。
関数の引数
function 関数名 (引数リスト) ブロック end 関数名 = function (引数リスト) ブロック end local function 関数名 (引数リスト) ブロック end local 関数名; 関数名 = function (引数リスト) ブロック end
ローカル宣言された関数内で自分自身を呼び出すためには、4番目の書き方にする必要があります。
関数宣言で引数の宣言は次のような形式です。
- func(a)
- func(a, b)
- func(a, b, ...)
3番めの形式で可変個の引数を実現できます。
関数呼び出しで obj.func(obj, a, b, ...) を obj:func(a, b, ...) と書くことができます。オブジェクト指向プログラミングの場合に便利なシンタックスシュガーとなっています。関数宣言では function obj:func(a, b, ...) は function obj.func(self, a, b, ...) と同じで、引数名はself となります。
関数の引数が1つの場合だけ、文字列やテーブルの引数をカッコを省略した形式が使用できます。
func' ' func" " func' ' func{ }
メタテーブル
メタテーブルはオブジェクト指向プログラミングの仕組みや演算子オーバロード等の知識を持っていないとちょっと理解し難い機能ですが、Lua でオブジェクト指向プログラミングを実現するために必要となる機能です。Lua はどんな型のデータでも「メタテーブル (metatable) 」を持つことができます。メタテーブルは Lua の通常のテーブルですが、C++の演算子オーバロードのように特定のデータの特定の演算に対して演算の内容を定義できます。
Luaのデータ型で、テーブル以外は型ごとにメタテーブルを持ちます。つまりすべての数値にはひとつだけメタテーブルが存在し、すべての文字列に対してひとつだけメタテーブルが存在することができます。 一方、テーブルの場合は、テーブル毎にメタテーブルを設定することができます。メタテーブルは設定されていないのがデフォルトとなっています。設定するためにはsetmetatable(テーブル, メタテーブル) を実行します。
tableA = { a1=1, a2=2, a3="A" } tableB = { b="B", c="C" } print( tableA.b ) nil metaTableA = { __index = tableB} setmetatable(tableA, metaTableA) print( tableA.b ) B
この例では、 tableA、 tableB、metaTableA ともに普通のテーブルです。最初の print文では tableA.b という項目はないので nil が表示されますが、2番目のprint 文では、tableA のメタテーブルに metaTableA が設定されて、その __index フィールドにtableB が設定されたため、tableA にないキーは tableB に対しても検索されて、結果として見つかった tableB.b が表示されています。
メタテーブルのキーをイベント、値をメタメソッドと呼びます。 加算を例にすると、"add" がイベント名で、キーが __add 、加算を行う関数がメタメソッドになります。
キー | 動作 |
---|---|
__add | + 演算 |
__sub | - 演算 |
__mul | * 演算 |
__div | / 演算 |
__mod | % 演算 |
__pow | ^ 演算 |
__unm | 単項 - 演算 |
__concat | .. 連結 |
__len | # 演算 |
__eq | == 等号 |
__lt | < |
__le | <= |
__index | table[key] 参照、table に key が存在しない場合だけ呼ばれる |
__newindex | table[key] = value 代入、table に key が存在しない場合だけ呼ばれる |
__call | 値を関数呼び出ししたときに呼ばれる |
__tostring | 文字列表現が必要な場合に呼ばれる |
テーブルにキーが存在しない場合だけ呼ばれる __index の利用法として、__index を使うとテーブルの拡張や共通部分の括りだしのような事が可能になります。 テーブルを参照するとき、該当するフィールドが見つからないとエラーになりますが、そのテーブルにメタテーブルが設定 (setmetatable) されていて、さらにメタテーブルの __index フィールドにテーブルが代入されている場合、その__indexのテーブルからもフィールドが探索されます。もしそのテーブルでも見つからない場合、そのテーブルにメタテーブルがあり、さらに __index でテーブルが指定されていれば、__index で指定されたテーブルを探索することになります。
LjES のオブジェクトモデル
3次元の世界を表現するとき、その空間にはたくさんの「物」が存在することになります。大きさ、色、位置、角度、速度などの性質が異なった「物」をコンピュータの中のプログラムで扱うには、オブジェクト指向というプログラミング手法が便利です。「物」を数値や文字と同じように1つのデータ型として扱えると好都合です。Lua には直接オブジェクト指向プログラミングを行う機能はありませんが、テーブルを利用して「オブジェクト」を実現することになります。 「LjES」を使う上ではほとんど必要ないと思いますが、「LjES」が内部で使っているオブジェクトモデルを紹介します。
データと関数 (メソッド) をまとめて1つの型として扱えるものを「オブジェクト」とするならば、Lua のテーブルにデータと関数を格納するだけで「オブジェクト(クラス)」の機能は実現できます。クラスのインスタンスはクラスとなっているテーブルをコピーする事でインスタンスを作成することになります。 そしてインスタンス毎にデータの内容を変更する事で別々のインスタンスを表現できます。
ここで、同じ型のデータ (オブジェクト) が多く登場する事を考えます。テーブルをコピーする方法では、インスタンス毎に変わらない関数(メソッド)の定義もコピーすることになってメモリの無駄遣いになります。 そこで、インスタンスの共通部分をくくり出したり、既存のクラスとは少し異なる派生クラスを表現するために「継承 (inheritance)」を実現する事にします。 継承とは、既存のクラスの内容を引き継いで新たな別のクラスを定義する機能のことですが、これをメタテーブルを利用して実現します。
「LjES」ではすべてのクラスとインスタンスの元となる Object という名のテーブルを定義しています。Object の new メソッドは新しい空のテーブルを生成(local obj = {})し、そのテーブル自身をメタテーブルとして設定(setmetatable(obj, obj))し、__index キーを継承元のテーブル(オブジェクト) に設定します。
Object = {} function Object.new(self) local obj = {} setmetatable(obj, obj) obj.__index = self return obj end
Object を継承したクラスは Object:new() で作成し、そのクラスにメソッドを追加していきます。 クラスとインスタンスの違いは、クラスとなるオブジェクトは1つだけ作成して必要なメソッドを追加しますが、インスタンス用のオブジェクトは元となるクラスを継承して、データだけ変更(追加)します。
詳細は Raspberry Pi メモ (31) - Lua でオブジェクト指向(2014/05/17) に書きました。
下の図は、Object → Shader → Font と継承した Font クラスから font インスタンスを作成した場合の、クラスやインスタンスとなるテーブルの概要です。
Object = {} +---------+--------+ メタテーブル無し | | | +---------+--------+ __index 無し function Object.new(self) local obj = {} setmetatable(obj, obj) -- 生成されるオブジェクト obj の obj.__index = self -- : metatable を self に設定 return obj -- : 検索先も self に設定 end Object +---------+--------+ | new | func | メタテーブルは自分 +---------+--------+ __index 無し | Object. | func | : methods : func : | | func | +---------+--------+ <--------------------------------------+ | | Shader = Object:new() | +---------+--------+ -------------------------------------- + | __index | Object | <-----+ setmetatable(obj, obj) | +---------+--------+ ------+ メタテーブルは自分 | 【生成時】 | | function Shader.new(self) | local obj = Object.new(self) -- 空objのmetatableにShaderを | obj.program = 0 -- : 設定して返す。 | return obj -- : 検索先も self に設定 | end | Shader | +---------+--------+ ---------------------------------------+ | __index | Object | <-----+ setmetatable(obj, obj) +---------+--------+ ------+ メタテーブルは自分 | new | func | +---------+--------+ | Shader. | func | : methods : func : | | func | +---------+--------+ <--------------------------------------+ 【メソッド、メンバ定義後】 | | Font = Shader:new() | +---------+--------+ -------------------------------------- + | __index | Shader | <-----+ setmetatable(obj, obj) | +---------+--------+ ------+ メタテーブルは自分 | | program | 0 | | +---------+--------+ | 【生成時】 | | function Font.new(self) | local obj = Shader.new(self) -- Shader.new(Font)で作成 | obj.aPosition = 0 -- 空objのmetatableにFont、 | obj.aTexCoord = 0 -- : データ program を設定し | obj.uTexUnit = 0 -- : 返す。 | obj.uColor = 0 | obj.uChar = 0 | obj.uScale = 0 | obj.scale = 1.0 | return obj | end | Font | +---------+--------+ ---------------------------------------+ | __index | Shader | <-----+ setmetatable(obj, obj) +---------+--------+-------+ メタテーブルは自分 | program | 0 | +---------+--------+ | new | func | +---------+--------+ | Func. | func | : methods : func : | | | +---------+--------+ <--------------------------------------+ 【メソッド、メンバ定義後】 | | font = Font:new() インスタンス(クラスオブジェクトではない) | +---------+--------+ ---------------------------------------+ | __index | Font | <-----+ setmetatable(obj, obj) +---------+--------+ ------+ メタテーブルは自分 | program | 0 | +---------+--------+ |aPosition| 0 | +---------+--------+ |aTexCoord| 0 | +---------+--------+ |uTexUnit | 0 | +---------+--------+ |uColor | 0 | +---------+--------+ |uChar | 0 | +---------+--------+ |uScale | 0 | +---------+--------+ |scale | 1.0 | +---------+--------+ 【生成時】
Lua には直接オブジェクト指向プログラミングを行う機能はないので、「オブジェクト」の実現方法は1つとは限りません。興味ある方は、Programming in Lua / 16–Object-Oriented Programming や https://lua-users.org/wiki/SampleCode を参考にして下さい。 ここで紹介した「LjES」のオブジェクトモデルは「LjES」を使う上ではほとんど必要ないと思います。 「LjES」のソースを解読する場合の参考にして下さい。