メタテーブル(metatable)とは
メタテーブルは、テーブルの動きをカスタマイズするための特別な設定用のテーブルです。
メタテーブルを別のテーブルに紐づけることで、そのテーブルに特殊な振る舞いを追加できます。
👇メタテーブルを使うと、こういったことがカスタマイズできます。
- 存在しないキーを読んだときにどうするか
- 値を書き込んだときにどうするか
-
+などの演算子をどう振る舞わせるか - 文字列化をどうするか
今回は「存在しないキーを読んだときにどうするか」を設定するメタメソッド「__index」を使ってみます。
最小構成のクラス風コード
PlayerClass(ModuleScript)
ModuleScript で、プレイヤー用のクラス風のコード(テーブル)を用意します。
local PlayerClass = {}
PlayerClass.__index = PlayerClass
function PlayerClass.new(name)
local self = setmetatable({}, PlayerClass)
self.Name = name
self.HP = 100
return self
end
function PlayerClass:TakeDamage(amount)
self.HP -= amount
print(self.Name .. " は " .. amount .. " ダメージを受けた!")
print("残りHP: " .. self.HP)
end
function PlayerClass:IsDead()
return self.HP <= 0
end
return PlayerClass
PlayerClass.__index = PlayerClass
-
PlayerClassをメタテーブルとして持つテーブル(例:player)で、指定したキーが見つからなければ、PlayerClassから探す
__index は、 存在しないキーにアクセスしたときにどこを参照するかを決めるメタメソッド です。
メタメソッド
「メソッド」と付きますが、__index = PlayerClass のように、関数のほかにもテーブルを入れることもできます。
「特別なキー」「特殊な振る舞いを決める仕組み」というイメージです。
PlayerClass.new(name)
-
setmetatable({}, PlayerClass)は新しい空のテーブルを作り、PlayerClass(メタテーブル)を紐づける(⇒self) -
NameとHPのフィールドを設定する -
PlayerClassをメタテーブルとして持つテーブルselfを返す(⇒return self)
つまり、new() は、 PlayerClass をメタテーブルとして持つ新しいテーブルを作って返します。
(オブジェクト指向プログラミング的な見方では、この返ってきた実体を「インスタンス」と呼びます)
PlayerClass:TakeDamage(amount)
-
self(PlayerClassから作られたテーブル)の HP を減らす - ダメージ量と、
selfの残り HP を表示する
PlayerClass:IsDead()
-
selfの HP が 0 以下か判定して返す
GameScript(Script)
local PlayerClass = require(script.Parent.PlayerClass)
local player = PlayerClass.new("勇者")
print(player.Name .. " の冒険が始まる!")
print("初期HP: " .. player.HP)
player:TakeDamage(30)
player:TakeDamage(50)
player:TakeDamage(30)
if player:IsDead() then
print(player.Name .. " は倒れた…")
else
print(player.Name .. " はまだ生きている!")
end
local player = PlayerClass.new("勇者")
-
PlayerClassからプレイヤー用のテーブルplayerを作る
print(player.Name .. " の冒険が始まる!")
print("初期HP: " .. player.HP)
-
playerテーブルの名前と HP を表示する
player:TakeDamage(30)
-
player自身にはTakeDamageがないため、PlayerClassのTakeDamageを使ってplayerの HP を削る
PlayerClass:TakeDamage(amount) が実行される理由
player 自身には Name や HP はありますが、TakeDamage がありません。
そこで PlayerClass.__index = PlayerClass の設定により、player に存在しないキーは PlayerClass から探されます。
その結果、PlayerClass.TakeDamage を参照します。
そして、: により player が self として渡されて実行されます。
勇者 の冒険が始まる!
初期HP: 100
勇者 は 30 ダメージを受けた!
残りHP: 70
勇者 は 50 ダメージを受けた!
残りHP: 20
勇者 は 30 ダメージを受けた!
残りHP: -10
勇者 は倒れた…
player:TakeDamage(30) が実行されるまでの流れ
player:TakeDamage(30) から PlayerClass.TakeDamage が参照され、実行されるまでの流れです。
-
player:TakeDamage(30)を実行しようとする - まず、
player.TakeDamageを探す。(player自身のキーから探す) -
player.TakeDamageが見つからないのでPlayerClass.TakeDamageを探す。(PlayerClassのキーから探す) -
PlayerClass.TakeDamageが見つかる。TakeDamageキーには、関数が入っている -
:での呼び出しなので、playerが第1引数selfとして渡される -
PlayerClass.TakeDamage(player, 30)のような形で実行される
: でメソッドを定義すると何が起こるか
次のように : を使ってメソッドを定義すると、
function PlayerClass:TakeDamage(amount)
self.HP -= amount
end
これは次のコードとほぼ同じ意味です。
PlayerClass.TakeDamage = function(self, amount)
self.HP -= amount
end
つまり、
function PlayerClass:TakeDamage(amount)(メソッド)を定義すると、 PlayerClass テーブルに TakeDamage というキーが作られ、その値には関数が入ります。その関数の第1引数には self が自動で追加されます。
このため、player.TakeDamage を参照したときに関数が見つかり、player:TakeDamage(30) のように実行できるようになっています。