「.」直アクセスと 「:WaitForChild()」 を正しく使い分ける

に公開

結論だけ先に

  • 存在が保証されているサービス/オブジェクト → . で即時アクセス
  • レプリケーション遅延や動的生成があり得る → 初期化フェーズ:WaitForChild() してキャッシュ
  • “パフォーマンス差” は CPU 観点では誤差。怖いのは ブロック時間ロジック停滞

1. そもそも何が違う?

parent.child(直アクセス) parent:WaitForChild("child")
存在しないと… nil → 後続でエラー 見つかるまで スレッド停止
典型ユース game:GetService("Players")
script.Parent.PartA
RemoteEvent, Character 部品
パフォーマンス ハッシュ検索 1 回で終了 FindFirstChild()→条件分岐
リスク ロード順バグで即クラッシュ 待ちすぎてフリーズ・デッドロック

2. 実測してみた(100 万回アクセス)

local parent = workspace
local t0 = os.clock()
for i = 1, 1e6 do
    local _ = parent.Terrain -- 存在する前提
end
print("dot:", os.clock() - t0)

t0 = os.clock()
for i = 1, 1e6 do
    local _ = parent:WaitForChild("Terrain", 0)
end
print("wait:", os.clock() - t0)
手法 所要時間(参考値)
. 20 ms
:WaitForChild(..., 0) 40 ms

差は 20 ms/100 万回。フレーム単位(16 ms ≒ 60 FPS)で数十回呼ぶ程度なら無視できる。


3. ブロッキングの落とし穴

-- 悪い例:ゲームループ内で毎フレーム待つ
RunService.Heartbeat:Connect(function()
    local obj = ReplicatedStorage:WaitForChild("Bullet") -- 生成が遅れるとフリーズ
    -- …
end)
  • 生成が遅れる/そもそも来ない → コールバックが 永久ブロック
  • デバッグも難航。「たまに固まる」症状の元凶になりがち

4. ベストプラクティス 3 箇条

  1. 初期化時に 1 回だけ待ち、即キャッシュ

    local RS = game:GetService("ReplicatedStorage")
    local spawnBulletEvent = RS:WaitForChild("SpawnBullet")
    
  2. キャッシュ変数を後続で使い回す

    • 毎回 WaitForChild() しない
  3. タイムアウトを付けて安全装置

    local gun = char:WaitForChild("Gun", 5)
    if not gun then
        warn("Gun が見つからずスキップ")
    end
    

5. 使い分けチャート

+┬ それは最初から必ず存在する?
│ ├─ YES → dot(.) で即アクセス
│ └─ NO
│     ┌ そのオブジェクトが来るまで待っても良い場所?
│     │ ├─ YES → 初期化で WaitForChild() → キャッシュ
│     │ └─ NO → FindFirstChild() で存在チェック+イベント駆動
└──────────────────────────

6. よくある Q&A

Q A
:FindFirstChild() と何が違う? 非ブロッキングで即 nil or 子を返すだけ。ループでポーリングすると重くなりやすい。
タイムアウト 0 を渡すテクニックってアリ? 「存在確認+警告」をワンライナーで書けるが、毎フレーム呼ぶのは不要。
サーバー側でも WaitForChild 必須? マップ生成を後から行う場合など、動的生成なら必要。固定配置なら不要。

まとめ

  • CPU コスト差は誤差、だが ブロッキング時間がゲーム体験を壊す
  • 初期化でキャッシュ → 以降は直アクセス が鉄則
  • ChildAdded / CharacterAdded など イベント駆動で “待たない設計” を心掛ける

これで “`. と WaitForChild どっち問題” とはサヨナラ。安全かつ滑らかなゲーム体験を届けよう!

Discussion