這個頁面描述了常見的效能問題和緩解最有效的最佳實踐。
腳本計算
Luau 代碼中的昂貴操作需要更長的處理時間,因此可能會影響幀率。除非它以並行方式執行,否則 Luau 代碼會同步運行,並在遇到使執行緒暫停的函數前阻塞主執行緒。
常見問題
在表結構上進行密集運算 - 複雜的操作,例如序列化、反序列化和深度複製,會產生高效能成本,尤其是在大型表結構上。如果這些操作是遞歸的或涉及遍歷非常大的數據結構,則尤其如此。
高頻率的事件 - 將昂貴的操作與 RunService 的基於幀的事件綁定而不限制其頻率,意味著這些操作在每個幀都會重複,這通常會導致不必要的計算時間增長。這些事件包括:
緩解
- 在 RunService 事件中稀疏地調用代碼,將使用限制在高頻調用至關重要的情況下(例如,更新相機)。大多數其他代碼可以在其他事件中執行,或在循環中以較低頻率執行。
- 使用 task.wait() 將大型或昂貴的任務分解,將工作平均分配到多個幀中。
- 確定並優化不必要的昂貴操作,並對於不需要訪問數據模型的計算密集型任務使用 多線程。
- 某些伺服器端腳本可以受益於 本地代碼生成,這是一個簡單的標誌,將腳本編譯為機器碼而不是字節碼。
MicroProfiler 範疇
| 範疇 | 相關計算 |
| RunService.PreRender | 在 PreRender 事件上執行的代碼 |
| RunService.PreSimulation | 在 Stepped 事件上執行的代碼 |
| RunService.PostSimulation | 在 Heartbeat 事件上執行的代碼 |
| RunService.Heartbeat | 在 Heartbeat 事件上執行的代碼 |
有關使用 MicroProfiler 調試腳本的更多信息,請參閱 debug 庫,該庫包括標記特定代碼的函數和進一步增加特異性的函數,例如 debug.profilebegin 和 debug.profileend。許多由腳本調用的 Roblox API 方法也有其相關的 MicroProfiler 標籤,這可以提供有用的信號。
腳本內存使用
當您編寫的腳本消耗內存而垃圾收集器無法正確釋放這些內存時,就會發生內存洩漏。在伺服器上,這種情況尤其普遍,因為它們可以持續在線上許多天,而客戶端會話則短得多。
開發者控制台中的以下內存值可以指示需要進一步調查的問題:
- LuaHeap - 高或增長的消耗表明存在內存洩漏。
- InstanceCount - 持續增長的實例數量表明您的代碼中對某些實例的引用未被垃圾收集。
- PlaceScriptMemory - 提供逐個腳本的內存使用情況分解。
常見問題
表格 - 將對象插入表格但不在不需要時刪除它們,會導致不必要的內存消耗,尤其是在跟踪用戶數據的表格中。例如,以下代碼示例在每次用戶加入時創建添加用戶信息的表格:
範例local playerInfo = {}Players.PlayerAdded:Connect(function(player)playerInfo[player] = {} -- 一些信息end)如果您不在不再需要時刪除這些條目,則該表將繼續增長並隨著更多用戶加入會消耗更多內存。任何迭代該表的代碼在表增長時也會變得計算成本更高。
緩解
為了清理所有使用的值以防止內存洩漏:
斷開所有連接 - 檢查您的代碼庫,確保通過以下其中一條路徑清理每個連接:
- 使用 Disconnect() 函數手動斷開。
- 使用 Destroy() 函數摧毀事件所屬的實例。
- 摧毀該連接追溯的腳本對象。
在用戶離開後刪除玩家對象和角色 - 啟用 Workspace.PlayerCharacterDestroyBehavior 在用戶離開後自動摧毀玩家對象和角色模型。如果您願意,您也可以選擇手動清理它們:
範例玩家和角色清理local Players = game:GetService("Players")Players.PlayerAdded:Connect(function(player)player.CharacterRemoving:Connect(function(character)task.defer(character.Destroy, character)end)end)Players.PlayerRemoving:Connect(function(player)task.defer(player.Destroy, player)end)
物理計算
過度的物理模擬可以是伺服器和客戶端每幀增加計算時間的關鍵原因。
常見問題
過度的物理時間步頻率 - 默認情況下,步進行為為 自適應模式,物理以 60 Hz、120 Hz 或 240 Hz 的速度進行,具體取決於物理機制的複雜性。
也可使用固定模式以提高物理的精確度,強迫所有物理組件以 240 Hz(每帧四次)的速率步進。這會導致每個幀顯著增加計算量。
過多的複雜模擬對象 - 模擬的 3D 組件越多,每個幀的物理計算所需時間就越長。通常,遊戲中會存在不需要被模擬的對象,或包含比所需更多的約束和關節的機制。
過於精確的碰撞檢測 - 網格部分具有 CollisionFidelity 屬性,用於檢測碰撞,提供各種性能影響不同的模式。網格部分的精確碰撞檢測模式成本最高,需要更長的計算時間。
緩解
固定不需要模擬的部分 - 鎖定所有不需要由物理驅動的部分,例如靜態 NPC。
使用自適應物理步進 - 自適應步進動態調整物理計算的速率,允許在某些情況下減少物理更新的頻率。
減少機制複雜性
- 在可能的情況下,最小化組件中物理約束或關節的數量。
- 透過限製或無碰撞的約束,減少機制內自我碰撞的程度,例如防止拉格多肢體相互碰撞。
減少網格的精確碰撞忠誠度的使用
對於小型或非互動對象,使用盒子忠誠度,因為用戶不會注意到差異。
對於小至中型對象,根據形狀使用盒子或外殼忠誠度。
對於大型和非常複雜的對象,儘可能使用不可見的部件構建自定義碰撞。
對於不需要碰撞的對象,禁用碰撞並使用盒子或外殼忠誠度,因為碰撞幾何體仍保存在內存中。
有關如何選擇平衡精度和性能需求的碰撞忠誠度選項的深入指南,請參閱 設置物理和渲染參數。
MicroProfiler 範疇
| 範疇 | 相關計算 |
| physicsStepped | 整體物理計算 |
| worldStep | 每個幀進行的離散物理步驟 |
物理內存使用
物理運動和碰撞檢測消耗內存。網格部分具有 CollisionFidelity 屬性,用於確定評估網格碰撞邊界的方法。
常見問題
默認和精確的碰撞檢測模式相比低忠誠度的兩種模式消耗的內存顯著更多。
如果您在 PhysicsParts 下看到高內存消耗,您可能需要考慮減少遊戲中對象的 碰撞忠誠度。
如何緩解
為了減少用於碰撞忠誠度的內存:
- 通過使用 CollisionFidelity 設置減少碰撞的忠誠度。Box 具有最低的內存開銷,而 Default 和 Precise 通常成本更高。
- 通常可以將任何小型固定部分的碰撞忠誠度設置為 Box。
- 對於非常複雜的大型網格,您可能需要使用小型對象和盒子碰撞忠誠度構建自己的碰撞網格。
人類角色
Humanoid 是一個為玩家和非玩家角色 (NPC) 提供廣泛功能的類。雖然功能強大,但 Humanoid 也帶來顯著的計算成本。
常見問題
- 對 NPC 啟用所有 HumanoidStateTypes - 保留某些 HumanoidStateTypes 啟用會產生性能成本。禁用對您的 NPC 不必要的狀態。例如,除非您的 NPC 需要爬梯子,否則可以安全地禁用 Climbing 狀態。
- 在不需要的情況下使用 Humanoids - 不會移動的靜態 NPC 通常不需要 Humanoid 類。
- 從伺服器上播放大量 NPC 的動畫 - 在伺服器上運行的 NPC 動畫需要在伺服器上進行模擬並複製到客戶端。這會造成不必要的開銷。
- 執行不必要的大小和縮放變更 - 大小/縮放變更會導致 FastCluster 重新建立。如果您在遊玩時遇到與 FastCluster 相關的性能問題,請嘗試減少此類更改。類似地,其他屬性變更可能也會導致 FastCluster 重新建立,因此通常應盡量減少這些變更。
緩解
- 在客戶端播放 NPC 動畫 - 在擁有大量 NPC 的遊戲中,考慮在客戶端創建 Animator 並在本地運行動畫。這樣可以減輕伺服器負擔,並減少不必要的複製,並且還可以實現其他優化(例如僅播放靠近角色的 NPC 的動畫)。
- 使用性能友好的替代品來取代 Humanoids - NPC 模型不一定需要包含 humanoid 對象。
- 對於靜態 NPC,使用簡單的 AnimationController,因為它們不需要移動,只需要播放動畫。
- 對於移動的 NPC,根據 NPC 的複雜程度考慮實現自己的移動控制器並使用 AnimationController 來播放動畫。
- 禁用未使用的人類狀態 - 使用 Humanoid:SetStateEnabled() 僅啟用每個人類所需的狀態。
- 使用頻繁重生的 NPC 模型池 - 不要完全摧毀 NPC,而是將 NPC 發送到非活動 NPC 的池中。這樣,當需要重生新 NPC 時,您可以簡單地重新激活池中的一個 NPC。這個過程稱為模型池,最小化了角色需要實例化的次數。
- 僅在用戶靠近時重生 NPC - 當用戶不在範圍內時不要重生 NPC,當用戶離開範圍時對其進行剔除。
- 避免在實例化後對化身層次結構進行更改 - 對化身層次結構進行某些修改會對性能產生重大影響。一些可用的優化包括:
MicroProfiler 範疇
| 範疇 | 相關計算 |
| stepHumanoid | 人類控制和物理 |
| stepAnimation | 人類和動畫者動畫 |
| updateInvalidatedFastClusters | 與實例化或修改化身相關的計算 |
渲染
客戶端每幀大部分時間都花在當前幀中渲染場景上。伺服器不進行渲染,因此本節僅限於客戶端。
繪圖調用
繪圖調用是引擎向 GPU 渲染某物的一組指令。繪圖調用具有顯著的開銷。通常,每幀的繪圖調用越少,渲染幀所花費的計算時間就越少。
您可以查看 Studio 中的 渲染統計 ⟩ 計時 項目,瞭解當前發生了多少次繪圖調用。您可以通過按 ShiftF2 在客戶端查看 渲染統計。
在給定幀中需要在場景中繪製的對象越多,則向 GPU 發出的繪圖調用也越多。然而,Roblox 引擎利用一個稱為 實例化 的過程,將相同紋理特徵的相同網格合併為單個繪圖調用。具體而言,當以下情況發生時,具有相同 MeshContent 的多個網格會在單個繪圖調用中處理:
- 如果存在,則 SurfaceAppearances 彼此相同;否則,當 TextureContents 彼此相同時。
- 當 SurfaceAppearance 和 MeshPart.TextureID 均不存在時,材料彼此相同。
其他常見問題
過度的物體密度 - 如果大量物體在高密度中集結,則渲染該場景區域需要更多繪圖調用。如果您發現查看地圖的某個部分時幀率下降,這可能是該區域物體密度過高的好信號。
像貼花、紋理和粒子這樣的對象不易批處理,會引入額外的繪圖調用。對於這些對象類型,請特別注意。特別地,對 ParticleEmitters 的屬性變更可能對性能產生重大影響。
錯過實例化機會 - 通常,一個場景中會包括相同網格多次複製,但每個複製的網格具有不同的網格或紋理資產 ID。這會阻止實例化,並導致不必要的繪圖調用。
此問題的常見原因是整個場景一次性導入,而不是單獨將資產導入 Roblox,然後在導入後複製組裝場景。
甚至像這樣的簡單腳本可以幫助您識別名稱相同但使用不同網格 ID 的網格部分:
for _,descendant in workspace:GetDescendants() doif descendant:IsA("MeshPart") thenprint(descendant.Name .. ", " .. descendant.MeshId)endend輸出(啟用 堆疊行 時)可能看起來像這樣。重複的行表示重用了相同的網格,這是好的。唯一的行不一定是壞事,但根據您的命名規則,可能表明在遊戲中存在重複的網格:
LargeRock, rbxassetid://106420009602747 (x144) -- goodLargeRock, rbxassetid://120109824668127LargeRock, rbxassetid://134460273008628LargeRock, rbxassetid://139288987285823LargeRock, rbxassetid://71302144984955LargeRock, rbxassetid://90621205713698LargeRock, rbxassetid://113160939160788LargeRock, rbxassetid://135944592365226 -- all possible duplicates過度的物體複雜性 - 雖然物體的繪圖調用數量並不如三角形的數量那麼重要,場景中的三角形數量確實會影響幀的渲染時間。具有大量非常複雜網格的場景是常見問題,具有許多網格的 MeshPart.RenderFidelity 屬性設置為 Precise 的場景也是如此。
過多的陰影投射 - 處理陰影是一個昂貴的過程,包含大量且密集的光源對象或受到陰影影響的小型部件的地圖可能會出現性能問題。
高透明重繪 - 將部分狀態透明的對象放置在相互靠近的位置,迫使引擎多次渲染重疊的像素,這會影響性能。欲了解更多有關識別和修復此問題的信息,請參考 刪除分層透明度。
不必要的綁定 MeshPart 移動 - 網格部分是沒有 Humanoid 的模型的一部分,通過空間組織的 FastClusters 進行分組。當這些 MeshParts 移動時,必須不斷地將它們添加到這些空間集群中並將其移除,迫使集群重新建立並影響性能。
- 一個有效的解決方法是將 Humanoid 嵌入模型中。Humanoid 的存在會覆蓋默認的空間集群行為,強制整個模型使用單一的統一 FastCluster。因此,位置更新不再需要集群重建,從而減輕性能瓶頸。這種技術應僅保留給預期會運動的 MeshParts,因為它可能會引入內存開銷並抵消空間優化的好處。我們建議在進行這類更改後總是對遊戲進行性能分析。請參見 Humanoid 性能提示 獲取更多信息。
在 Model 中的部件過多 - 模型中的部件過多可能導致重新建立過於頻繁,因為部件的屬性改變可能導致要求完整重建。當使用 FastCluster 時,請找到模型中部件的正確平衡。
緩解
實例化相同的網格並減少獨特網格的數量 - 如果您確保所有相同的網格具有相同的基礎資產 ID,則引擎可以識別並在單個繪圖調用中渲染它們。確保在地圖中只上傳每個網格一次,然後在 Studio 中複製它們以便重用,而不是一次性導入大型地圖,這可能導致相同的網格擁有不同的內容 ID,並被引擎識別為獨特的資產。封裝是對象重用的有用機制。
剔除 - 剔除是描述刪除不影響最終渲染幀的繪圖調用的過程。默認情況下,引擎會跳過相機視場以外的對象(視錐剔除)和被其他對象遮擋的部件、網格和地形(遮挡剔除)的繪圖調用。在某些場景中,例如室內環境中,您可以實施房間或門戶系統,並手動剔除對象,以進一步減少繪圖調用或整體計算負擔。
減少模型的細節層級 - 啟用 實例流式傳輸,並將您的世界模型的 LevelOfDetail 屬性設置為 SLIM,以在距離相機增大時渲染 優化的輕量級 SLIM 網格。
減少化身的細節層級 - 啟用實例流式傳輸並將 Workspace.EnableSLIMAvatars 設置為隨著距離相機增大而渲染平台化身為 優化的輕量級 SLIM 表示,具有全面的動畫支持。
減少渲染忠誠度 - 將 MeshPart.RenderFidelity 設置為 Automatic 或 Performance。這允許網格回退到較不複雜的替代方案,從而減少需要繪製的多邊形數量。
禁用適當部分和光源上的陰影投射 - Roblox 引擎會隨著客戶端圖形質量等級的降低,自動降低陰影質量,最終在等級低於 4 時完全禁用陰影。然而,您可以選擇性地禁用光源和部件上的陰影投射屬性,以提高在啟用陰影時的性能並增加陰影保持啟用的可能性。一些您可以在編輯時或在運行時動態進行的優化示例:
使用 BasePart.CastShadow 屬性禁用不太可能顯示陰影的小部件的陰影投射。這種策略在應用於遠離用戶相機的部件時特別有效。
儘可能禁用移動對象的陰影。
禁用不需要投射陰影的光源實例上的 Light.Shadows。
限制光源實例的範圍和角度。
使用更少的光源實例。
考慮禁用超出特定範圍的光源或在室內環境中逐間禁用光源。
MicroProfiler 範疇
| 範疇 | 相關計算 |
| Prepare and Perform | 整體渲染 |
| Perform/Scene/computeLightingPerform | 光網格和陰影更新 |
| LightGridCPU | 體素光網格更新 |
| ShadowMapSystem | 陰影映射 |
| Perform/Scene/UpdateView | 渲染和粒子更新的準備 |
| Perform/Scene/RenderView | 渲染和後處理 |
網絡和複製
網絡和複製描述了資料在伺服器和連接的客戶端之間發送的過程。每幀在客戶端和伺服器之間都會發送資料,但更大的資料量需要更多計算時間。
常見問題
過多的遠程流量 - 通過 RemoteEvent 或 RemoteFunction 對象發送大量數據,或非常頻繁地調用它們,可能導致每幀處理進來的數據包消耗大量 CPU 時間。常見錯誤包括:
- 每幀複製不需要複製的數據。
- 在用戶輸入時複製數據,但沒有任何限制機制。
- 散發超過所需的數據。例如,在玩家購買物品時發送整個物品清單,而不僅僅是購買的物品的詳細信息。
創建或移除複雜實例樹 - 當伺服器上對數據模型進行更改時,這會被複製到連接的客戶端。這意味著在運行時創建和摧毀大型實例層次結構(例如地圖)可能會非常耗費網絡資源。
這裡的一個常見罪魁禍首是由 Animation Editor 插件在模型中儲存的複雜動畫數據。如果這些在遊戲發布前未被刪除,而動畫模型被經常克隆,將發送大量不必要的數據。
服務器端 TweenService - 如果 TweenService 在服務器端用於動畫對象,則每幀會將動畫的屬性複製到每個客戶端。這不僅會導致不穩定的動畫,因為客戶端的延遲波動,還會造成大量不必要的網絡流量。
緩解
您可以採用以下策略來減少不必要的複製:
- 避免通過遠程事件一次性發送大量數據。相反,僅以較低的頻率發送必要數據。例如,為角色的狀態,在其更改時複製,而不是每幀都複製。
- 將複雜的實例樹分塊(例如地圖)並分段加載,以分散多幀中複製工作的負擔。
- 清理動畫元數據,特別是對於模型的動畫目錄,導入後進行清理。
- 限制不必要的實例複製,特別是當伺服器不需要知道創建的實例時。這包括:
- 視覺效果,如爆炸或魔法攻擊。伺服器只需要知道位置來確定結果,而客戶端可以在本地創建視覺效果。
- 第一人稱物品視圖模型。
- 在客戶端而不是服務器上進行動畫對象進行 Tween。
MicroProfiler 範疇
| 範疇 | 相關計算 |
| ProcessPackets | 處理來自網絡的數據包,例如事件調用和屬性變更 |
| Allocate Bandwidth and Run Senders | 伺服器上相關的輸出事件 |
資產內存使用
創作者改善客戶端內存使用的最高影響機制是啟用 實例流式傳輸。
實例流式傳輸
實例流式傳輸有選擇地加載不需要的數據模型部分,這可以顯著減少加載時間並增加客戶端在面臨內存壓力時防止崩潰的能力。
如果您遇到內存問題並且禁用了實例流式傳輸,考慮更新您的遊戲以支持它,特別是如果您的 3D 世界很大。實例流式傳輸基於三維空間中的距離,因此較大的世界自然受益更多。
如果啟用了實例流式傳輸,您可以增加其積極性。例如,考慮:
- 減少 Enum.ModelStreamingMode.Persistent 的使用。 如果您將其用作兼容措施,則可能需要更新您的腳本。
- 減少 Workspace.StreamingMinRadius 和 Workspace.StreamingTargetRadius。
有關流式傳輸選項及其好處的更多資訊,請參閱 流式傳輸屬性。
其他常見問題
資產重複 - 常見錯誤是多次上傳相同資產,導致不同的資產 ID。這可能導致相同的內容被多次加載到內存中。
資產體積過大 - 即使資產不相同,通常也會錯過重用相同資產並節省內存的機會。
音頻文件 - 音頻文件可能是內存使用的驚人貢獻者,特別是當您一次性將所有文件加載到客戶端,而不是僅根據遊戲的一部分僅加載所需的音頻文件時。策略請參見 加載時間。
高解析度的紋理 - 紋理的圖形內存消耗與磁碟上紋理的大小無關;紋理中的像素數量決定了內存使用。例如,1024x1024 像素的紋理消耗的圖形內存是 512x512 紋理的四倍。
上傳到 Roblox 的圖像會轉碼為固定格式,因此上傳以更少字節每像素關聯的顏色模型的圖像不會帶來內存好處。同樣,在上傳之前壓縮圖像或從不需要的圖像中刪除 alpha 通道可以減小磁碟上的圖像大小,但不會改善內存使用。
在遊戲加載時,引擎會自動從較低質量的紋理開始,然後根據可用設備內存、距離相機、紋理占用的屏幕空間量和其他因素提升質量。即便如此,戰略性地調整您的紋理大小可以改善您遊戲中的內存使用。
緩解
僅上傳一次資產 - 在對象之間重用相同的資產 ID,確保相同的資產,特別是網格和圖像,沒有被重複上傳多次。
尋找並修復重複資產 - 尋找以不同 ID 重複上傳的相同網格部分和紋理。
- 雖然沒有 API 可以自動檢測資產的相似性,但您可以在您的地方收集所有圖像資產 ID(無論是手動還是通過腳本),下載它們並使用外部比較工具進行比較。
- 對於網格部分,最佳策略是根據大小組織唯一的網格 ID,以手動識別重複項。
- 不要為不同顏色使用不同的紋理,而是上傳單一的紋理並使用 SurfaceAppearance.Color 屬性施加多種色調。
分別導入地圖資產 - 不要一次性導入整個地圖,而是單獨導入和重建地圖中的資產。導入器不會進行任何網格的去重,因此如果您導入一個有很多單獨地板瓷磚的大型地圖,每一個瓷磚都會被導入為單獨的資產(即使它們是重複的)。這可能導致性能和內存使用問題,因為每個網格都被視為個別且佔用內存和繪圖調用。
限制圖像的像素數,不超過必要的數量。除非圖像占據屏幕上的大量物理空間,否則通常最多需要 512x512 像素。大多數小圖像應小於 256x256 像素。
使用修整圖紙,以確保在 3D 地圖中最大化紋理的重用。了解如何創建修整圖紙的步驟和示例,請參見 創建修整圖紙。
您還可以考慮使用精靈圖表將許多小型 UI 圖像作為單個圖像加載。然後,您可以使用 ImageLabel.ImageRectOffset 和 ImageLabel.ImageRectSize 顯示圖表的部分。
加載時間
許多遊戲實現自定義加載屏幕,並使用 ContentProvider:PreloadAsync() 方法請求資產,以便在背景中下載圖像、聲音和網格。
這種方法的優勢在於它允許您確保遊戲的重要部分完全加載而不會出現閃現。然而,常見的錯誤是過度利用這種方法預加載比實際所需更多的資產。
一個壞習慣的例子是加載整個 Workspace。雖然這可能防止紋理在出現時閃現,但它顯著增加了加載時間。
另一種類似的做法是利用 ContentProvider.RequestQueueSize 以確保所有請求的資產都已完成加載。然而,這與症狀相同,顯著增加加載時間,同時由於其波動性,這也是一個不可靠的方法。
相反,只有在必要的情況下使用 ContentProvider:PreloadAsync(),這些情況包括:
- 加載屏幕中的圖像。
- 您遊戲菜單中重要圖像,例如按鈕背景和圖標。
- 起始或重生區域中的重要資產。
如果您必須加載大量資產,我們建議您提供一個 跳過加載 按鈕。