Hướng dẫn này phác thảo một số kỹ thuật để sử dụng instance streaming trong trò chơi một cách hiệu quả và hiệu quả. Mặc dù không có giải pháp "một kích thước phù hợp cho tất cả" cho việc thiết kế một trò chơi stream hoặc chuyển đổi một trò chơi không stream thành stream, việc thực hiện những bước cấp cao này sẽ giúp bạn đến gần điều đó hơn.
Thuộc tính stream
Khi StreamingEnabled được bật cho đối tượng Workspace trong Studio, hãy đặt các thuộc tính liên quan của nó thành các giá trị được đề xuất sau:
| Thuộc tính | Đề xuất |
|---|---|
| EnableSLIMAvatars | Sử dụng Enabled để hiển thị avatar rig tiêu chuẩn như là các mô hình thay thế nhẹ và hoạt hình khi phù hợp. Xem thêm về avatar SLIM. |
| ModelStreamingBehavior | Sử dụng Improved để bật stream hiệu quả nhất cho Models có các đối tượng con BasePart. |
| StreamingIntegrityMode | Sử dụng PauseOutsideLoadedArea để cân bằng tính toàn vẹn của gameplay mà không tạm dừng một cách không cần thiết hoặc quá thường xuyên. |
| StreamingMinRadius | Sử dụng giá trị mặc định là 64 để tối đa hóa mức độ game có thể được giảm tải cho các thiết bị hiệu suất thấp. |
| StreamingTargetRadius | Sử dụng giá trị mặc định là 1024 để đạt được sự cân bằng tốt giữa khả năng nhìn thấy của người chơi trên các thiết bị hiệu suất cao và dấu chân bộ nhớ hợp lý. |
| StreamOutBehavior | Sử dụng Opportunistic để cho phép client thu gọn nội dung một cách mạnh mẽ, giảm đáng kể mức sử dụng bộ nhớ và giúp ngăn chặn các sự cố hết bộ nhớ. |
Cấp độ chi tiết mô hình
Model.LevelOfDetail giúp lấp đầy nội dung Model không được stream bằng các mô hình composite hoặc imposter nhẹ, làm cho thế giới trông hoàn chỉnh về mặt hình ảnh. SLIM (Mô hình Tương tác Nhẹ, Tinh vi) composite thực sự hiệu quả, vì người chơi thường không thể phân biệt giữa một mô hình SLIM và mô hình gốc đã được stream vào đầy đủ.
Để có được kết quả tốt nhất:
- Nhóm các phần có liên quan không gian và logic, ví dụ như tất cả các phần của một chiếc xe.
- Đặt LevelOfDetail thành SLIM trên các mô hình chứa các mô hình và phần tĩnh. Các mô hình chứa mô hình skinned, được sửa đổi tại thời gian chạy hoặc phát hoạt ảnh sẽ không được hỗ trợ.
- Giữ giới hạn không gian của mỗi mô hình dưới ~64 studs khối để tăng tính khả thi rằng toàn bộ mô hình thực sự sẽ được stream vào cùng nhau. Nếu một mô hình có kích thước rất lớn, hãy phân chia nó thành các mô hình nhỏ hơn và áp dụng một LevelOfDetail phù hợp cho từng cái.
Cấu trúc mô hình
Ngoài việc thiết lập cấp độ chi tiết mô hình, cấu trúc và cài đặt của Models của bạn có ảnh hưởng đáng kể đến cách thức hoạt động của việc stream. Khi bạn xây dựng hoặc chuyển đổi một trò chơi hiện có:
Sử dụng mô hình nguyên tử để nhóm hợp lý — Khi một script cần truy cập tất cả các phần trong một mô hình, hãy đặt ModelStreamingMode của nó thành Atomic. Điều này cho phép các script phía client truy cập an toàn vào các đối tượng bên trong mô hình mà không phải sử dụng quá nhiều WaitForChild() (mặc dù các script như vậy vẫn phải sử dụng WaitForChild() cho mô hình nguyên tử tổng thể).
Giảm thiểu các mô hình bền vững — Các mô hình bền vững tải sau khi gia nhập và không bao giờ được stream ra, chiếm bộ nhớ vĩnh viễn. Đặt ModelStreamingMode của một mô hình thành Persistent chỉ khi nó phải luôn luôn có sẵn và có thể được truy cập bởi các script.
Phân giải các mô hình chứa — Một mẫu không streaming phổ biến là một Model khổng lồ chứa nhiều NPC, props hoặc các nhóm tương tự. Dưới sự stream, các mô hình chứa giảm hiệu suất của việc stream và chúng không tối ưu cho cấp độ chi tiết mô hình hoạt động tốt nhất với các đối tượng được nhóm lại gần nhau. Hãy phân giải các mô hình chứa thành các mô hình nhỏ hơn với các phần vật lý gần nhau hoặc liên quan về mặt logic.
Avatar SLIM
Các avatar trên nền tảng nằm ngoài khu vực hiện đang được stream sẽ không hiển thị theo mặc định, nhưng việc bật Workspace.EnableSLIMAvatars sẽ trình diễn các avatar rig tiêu chuẩn như các mô hình thay thế nhẹ và hoạt hình khi phù hợp. Thực sự thì, engine:
- Hiển thị một phiên bản SLIM khi mô hình avatar thực sự được stream ra.
- Hoán đổi giữa SLIM/thực tế bằng cách sử dụng tài nguyên có sẵn bên trong bán kính stream, vì các mô hình SLIM có thể ít tốn kém hơn nhiều để hiển thị hơn so với mô hình độ phân giải đầy đủ.
- Điều chỉnh tốc độ hoạt hình SLIM dựa trên tầm quan trọng của cảnh và băng thông.
Hệ thống cấp độ chi tiết của avatar có những tối ưu hóa cụ thể cho các avatar. Dưới đây mô tả những gì sẽ hoặc sẽ không được xử lý bởi SLIM:
Mẫu script
Các mẫu script sau đây thường bị ảnh hưởng bởi việc stream. Chiến lược chính xác phụ thuộc vào ý định của mã, vì vậy mỗi mẫu liệt kê nhiều tùy chọn khi cần thiết.
Chỉ mục trực tiếp vào các đối tượng con
Chỉ mục vào các đối tượng con của Workspace với toán tử . sẽ xảy ra lỗi nếu bất kỳ đối tượng nào trong đường dẫn không được stream vào. Điều này cũng áp dụng cho FindFirstChild(), FindFirstChildWhichIsA() và FindFirstChildOfClass() mà trả về nil nếu đứa trẻ chưa được stream vào.
Tìm kiếm Đối tượng Conlocal house1 = workspace:FindFirstChild("House1") -- nil nếu "House1" chưa được stream vàolocal door = workspace.House1.Door -- Lỗi nếu "House1" hoặc "Door" chưa được stream vào
Một mẫu tương tự là truy cập Humanoid hoặc các đối tượng con khác của nhân vật trực tiếp bên trong một kết nối Player.CharacterAdded. Dưới việc stream, mô hình nhân vật được gán cho Workspace trước khi tất cả các đối tượng con của nó được sao chép, do đó việc chỉ mục trực tiếp sẽ thất bại.
Đối tượng Con của Nhân vật
local Players = game:GetService("Players")
local player = Players.LocalPlayer
player.CharacterAdded:Connect(function(character)
local humanoid = character.Humanoid
end)
Nếu script không thể tiến hành mà không có một đối tượng, hãy đợi nó với WaitForChild():
Tìm kiếm Đối tượng Conlocal house1 = workspace:WaitForChild("House1")local door = house1:WaitForChild("Door")
Các đối tượng được gửi từ xa
Một tín hiệu RemoteEvent/RemoteFunction và đối tượng mà nó tham chiếu di chuyển độc lập, do đó tín hiệu có thể đến client trước khi đối tượng có mặt — hoặc đối tượng có thể không bao giờ có mặt. Hai nguyên nhân khả thi bao gồm:
Dưới việc stream, có thể có một chút trễ giữa lúc một phần/mô hình được tạo trên server và khi nó được sao chép cho các client. Thực tế, một phần được tham chiếu bởi một RemoteEvent/RemoteFunction có thể đơn giản là chưa tồn tại, ngay cả trong một khu vực đã được stream.
Việc gửi một tham chiếu phần/mô hình từ server đến client thông qua một RemoteEvent hoặc RemoteFunction yêu cầu rằng đối tượng phải được sao chép cho client nhận. Gửi một đường dẫn đối tượng dưới dạng chuỗi có cùng vấn đề, vì đường dẫn có thể giải quyết đến một vị trí không tồn tại trên client:
Script Clientlocal ReplicatedStorage = game:GetService("ReplicatedStorage")local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")remoteEvent.OnClientEvent:Connect(function(data)local checkpoint = data.checkpoint -- Lỗi nếu "checkpoint" chưa được stream vàolocal level = workspace.Levels[data.levelPath] -- Lỗi nếu đường dẫn chưa được stream vàoend)
Nếu script trên client nhận đối tượng cần tiến hành, hãy bao gồm WaitForChild() trước khi sử dụng nó. Lưu ý rằng điều này có thể chờ vô thời hạn nếu đối tượng không bao giờ được stream vào, vì vậy hãy xem xét việc thêm một thời gian chờ là tham số thứ hai của WaitForChild().
Sự không đồng bộ của Client
Sự không đồng bộ phía client nên được coi là một ngoại lệ, không phải là một mẫu thiết kế tiêu chuẩn. Việc giới thiệu các bản sao chỉ dành cho client hoặc thay đổi lại các đối tượng một cách nội bộ có thể tạo ra các vấn đề nghiêm trọng. Kiểm tra mã của bạn cho những nơi mà dựa vào những thay đổi loại này sẽ duy trì trên client.
Ví dụ, việc thay đổi lại một đối tượng một cách nội bộ từ ReplicatedStorage vào Workspace có thể khiến đối tượng đó đủ điều kiện để được stream ra. Tương tự, việc sao chép một đối tượng một cách nội bộ (Instance:Clone()) từ ReplicatedStorage vào Workspace tạo một bản sao chỉ dành cho client mà không còn thuộc về kết nối sao chép server và sẽ không nhận các bản cập nhật thuộc tính từ đối tượng gốc do server sở hữu.
Khái niệm tương tự áp dụng khi gọi Instance:Destroy() trên client cho một đối tượng do server sở hữu. Điều này loại bỏ đối tượng một cách nội bộ nhưng server vẫn còn có nó, do đó nó sẽ được stream vào một lần nữa với trạng thái ban đầu khi đủ điều kiện.
Stream chủ động
Khi điểm đến tiếp theo của người chơi có thể được dự đoán, hãy thực hiện các lệnh gọi phía server đến Player:RequestStreamAroundAsync() để stream các khu vực tạm thời cho việc tải tạm thời, hoặc sử dụng Player:AddReplicationFocus() trên một cơ sở hạn chế cho các khu vực cần duy trì tải đến khi được giải phóng một cách rõ ràng.
Ví dụ, khi một nhân vật người chơi sắp được dịch chuyển bằng cách thay đổi CFrame đến một ngôi nhà của người chơi khác ở một vị trí xa, bạn có thể tiền lấy khu vực đích để giảm thiểu hiệu ứng nổi bật và cung cấp một quá trình chuyển tiếp mượt mà hơn. Script sau đây cho thấy cách một sự kiện từ xa từ client đến server có thể được phát để di chuyển nhân vật người chơi bằng cách sử dụng phương pháp tiền lấy. Nếu yêu cầu tiền lấy thành công khi hàm trả về, bán kính tối thiểu xung quanh vị trí mục tiêu sẽ có mặt trên client.
Script Server - Dịch chuyển Nhân vật Người chơi
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local teleportEvent = ReplicatedStorage:WaitForChild("TeleportEvent")
local function teleportPlayer(player, teleportTarget)
-- Yêu cầu stream xung quanh vị trí mục tiêu
player:RequestStreamAroundAsync(teleportTarget)
-- Dịch chuyển nhân vật
local character = player.Character
if character and character.Parent then
local currentPivot = character:GetPivot()
character:PivotTo(currentPivot * CFrame.new(teleportTarget))
end
end
-- Gọi hàm dịch chuyển khi client phát sự kiện từ xa
teleportEvent.OnServerEvent:Connect(teleportPlayer)
Đọc thuộc tính đối tượng
Khi một đối tượng được stream ra, các cập nhật thuộc tính của nó không còn được sao chép cho client đó nữa. Đọc các thuộc tính như BasePart.Position tiếp tục thành công nhưng trả về giá trị đã sao chép cuối cùng có thể là lỗi thời.
local Players = game:GetService("Players")local player = Players.LocalPlayer-- Vị trí có thể lỗi thời nếu "target" đã được stream ralocal dist = (target.Position - player.Character.HumanoidRootPart.Position).Magnitude
Chuyển logic sang server, vì các script phía server luôn thấy tất cả các đối tượng. Đây thường là tùy chọn đáng tin cậy nhất cho các kiểm tra khoảng cách và các logic nhạy cảm với vị trí khác.
Chờ trên đường đi quan trọng
Một số trò chơi không stream tải bản đồ của chúng bằng cách sao chép nó từ ReplicatedStorage vào Workspace, sau đó chờ nó ở phía client trước khi tắt màn hình tải và chỉ báo sự sẵn sàng. Dưới việc stream điều này kéo dài vô thời hạn — nhân vật của client chưa được sinh ra, do đó không có tập trung sao chép, và bản đồ không gian không bao giờ được stream vào.
Di chuyển logic màn hình tải để nó không phụ thuộc vào một thực thể không gian cụ thể có mặt, ví dụ bằng cách báo hiệu sự sẵn sàng khi nhân vật đã được sinh ra và khu vực xung quanh ngay lập tức đã được stream vào.
Xử lý thay đổi tín hiệu
Các tín hiệu như Instance.ChildAdded/Instance.ChildRemoved và các tín hiệu của CollectionService như GetInstanceAddedSignal() hoặc GetInstanceRemovedSignal() cũng sẽ phát ra khi stream vào/ra, không thể phân biệt với các lần sinh/tháo thực tế. Các script nhận không thể phân biệt khác biệt từ tín hiệu đó mà không cần logic cho rằng một tín hiệu tương ứng với một sự kiện "thực", điều cần được cập nhật.
Kiểm tra các script cho bất kỳ trình nghe tín hiệu nào có thể bị lỗi hoặc thay đổi đáng kể khi được kích hoạt bởi sự kiện stream vào và/hoặc stream ra. Ví dụ, nếu bạn phát âm thanh hoặc hiệu ứng hình ảnh khi một NPC kẻ thù lần đầu tiên xuất hiện vào thế giới, hãy gán mỗi kẻ thù một thuộc tính như Spawned vào lần sinh đầu tiên, và bỏ qua việc phát lại cùng một âm thanh/hiệu ứng trong các lần stream vào sau này của kẻ thù.
Theo dõi Thuộc tính
local CollectionService = game:GetService("CollectionService")
local TAG_NAME = "Enemy"
CollectionService:GetInstanceAddedSignal(TAG_NAME):Connect(function(enemy)
if not enemy:GetAttribute("Spawned") then
-- Đặt thuộc tính "Spawned" trên kẻ thù cho lần sinh đầu tiên
enemy:SetAttribute("Spawned", true)
-- Phát âm thanh/hiệu ứng hình ảnh cho lần sinh đầu tiên này
playSpawnEffects(enemy)
end
end)
Lặp qua các bộ sưu tập
Các lần lặp qua bộ sưu tập phía client như Instance:GetChildren() và Instance:GetDescendants() chỉ trả về tập hợp con đã được stream vào của các đối tượng con. Điều này áp dụng ngay cả khi đối tượng cha luôn được sao chép, chẳng hạn như một Folder ngay dưới Workspace mà các đối tượng con không gian của nó sẽ stream vào và ra.
local Players = game:GetService("Players")local player = Players.LocalPlayer-- Thư mục "Homes" luôn được sao chép nhưng các đối tượng con của nó stream vào và ra-- Vòng lặp này có thể bỏ lỡ những ngôi nhà không hiện đang được stream vàofor _, home in workspace.Homes:GetChildren() doif home.Settings.Owner.Value == player.Name thenreturn homeendend
Nếu cần thiết phải thực hiện một liệt kê hoàn chỉnh, hãy quét trên server và chuyển kết quả cho người chơi thông qua một RemoteEvent nếu cần.
Các truy vấn không gian
Các truy vấn không gian phía client như WorldRoot:Raycast(), WorldRoot:GetPartBoundsInBox(), và Model:GetBoundingBox() chỉ phản ánh nội dung được stream vào. Có phải điều này là vấn đề hay không phụ thuộc vào những gì truy vấn được sử dụng cho.
Sử dụng server cho các truy vấn mà kết quả phải phản ánh toàn bộ thế giới, ví dụ như một raycast kiểm tra xem người chơi có nhìn thấy mục tiêu xa hay không.
Các mẫu khác
Các mẫu sau đây cũng có thể áp dụng và nên được xem xét cẩn thận:
Một Sound hoặc AudioPlayer được gán cho một đối tượng 3D sẽ dừng lại khi đối tượng đó được stream ra. Đối với âm thanh môi trường nên tồn tại bất kể việc streaming, hãy gán phần phát âm thanh vào một mô hình bền vững hoặc một bộ chứa không stream.
Các đối tượng UI trong trò chơi như BillboardGui hoặc SurfaceGui cũng như các hiệu ứng hình ảnh như Beams hoặc Highlights có adornee hoặc kết nối stream ra sẽ dừng việc hiển thị. Điều này có thể là hành vi mong muốn, nhưng bạn nên xác minh điều đó.
Các sự kiện BasePart.Touched, ProximityPrompts, DragDetectors, và ClickDetectors sẽ không hoạt động với các người chơi mà client của họ không có phần/mô hình tương ứng được stream vào. Nếu tương tác phải có khả năng từ bất kỳ khoảng cách nào, mô hình cần phải được bền vững hoặc tương tác cần một cơ chế khác.
Đối với PathfindingService và việc tìm đường phía client, trình tìm đường chỉ thấy hình học đã được stream vào trên client và nó có thể đi qua các chướng ngại vật tồn tại trên server. Xem ở đây để tìm hiểu về các chiến lược.
Điều kiện kiểm tra thực tế
Khi các script được cập nhật, hãy kiểm tra kỹ lưỡng trò chơi. Các lỗi streaming thường chỉ xuất hiện ở các rìa của khu vực đã được stream hoặc trong quá trình chuyển tiếp, vì vậy chỉ kiểm tra gần khu vực sinh hoặc bán kính mục tiêu sẽ không đủ.
Kiểm tra với Workspace.StreamingTargetRadius được đặt thành giá trị tối thiểu (64). Một số lỗi streaming chỉ xuất hiện khi khu vực được stream nhỏ.
Chơi qua đầy đủ các mẫu di chuyển của trò chơi, teleport giữa các khu vực xa và quay lại các khu vực sau khi rời khỏi chúng. Đây là những tình huống sẽ yêu cầu stream vào và stream ra nhiều nhất.
Sử dụng overlay debug streaming để theo dõi các cài đặt streaming hiện tại, các khu vực đã tải và trạng thái streaming thời gian thực.
Theo dõi cửa sổ Output và Developer Console để tìm lỗi, vì nhiều mẫu script tạo ra lỗi thay vì hành vi im lặng không chính xác. Hãy đặc biệt chú ý đến các lỗi có dạng attempt to index nil with ... thường chỉ ra một cuộc gọi WaitForChild() bị thiếu.
Trang bị và kích hoạt Tools, bắn vũ khí và kích hoạt các tương tác khác trong trò chơi.

