roblox-luau について
roblox-luauは、Roblox向けの最新Luauスクリプトの作成・レビューに使うコーディングスキルです。厳密な型付け、現行API、安全な非同期処理、クリーンアップ、パフォーマンスの実践を重視します。
主な機能
- 01厳密な型付けと最新Luau構文を適用
- 02非推奨APIに代わる現行Roblox APIを案内
- 03エラー処理とtaskによる並行処理を解説
- 04接続とインスタンスのクリーンアップを案内
- 05負荷の高いコード向けの性能手法を掲載
活用シーン
- 01新しいRoblox ModuleScriptsの作成
- 02非推奨APIを使うスクリプトの更新
- 03非同期呼び出しとリソース解放の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-luauインストール後、Claude Code で/roblox-luauを入力して読み込まれたか確認します。
同梱ファイルreferences/stdlib-cheatsheet.mdreferences/type-system.md
詳細
Roblox 向けモダン Luau
Luau は Roblox の段階的型付けされた Lua 5.1 の派生言語です。エンジンと言語は進化が速く、古いチュートリアル(および古い学習データ)の多くのパターンは非推奨になっています。プロジェクトが明らかに別の規約を使用していない限り、すべてのスクリプトにこれらのデフォルトを適用してください。
生成されるすべてのコードのデフォルト
- 新しい ModuleScript(および実用的な場合は Script)の先頭に
--!strictを付けます。公開関数には型を付けます。 - サービスはファイルの先頭で
game:GetService("Name")を使って取得し、game.Nameは決して使いません。 taskライブラリを使用します:task.wait、task.spawn、task.defer、task.delay、task.cancel。wait、spawn、delayは決して使いません(非推奨で、スロットリングされ、不正確です)。- 失敗する可能性のある yield するエンジン呼び出し(DataStore、HTTP、Marketplace、Teleport、
*Async)はpcallの中に入れます。 - API の
*Asyncバリアントを優先します。Roblox はほとんどの yield するメソッドの名前を変更しました (LoadCharacterAsync、GetProductInfoAsync、UserHasBadgeAsync、...)。以下の表を参照してください。 - 作成した接続は切断し、インスタンスは破棄します(以下の「クリーンアップ」を参照)。
- 命名: サービス、モジュール、クラス、型、インスタンス名には
PascalCase、 ローカル変数と関数にはcamelCase、定数にはSCREAMING_SNAKE_CASEを使います。 - インデントにはタブ(Roblox エコシステムにおける StyLua のデフォルト)、ダブルクォートを使います。
モダン構文チートシート
local Players = game:GetService("Players")
local count = 0
count += 1 -- compound assignment: += -= *= /= //= %= ^= ..=
local label = if count > 1 then "many" else "one" -- if-expression (no `and/or` pitfalls)
print(`Player count: {#Players:GetPlayers()} ({label})`) -- string interpolation
for index, player in Players:GetPlayers() do -- generalized iteration: no pairs/ipairs needed
if player.AccountAge < 1 then
continue -- continue is a keyword
end
print(index, player.Name)
end
local half = 7 // 2 -- floor division → 3
local frozen = table.freeze({ Speed = 16 }) -- read-only table; writes throw
local list = table.create(100, 0) -- preallocated array
local copy = table.clone(frozen) -- shallow copy (unfrozen)
local mid = math.lerp(0, 10, 0.5) -- also: math.map, math.clamp, math.round, math.sign
print(half, list[1], copy.Speed, mid)
pairs/ipairsはまだ動作しますが、一般化された反復が推奨され、配列を順番に反復します。#tは穴のない配列に対してのみ定義されます。途中でt[i] = nilを設定すると穴ができます。- 数値は倍精度浮動小数点数です。整数は 2^53 まで正確です。整数演算には
math.floor///を使います。 tick()は推奨されません。ベンチマークにはos.clock()、クライアント/サーバー間で同期された時刻にはworkspace:GetServerTimeNow()、実時間の UTC にはos.time()/DateTime.now()を使います。
型(新しい型ソルバー)
Roblox の新しい型ソルバーは一般提供されています(2025 年 11 月)で、Workspace.UseNewLuauTypeSolver
がそれを制御します。Workspace.LuauTypeCheckMode がデフォルトモードを設定します。--!strict の下で
クリーンにチェックが通るコードを書いてください。
--!strict
export type Item = {
id: string,
name: string,
stack: number,
tags: { string }?, -- optional field
}
type Rarity = "Common" | "Rare" | "Legendary" -- singleton (literal) union
local function totalStack(items: { Item }, filter: ((Item) -> boolean)?): number
local total = 0
for _, item in items do
if filter == nil or filter(item) then
total += item.stack
end
end
return total
end
local function first<T>(list: { T }): T? -- generics
return list[1]
end
local rarity: Rarity = "Rare"
local part = workspace:FindFirstChild("Door") :: BasePart? -- cast with ::
print(totalStack({}, nil), first({ 1, 2 }), rarity, part)
- 使用前に絞り込みます:
if part then ... end、if typeof(x) == "Instance" and x:IsA("BasePart") then。 typeof(v)は Roblox の型("Vector3"、"Instance"、...)を認識します。type(v)は Lua の型のみを認識します。- インスタンス型は
IsAの絞り込み、またはFindFirstChildOfClass/FindFirstChildWhichIsAから得られます。 anyは信頼境界(リモートペイロード、デコードされた JSON)以外では避けます — そこで検証してから絞り込みます。
詳細: ジェネリクス、型パック、read/write プロパティ、ユーザー定義型関数、OOP クラスの型付け、
レガシーコードの strict への移行: references/type-system.md。
モジュールと require
ModuleScript は環境ごと(サーバーまたは各クライアント)に一度だけ実行され、1 つの値を返し、以降の
すべての require でキャッシュされます。ReplicatedStorage 内のモジュールが両側から require されると、
側ごとに一度ずつ、別々の状態で 2 回実行されます。
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Signal = require(ReplicatedStorage.Packages.Signal) -- instance path (always works)
local Config = require("./Config") -- require-by-string: sibling of this script (script.Parent)
local Utils = require("@self/Utils") -- child of this script
local Shared = require("@game/ReplicatedStorage/Shared") -- absolute from game
print(Signal, Config, Utils, Shared)
- 文字列パス:
./=script.Parent、../=script.Parent.Parent、@self/=script、@game/=game。これらはインスタンスの存在を待ちません。モジュールがまだレプリケート されていない可能性がある場合は、WaitForChildを使ったインスタンスパスを使用してください。 - 循環 require は決して作成しないでください。デッドロックまたはエラーになります。循環は依存性注入 またはイベントで断ち切ってください。
- モジュールのトップレベルで yield しないでください(決して存在しない可能性のあるものに対する
WaitForChildチェーンや DataStore 呼び出しは避けます): すべての require 元はモジュールが 戻るまでブロックされます。
モジュール / クラスパターン
--!strict
local Counter = {}
Counter.__index = Counter
export type Counter = typeof(setmetatable({} :: { value: number, step: number }, Counter))
function Counter.new(step: number?): Counter
return setmetatable({ value = 0, step = step or 1 }, Counter)
end
function Counter.increment(self: Counter): number
self.value += self.step
return self.value
end
return Counter
深い継承よりも、関数 + データテーブルの単純なモジュールを優先してください。コンポジションを使います。
エラーとリトライ
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")
local function retry<T...>(attempts: number, fn: () -> T...): (boolean, T...)
local delaySeconds = 1
for attempt = 1, attempts do
local result = table.pack(pcall(fn))
if result[1] then
return table.unpack(result, 1, result.n)
end
if attempt < attempts then
warn(`attempt {attempt} failed: {result[2]}`)
task.wait(delaySeconds)
delaySeconds *= 2 -- exponential backoff
end
end
return false
end
local ok, data = retry(3, function()
return store:GetAsync("Player_1")
end)
print(ok, data)
error("message", 2)は呼び出し元を非難します。error({ code = "X" })は構造化エラー用に テーブルをスローします。xpcall(fn, debug.traceback)はスタックトレースを保持します。- すべてを
pcallで包まないでください — 自分のコードの外側の理由で失敗する可能性のある呼び出し のみです。
並行処理: タスクライブラリ
| 呼び出し | 動作 |
|---|---|
task.spawn(fn, ...) | 新しいスレッドで即座に実行します (最初の yield まで)。 |
task.defer(fn, ...) | 現在の再開サイクルの最後に実行します。「これが終わったら」に適しています。 |
task.delay(t, fn, ...) | t 秒後に実行します。task.cancel できるスレッドを返します。 |
task.wait(t?) | t 秒以上 yield します (省略時は 1 フレーム)。経過時間を返します。 |
task.cancel(thread) | spawn/delay されたスレッドを停止します。 |
task.desynchronize() / task.synchronize() | 並列 Luau (Actors 内) — roblox-performance を参照してください。 |
- フレームごとの処理:
RunService.Heartbeat(物理演算の後)、RunService.PreSimulation/PostSimulation、RunService:BindToRenderStep(クライアント、レンダリング前、順序付き)。フレームごとのロジックにwhile true do task.wait() endを使わないでください。イベントに接続し、そのdeltaTimeを使用してください。 - deferred シグナル動作 (
Workspace.SignalBehavior) で機能するハンドラーを書いてください。ハンドラーは フレーム内のより後のタイミングで実行され、インラインでは実行されません。新しいテンプレートのプレースはDeferredを使用し、Defaultはそれに切り替わる予定で、サーバー権威にはそれが必須です。:Fire()の直後にハンドラーの副作用が 見えることに依存しないでください。 - メタメソッドや、同期的でなければならない
Changed形式のコールバック内では決して yield しないでください。
クリーンアップ (メモリリークは Roblox のバグ第 1 位)
local Players = game:GetService("Players")
local connections: { RBXScriptConnection } = {}
table.insert(
connections,
Players.PlayerRemoving:Connect(function(player)
print(`{player.Name} left`)
end)
)
local function cleanup()
for _, connection in connections do
connection:Disconnect()
end
table.clear(connections)
end
cleanup()
Instance:Destroy()はそのインスタンスの接続を切断し、Parentをロックします。ただし、あなたの Lua 参照を クリアするわけではありません —PlayerRemovingでプレイヤーやインスタンスを保持するテーブルは nil にしてください。- 一回限りの接続には
:Once()を使用してください。グループには Trove/Janitor/Maid スタイルのヘルパーを使用してください。 - 長期間保持されるテーブル内のキーが Player/Instance オブジェクトである場合、退出/破棄時に削除しないとリークします。
バッファとベクター (パフォーマンスに敏感なデータ)
local packet = buffer.create(10)
buffer.writeu16(packet, 0, 513) -- item id
buffer.writef32(packet, 2, 12.5) -- amount
buffer.writei32(packet, 6, -4)
print(buffer.readu16(packet, 0), buffer.readf32(packet, 2), buffer.len(packet))
local v = vector.create(1, 2, 3) -- native 3-float vector (same type as Vector3 at runtime)
print(vector.magnitude(v), vector.normalize(v), vector.dot(v, vector.one))
buffer は、コンパクトなネットワークペイロード、大規模なグリッド、バイナリシリアライゼーションに適したツールです。
標準ライブラリのクイックリファレンス: references/stdlib-cheatsheet.md。
レガシー → 現行 (最もよくある間違い)
| 書かないでください | 代わりに書いてください |
|---|---|
wait(), spawn(), delay() | task.wait(), task.spawn(), task.delay() |
game.Workspace, game.Players | workspace, game:GetService("Players") |
Instance.new("Part", parent) | 作成し、プロパティを設定し、その後 最後に .Parent を設定します |
Humanoid:LoadAnimation() | Animator:LoadAnimation() |
Model:SetPrimaryPartCFrame() | Model:PivotTo() / Model:GetPivot() |
workspace:FindPartOnRay(Ray.new(...)) | workspace:Raycast(origin, direction, params) |
BodyVelocity, BodyPosition, BodyGyro | LinearVelocity, AlignPosition, AlignOrientation |
player:LoadCharacter() | player:LoadCharacterAsync() |
MarketplaceService:GetProductInfo() | :GetProductInfoAsync() |
BadgeService:AwardBadge() | :AwardBadgeAsync() |
player:GetRankInGroup() | GroupService:GetRolesInGroupAsync() / player:IsInGroupAsync() |
TeleportService:Teleport*() の各バリアント | TeleportService:TeleportAsync() |
Chat サービス / ChatService モジュール | TextChatService |
GuiObject.Draggable | UIDragDetector |
table.getn, table.foreach, getfenv | #t, for ... in, なし |
完全な非推奨マップは roblox-code-review スキルにあります。
パフォーマンスのイディオム (重要な箇所にのみ)
- まず計測してください (
debug.profilebegin/profileend、MicroProfiler、Script Profiler)。 - スクリプトの先頭に
--!native、または関数に@nativeを付けると、ホットな数値コードがネイティブにコンパイルされます。 数学的に重いモジュールに使用し、どこにでも使用しないでください (コンパイル時間 + メモリ制限)。型アノテーションが役立ちます。 Instanceの検索とサービスの参照をループの外でキャッシュしてください。フレームごとにFindFirstChildの連鎖を避けてください。- 大きな文字列は
table.concatまたはbufferで構築し、ループ内で..を繰り返さないでください。 - 呼び出しごとに作成するのではなく、
RaycastParams/OverlapParamsオブジェクトを再利用してください。
関連スキル
roblox-architecture(プロジェクト構成、クライアント/サーバー)、roblox-networking(リモート)、
roblox-performance(プロファイリング、Parallel Luau)、roblox-code-review(レビューチェックリスト、非推奨の全体マップ)。