Luaでコードを書くときの注意点
Luaは「軽量で書きやすい」言語ですが、プロジェクトが大規模化するにつれて、暗黙の仕様やテーブル・メタテーブルの扱い、GC(ガベージコレクション)、そして
1始まりの配列などがバグやパフォーマンス低下の原因になりやすいという特徴があります。
概要
Luaは「小さく書くには快適」ですが、大きくなるほど規約・型注釈・メモリ管理方針が重要になります。
特にPlaydateのような制約の強い環境では「何でもクラス化する」ことを避け、以下のようなルールをプロジェクト全体で徹底することが成功の鍵となります。
- 配列は基本的に 1始まり で考える
- local を忘れるとグローバル変数になるため徹底する
- nil は値ではなく「存在しない」に近いため、配列の途中に入れない
- # (長さ取得) は途中に nil があると信用できない
- 重い処理は毎フレーム行わない (テーブルの作成 / 文字列連結 / インスタンスの生成 / クロージャーの作成)
- メタテーブルによるOOPは、大量オブジェクト生成時にメモリ・GC負荷になりやすい (→Playdateでオブジェクト指向プログラミングを避けるべき理由)
- "." と ":" の違いを間違えると self がずれる
- テーブルは 参照型 なので、意図せぬデータ共有に注意する
- pairs の要素取り出し順序は保証されない
- 動的型付けのため Typo が残りやすい(Linterや型注釈を活用する)
1. 構文と基本仕様の落とし穴
他言語(C++, C#, JavaScriptなど)の経験者が直感で書くとバグにつながりやすい仕様です。
- 配列は「1始まり」が基本
- Luaの配列は 1 から始まります。
local items = { "sword", "shield", "potion" }
-- 要素の取得.
print(items[1]) -- 1番目は "sword"
print(items[0]) -- 0番目は "nil"
-- for文での回し方.
for i = 1, #items do
print(items[i])
end
- 無理に 0 始まりにすると #(長さ取得)や ipairs との相性が悪くなるため、原則として1始まりに統一するのが安全です。
- 変数は必ず "local" をつける
- "local" を付けずに宣言した変数はグローバル変数になります。
count = 0 -- グローバル (スコープ外の変数の値を書き換える可能性がある.
local count = 0 -- ローカル (スコープ内のみ有効)
- 意図しない変数の上書き(スコープ汚染)を防ぐため、変数や関数、モジュールは必ず local で宣言してください。
- メソッド呼び出しの ":" と "." の違い
- actor:update() は actor.update(actor) のシンタックスシュガーであり、暗黙的に第1引数に self を渡します。
function Actor:update()
self.x = self.x + 1
end
-- 上記は以下と同じ (":" は self が不要)
function Actor.update(self)
self.x = self.x + 1
end
- 定義側と呼び出し側で ":" と "." を間違えると引数がずれてエラーになります。
- 偽(False)になるのは false と nil のみ
- 0 や空文字列 "" は真(True)として評価されます。
- 数値のチェックは if count > 0 then のように明示的に行う必要があります。
- and / or を使った三項演算子風の記述に注意
- condition and a or b という書き方は、a が false や nil の場合に破綻します。
- 安全性を重視するなら素直に if ~ else を使いましょう。
- 関数の複数戻り値
- 関数は複数の値を返せますが、テーブル初期化などで複数戻り値が「最後の式」でない場合、最初の1つしか展開されず残りは切り捨てられます。
local t = { getPosition() } -- getPosition() が {10, 20} を返す場合…
-- 特にエラーにならず "20" は捨てられる
local t = { getPosition(), 30 } -- {10, 30}
2. テーブル操作とデータ構造
Luaの万能データ構造である「テーブル」に関する特有の挙動です。
- nil は「値の不在(削除)」を意味する
- テーブルの要素に nil を代入することは、要素の削除と同義です。
local t = { "A", nil, "C" }
print(#t) -- 3ではなく2となる.
- 配列の途中に nil が入ると連続性が崩れ、# 演算子での要素数取得が正確に行えなくなります。空スロットを表現したい場合は専用のダミー値(例: EMPTY = {})を使う方が安全です。
- テーブルは「参照型」
- local b = a としてもテーブルはコピーされず、同じ参照を共有します。
- 独立したデータが必要な場合は、明示的にコピー(Shallow Copy / Deep Copy)を実装する必要があります。
- pairs のループ順序は保証されない
- テーブルを pairs で回した際の要素の取り出し順はランダムです。
- 順序が重要なデータは配列で管理し、ipairs や数値の for ループを使用してください。
3. オブジェクト指向とパフォーマンス (GC対策)
ゲーム開発など、毎フレームの処理速度が求められる環境で特に意識すべき点です。
- クラスは標準機能ではなく「メタテーブル」で代用
- Luaにはクラス構文がないため、テーブルとメタテーブル (__index) でオブジェクト指向風に実装するのが慣習です。
- (→Luaでオブジェクト指向プログラミングをする方法)
- 大量のオブジェクト生成はメモリとGCを圧迫する
- 弾やパーティクルなど、大量に生成・破棄されるものをすべてメタテーブルを使ったクラス風オブジェクトにすると、テーブル確保やメソッド解決のコストがかさみ、GCによるカクつき(スパイク)の原因になります。
- (→Playdateでオブジェクト指向プログラミングを避けるべき理由)
- ただし、速度を求めないゲーム (e.g. ターン制) やオブジェクト数が少ないゲーム、一部のオブジェクトにのみ使用するといった用途であれば許容されることがあります。
- 毎フレームの「使い捨てオブジェクト」を避ける
- update 関数の中で毎回一時テーブル { x = ..., y = ... } を作ったり、無駄な文字列連結(..)を行ったり、クロージャ(無名関数)を大量生成したりするとGC負荷が上がります。
- 変数を使い回すなどの工夫が必要です。
- 用途に応じたデータ構造の使い分け
- パフォーマンスを最適化するため、以下のように使い分けるのが効果的です。
| 用途 |
推奨される実装方法 |
| プレイヤー・敵・UI・管理クラス |
"メタテーブル" によるクラス風実装にしても良い |
| 弾・エフェクト(大量・短命) |
単純なテーブル、配列、またはオブジェクトプール (インスタンスを使い回す) を検討する必要がある |
| 大量の数値データ |
極力シンプルな配列 |
| 高頻度更新される処理 |
関数呼び出しやメタテーブル解決を減らす |
4. 大規模化に向けた設計と運用ルール
Luaの自由度の高さを制御し、保守性を保つためのアプローチです。
- 動的型付けによる Typo に弱い
- 変数名やメソッド名の打ち間違いが実行時まで発覚しません。
- Lua Language Serverの導入、---@class などの型アノテーション、重要な箇所での assert() の活用がバグ防止に繋がります。
- require はキャッシュされる
- 一度 require で読み込んだモジュールはキャッシュされ、どこから呼んでも同じ参照(シングルトン)になります。
- 新しいインスタンスが必要な場合は、モジュールから「生成関数(Factory)」を返す設計にしてください。
- 標準ライブラリが最小限
- クラス、Enum、ディープコピー、JSON処理などの便利機能は標準で用意されていないため、プロジェクトの初期段階で共通ユーティリティとして整備しておくことが重要です。
参考:重い処理の例 (GC負荷)
Luaはガベージコレクションを持つため、不要になったテーブルや文字列は自動的に回収されます。ただし、ゲームではGCがフレーム中に走るとカクつきの原因になります。
特に注意したいのは以下です。
- 毎フレーム新しいテーブルを作る
- 毎フレーム文字列連結を大量に行う
- 弾やエフェクトを毎回生成・破棄する
- クロージャを頻繁に作る
- 一時テーブルを大量に作る
- 悪い例: 一時テーブルの毎フレーム作る
- 例えば、次のようなコードは小規模なら問題ありませんが、高頻度では避けたいです。
function update()
-- 毎フレーム "一時テーブル" を作っている.
local pos = { x = player.x, y = player.y }
end
- 軽くするなら、キャッシュして使い回しを検討します。
local tempPos = { x = 0, y = 0 }
function update()
tempPos.x = player.x
tempPos.y = player.y
end
- 悪い例: 毎フレーム文字列連結を行う
- ログ用の文字列連結も、毎フレーム大量に行うと負荷になります。
print("x=" .. x .. ", y=" .. y)
- デバッグ中は便利ですが、製品版や高頻度処理では抑制したほうがよいです。
- 悪い例: クロージャーを作りすぎる
- Luaでは関数も値なので、関数の中で関数を作れます。
function createCallback(actor)
-- 関数を返す.
return function()
actor:update()
end
end
- これは便利ですが、毎フレームこの関数が呼び出される場合は問題です。
関連ページ
最終更新:2026年06月16日 00:15