mcpField
R
roblox-luau

roblox-luau

github.com/el4cteo開発59623 回インストール2 週間前に更新
リポジトリ

roblox-luau について

roblox-luauは、Roblox向けの最新Luauスクリプトの作成・レビューに使うコーディングスキルです。厳密な型付け、現行API、安全な非同期処理、クリーンアップ、パフォーマンスの実践を重視します。

主な機能

  1. 01厳密な型付けと最新Luau構文を適用
  2. 02非推奨APIに代わる現行Roblox APIを案内
  3. 03エラー処理とtaskによる並行処理を解説
  4. 04接続とインスタンスのクリーンアップを案内
  5. 05負荷の高いコード向けの性能手法を掲載

活用シーン

  1. 01新しいRoblox ModuleScriptsの作成
  2. 02非推奨APIを使うスクリプトの更新
  3. 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 の派生言語です。エンジンと言語は進化が速く、古いチュートリアル(および古い学習データ)の多くのパターンは非推奨になっています。プロジェクトが明らかに別の規約を使用していない限り、すべてのスクリプトにこれらのデフォルトを適用してください。

生成されるすべてのコードのデフォルト

  1. 新しい ModuleScript(および実用的な場合は Script)の先頭に --!strict を付けます。公開関数には型を付けます。
  2. サービスはファイルの先頭で game:GetService("Name") を使って取得し、game.Name は決して使いません。
  3. task ライブラリを使用します: task.wait、task.spawn、task.defer、task.delay、task.cancel。 wait、spawn、delay は決して使いません(非推奨で、スロットリングされ、不正確です)。
  4. 失敗する可能性のある yield するエンジン呼び出し(DataStore、HTTP、Marketplace、Teleport、*Async)は pcall の中に入れます。
  5. API の *Async バリアントを優先します。Roblox はほとんどの yield するメソッドの名前を変更しました (LoadCharacterAsync、GetProductInfoAsync、UserHasBadgeAsync、...)。以下の表を参照してください。
  6. 作成した接続は切断し、インスタンスは破棄します(以下の「クリーンアップ」を参照)。
  7. 命名: サービス、モジュール、クラス、型、インスタンス名には PascalCase、 ローカル変数と関数には camelCase、定数には SCREAMING_SNAKE_CASE を使います。
  8. インデントにはタブ(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.Playersworkspace, 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, BodyGyroLinearVelocity, 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.DraggableUIDragDetector
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(レビューチェックリスト、非推奨の全体マップ)。

コメント