คู่มือนี้สรุปเทคนิคต่างๆ สำหรับการสร้างเกมมัลติเพลเยอร์ที่มีคุณภาพสูงและราบรื่นโดยใช้ โมเดลการอนุญาตของเซิร์ฟเวอร์
การสร้างอินสแตนซ์เชิงพยากรณ์ (instance stitching)
การ เย็บอินสแตนซ์ ช่วยให้สคริปต์ของลูกค้าสามารถสร้าง Instances เชิงพยากรณ์ภายใน RunService:BindToSimulation() callback ลูกค้าสร้าง Instance ทันทีโดยไม่ต้องรอการติดต่อกับเซิร์ฟเวอร์; เมื่อสำเนาที่ถูกต้องของเซิร์ฟเวอร์มาถึง อินสแตนซ์ที่สร้างโดยลูกค้าและสำเนาที่ถูกต้องของเซิร์ฟเวอร์จะถูกรวมเป็นหนึ่ง จากมุมมองของสคริปต์ของคุณ Instance จะมีอยู่ทันทีและสอดคล้องกับเซิร์ฟเวอร์
การเย็บอินสแตนซ์มีประโยชน์ในกรณีที่อินสแตนซ์ต้องมองเห็นและทำงานบนลูกค้าโดยเร็วที่สุด แม้ว่าเซิร์ฟเวอร์จะทำการจำลองอินสแตนซ์ใดๆ ที่ลูกค้าต้องการในที่สุด (พร้อมกับผลกระทบใดๆ ที่พวกเขามีต่อโลก) แต่กระบวนการนี้จะมีความหน่วงที่เกิดจากการสื่อสารกับเซิร์ฟเวอร์เป็นอย่างน้อยหนึ่งรอบ ตัวอย่างประกอบด้วยการยิงอาวุธจรวดและการสร้างข้อจำกัดทางฟิสิกส์ — หากไม่มีการเย็บ อินสแตนซ์ที่ลูกค้าจะเห็นจรวดจะปรากฏออกมาจากที่ไกลหรือมีอาการสั่นเมื่อข้อจำกัดใหม่ถูกจำลองให้กับพวกเขา
พฤติกรรมทางเทคนิค
การเย็บอินสแตนซ์ทำงานโดยการสร้าง GUID ที่เท่ากันระหว่างคลาสลูกค้าและเซิร์ฟเวอร์ GUID จะถูกดึงมาจากสี่ค่า: ประเภทของ Instance ที่ถูกสร้าง อัตลักษณ์ของแหล่งที่มา (ดูข้างล่าง) เฟรมการจำลองปัจจุบัน และตัวนับการเรียกใช้ที่ถูกตั้งค่าใหม่ในแต่ละเฟรม
- สำหรับ Instance.new() — แหล่งที่มาคือสคริปต์เอง (สคริปต์สองตัวที่มีข้อความเหมือนกันถือว่าแตกต่างกัน)
- สำหรับ Instance:Clone() — อินสแตนซ์ที่ถูกโคลนแต่ละตัวจะใช้ GUID ของอินสแตนซ์ต้นฉบับเป็นค่า seed ของบริบท
หากลูกค้าและเซิร์ฟเวอร์มีความเห็นตรงกันเกี่ยวกับค่าเหล่านี้ พวกเขาจะผลิต GUID ที่ตรงกันและการเย็บจะสำเร็จ
การใช้งาน
เพื่อใช้การเย็บอินสแตนซ์ให้เรียก Instance.new(), Instance:Clone(), หรือ Instance.fromExisting() ภายใน BindToSimulation() callback จาก ModuleScript ที่ถูกเรียกใช้งานทั้งบนลูกค้า และ เซิร์ฟเวอร์ ไม่จำเป็นต้องทำอย่างอื่นจากด้านคุณ ระบบจะจัดการการกำหนด GUID และการประสานงานโดยอัตโนมัติ
คุณสามารถตั้งค่าคุณสมบัติที่ไม่ใช่การเข้าถึงการจำลอง เช่น Name, Size, หรือ Parent บนอินสแตนซ์ก่อนที่มันจะถูกใส่เข้าไปใน DataModel
local RunService = game:GetService("RunService")
local Simulation = {}
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local part = Instance.new("Part")
part.Name = "PredictedPart"
part.Size = Vector3.new(2, 2, 2)
part.Parent = workspace -- ส่วนนี้อยู่ในข้อมูลโมเดลแล้ว; การเปลี่ยนแปลงใดๆ ที่ไม่ใช่การเข้าถึงการจำลองจะมีข้อผิดพลาดหลังจากนี้
-- ส่วนนี้มีอยู่ทันทีบนลูกค้าและจะถูกประสานงานกับเซิร์ฟเวอร์
end)
end
return SimulationInstance:Clone() และ Instance.fromExisting() จะมีการเย็บอย่างถูกต้องเมื่ออินสแตนซ์ต้นฉบับถูกจำลองไปยังลูกค้าและเซิร์ฟเวอร์; ทั้งสองด้านจะโคลนจาก GUID ต้นฉบับที่ตรงกันและผลิต GUID เชิงพยากรณ์ที่ตรงกัน
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- อินสแตนซ์ที่ถูกจำลอง
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- โครงสร้างที่ถูกโคลนจะถูกเย็บกับสำเนาที่ถูกต้องของเซิร์ฟเวอร์
end)
end
return Simulationการทำให้ตำแหน่งราบเรียบ
คุณสามารถทำให้ตำแหน่งของวัตถุที่จำลองมีความราบเรียบโดยการแสดงวัตถุที่แตกต่างจากสิ่งที่กำลังถูกจำลอง
- ทำให้วัตถุ ที่จำลอง มองไม่เห็น
- สร้างวัตถุ เรนเดอร์ เป็นโคลนที่ไม่มีมวล ไม่มีการชน โดยมองเพียงอย่างเดียวเพื่อติดตามวัตถุที่จำลอง
- แนบสคริปต์ไปยังวัตถุเรนเดอร์ที่ติดตามตำแหน่งของวัตถุที่จำลองที่มองไม่เห็นอย่างราบเรียบ การแยกการเรนเดอร์และการจำลองนี้ช่วยให้คุณปรับตำแหน่งของวัตถุเรนเดอร์เพื่อสร้างประสบการณ์ที่มองเห็นได้ราบเรียบ
ในสคริปต์ Script ตัวอย่างต่อไปนี้ วัตถุที่เรนเดอร์ติดตามวัตถุที่จำลองอย่างราบเรียบ วัตถุที่เรนเดอร์มักจะ "ตามหลัง" วัตถุที่จำลองซึ่งโดยปกติถือว่าใช้ได้ แต่บางสถานการณ์อาจไม่ต้องการแบบนี้
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- วัตถุที่ติดตามอย่างราบเรียบ
local smoothTarget:BasePart = workspace.SimulatedPart
-- วัตถุที่มองเห็นซึ่งจะถูกทำให้ราบเรียบ
local renderer:BasePart = script.Parent
-- เวลาในการทำให้ราบเรียบ; ยิ่งน้อยจะยิ่งเร็ว
local smoothTime = 0.07
-- เก็บข้อมูลที่จำเป็นในการคำนวณตำแหน่งที่ราบเรียบ
local smoothVelocity = Vector3.new()
-- ปิดฟิสิกส์ของวัตถุเรนเดอร์
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- ติดตามวัตถุเป้าหมายอย่างราบเรียบ
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)เกมตัวอย่าง Soccer ใช้ความหลากหลายของเทคนิคนี้เพื่อเปิดและปิดการทำให้ตำแหน่งราบเรียบของลูกบอลฟุตบอลอย่างชาญฉลาด โดยเฉพาะอย่างยิ่ง ลูกบอลฟุตบอลจะทำให้ตำแหน่งของมันราบเรียบเฉพาะเมื่อบอลที่จำลอง "กระโดด" ห่างออกไปจากลูกบอลที่เรนเดอร์ พฤติกรรมนี้ให้ความได้เปรียบทั้งสองด้าน: ลูกบอลฟุตบอลไม่มีความล่าช้าในการมองเห็นในสภาวะปกติ และเกมจะทำให้ตำแหน่งของมันราบเรียบเฉพาะหลังจากที่ลูกบอลที่จำลองกระโดดไปยังสถานที่ใหม่ ซึ่งมักจะเกิดจากอาร์ติแฟคเครือข่ายหรือการเปลี่ยนแปลงที่ฝ่ายเซิร์ฟเวอร์
การเขียนโค้ดอนิเมชัน
ภายใต้การอนุญาตของเซิร์ฟเวอร์ การจำลองของลูกค้าสามารถมีการ ย้อนกลับและจำลองใหม่ เมื่อเซิร์ฟเวอร์แก้ไขการคาดการณ์ผิด ในระหว่างการย้อนกลับ สถานะอนิเมชันจะถูกย้อนกลับ ซึ่งหมายความว่า AnimationTrack ที่คุณเก็บไว้ในเฟรมก่อนหน้าอาจจะไม่ถูกต้องอีกต่อไป
กระจกตรรกะอนิเมชัน
เช่นเดียวกับตรรกะการเล่นเกมหลักใดๆ ตรรกะในการควบคุมอนิเมชันจะต้องซิงโครไนซ์ระหว่างเซิร์ฟเวอร์และลูกค้า หรือจะมีการคาดการณ์ผิดและพฤติกรรมที่กระตุก ดูการซิงโครไนซ์การจำลอง สำหรับรูปแบบที่เชื่อมโยงฟังก์ชันผ่าน RunService:BindToSimulation() ใน ModuleScript ที่ถูกตั้งค่าในทั้งเซิร์ฟเวอร์และลูกค้า
หลีกเลี่ยงการเก็บข้อมูลแทร็ก
รูปแบบทั่วไปในสคริปต์ที่ไม่ใช้การอนุญาตจากเซิร์ฟเวอร์คือการเก็บ AnimationTrack วัตถุในระยะเวลาเริ่มต้นและนำกลับมาใช้ใหม่ตลอดไป รูปแบบนี้จะล้มเหลวในเกมที่มีการอนุญาตจากเซิร์ฟเวอร์เมื่อเซิร์ฟเวอร์แก้ไขการคาดการณ์ผิดและลูกค้าถอยกลับ/จำลองใหม่การจำลองของพวกเขาด้วยข้อมูลที่ได้รับการแก้ไข หากสคริปต์ของคุณยังถืออ้างอิงถึงแทร็กที่หยุดหรือแทนที่ การเรียกใช้เช่น AdjustWeight() หรือ AdjustSpeed() จะดำเนินการกับแทร็กที่ไม่ถูกต้องในการมองเห็นอีกต่อไป
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
-- เก็บแทร็กอนิเมชัน
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)แทนที่จะเก็บไว้ในอ็อบเจ็กต์แทร็ก ใหเก็บ ID อนิเมชัน (หรือ Animation อินสแตนซ์) และเรียก Animator สำหรับแทร็กที่ถูกนำไปใช้เมื่อคุณต้องการมีปฏิสัมพันธ์กับมัน มี API สองตัวที่สามารถใช้ได้สำหรับเรื่องนี้:
- Animator:GetTrackByAnimationId() — คืนค่าแทร็กที่ใช้งานอยู่สำหรับ ID อนิเมชันเฉพาะ หรือ nil หากไม่มีอนิเมชันที่ใช้งานอยู่ที่มี ID นั้น ใช้เมื่อคุณทราบว่าคุณกำลังมองหาอนิเมชันเฉพาะอะไร
- Animator:GetPlayingAnimationTracks() — คืนค่าทุกแทร็กที่ใช้งานอยู่ (กำลังเล่น, กำลังลดระดับ, หรือหยุดชั่วคราว) ใช้เมื่อคุณต้องการวนซ้ำทุกอย่างที่กำลังทำงานอยู่ (เช่น เพื่อหยุดอนิเมชันทั้งหมดหรือหาคุณสมบัติตามเกณฑ์)
ModuleScript ชื่อ CustomAnimate ใน ReplicatedStorage:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- เก็บการอ้างอิงอนิเมชัน (ไม่ใช่แทร็กที่โหลด)
local animations = {
WalkForward = ReplicatedStorage.Animations.WalkForward,
}
local function getOrLoadTrack(animator: Animator, animation: Animation): AnimationTrack
local track = animator:GetTrackByAnimationId(animation.AnimationId)
if not track then
track = animator:LoadAnimation(animation)
end
return track
end
CustomAnimate.SyncAnimations = function(character)
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
RunService:BindToSimulation(function(dt: number)
local walkTrack = getOrLoadTrack(animator, animations.WalkForward)
if not walkTrack.isPlaying then
walkTrack.Looped = true
walkTrack.Priority = Enum.AnimationPriority.Core
walkTrack:Play()
end
walkTrack:AdjustSpeed(1 + math.cos(time()))
end)
end
return CustomAnimateการเล่นเสียงและเอฟเฟกต์ภาพ
ในการจำลองที่คาดการณ์ได้ เป็นไปได้ที่จะสร้างเอฟเฟกต์หรือเสียงสำหรับเหตุการณ์ที่ลูกค้าคาดการณ์ว่าจะเกิดขึ้นแต่ไม่เกิดขึ้นในเซิร์ฟเวอร์ ระบบการเรนเดอร์ควรพร้อมที่จะ "ยกเลิก" เอฟเฟกต์ที่มีการคาดการณ์ผิด ตัวอย่างเช่น ลูกค้าอาจคาดการณ์ว่าระเบิดมือระเบิดและสร้างเอฟเฟกต์อนุภาค แต่หากผู้เล่นคนอื่นยกเลิกการทำงานของระเบิด ลูกค้าควรซ่อนเอฟเฟกต์อนุภาค
กลยุทธ์ที่ดีสำหรับการเรนเดอร์การจำลองที่คาดการณ์ได้คือการซิงโครไนซ์รูปแบบของเครื่องจักรสถานะภายในลูปการจำลองและทำการเรนเดอร์การเปลี่ยนแปลงสถานะในฟังก์ชันขั้นตอนการเรนเดอร์ ตัวอย่างต่อไปนี้จำลองระเบิดมือด้วยรูปแบบเครื่องจักรสถานะ:
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- กำหนดสถานะระเบิดมือเริ่มต้น
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- เพิ่มเวลาในการนับระเบิดมือ
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- ทำให้ระเบิดมือที่จุดไฟเกิดระเบิด
if grenadeState == module.GrenadeStates.Lit then
if timer >= module.GrenadeExplodeTime then
grenadeState = module.GrenadeStates.Exploded
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
end
end)
end
return moduleเมื่อมีเครื่องจักรสถานะแบบก่อนหน้านี้คุณสามารถทำการเรนเดอร์เอฟเฟกต์ระเบิดมือในการเชื่อมต่อ RunService.RenderStepped ภายในสคริปต์แยกต่างหากตามสถานะระเบิดมือที่ซิงโครไนซ์:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- ไฮไลต์อินสแตนซ์เพื่อแสดงสถานะของระเบิดมือ
local highlight = Instance.new("Highlight")
highlight.Parent = grenade
highlight.FillTransparency = 1
highlight.OutlineTransparency = 1
highlight.DepthMode = Enum.HighlightDepthMode.Occluded
RunService.RenderStepped:Connect(function(deltaTime: number)
local grenadeState = grenade:GetAttribute("State")
local grenadeTimer = grenade:GetAttribute("Timer")
-- ปล่อยอนุภาคเมื่อระเบิดมือจุดไฟ
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- เล่นเสียงระเบิดเมื่อระเบิดมือเพิ่งระเบิด
if previousGrenadeState ~= grenadeState then
if grenadeState == Simulation.GrenadeStates.Exploded and grenadeTimer < 0.2 then
grenade.ExplosionEmitter:Emit(100)
grenade.ExplosionSound:Play()
end
previousGrenadeState = grenadeState
end
-- เปลี่ยนสีไฮไลต์ของระเบิดมือขึ้นอยู่กับสถานะและเวลา
if grenadeState == Simulation.GrenadeStates.Lit then
highlight.FillColor = Color3.fromRGB(255, 0, 0)
highlight.FillTransparency = 1 - (grenadeTimer / Simulation.GrenadeExplodeTime)
elseif grenadeState == Simulation.GrenadeStates.Idle then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Exploded then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Defused then
highlight.FillColor = Color3.fromRGB(0, 255, 125)
highlight.FillTransparency = 0.5
end
end)การออกแบบรอบความล่าช้าของเครือข่าย
กลไกการเล่นบางตัวเหมาะสมกว่ากับการเล่นเกมแบบหลายผู้เล่นที่เชื่อมต่อกันมากกว่ากลไกอื่นๆ ผู้เล่นจะมีความล่าช้าระหว่างเวลาที่ผู้เล่นคนอื่นทำการกระทำและเวลาที่พวกเขาได้รับข้อมูลการเล่นของผู้เล่นคนนั้น วิธีที่ดีที่สุดในการสร้างเกมมัลติเพลเยอร์ที่ราบรื่นคือการออกแบบเกมของคุณโดยคำนึงถึงข้อจำกัดเหล่านี้
ตัวอย่างเช่น เกมที่มีการเร่งความเร็วในการเคลื่อนที่ของผู้เล่นที่ช้ากว่าจะให้ดูราบรื่นกว่าเกมที่มีการเร่งความเร็วสูงกว่าเพราะความแตกต่างในตำแหน่งที่เกิดจากความล่าช้าของเครือข่ายจากการป้อนข้อมูลของผู้เล่นจะน้อยกว่ามากในเกมที่มีการเร่งความเร็วสูง
อีกตัวอย่างหนึ่งคือ กลไกการเล่นที่ผู้เล่นสามารถ กระตุ้น การระเบิดครั้งใหญ่ได้ทันทีโดยการกดปุ่มจะมีอาร์ติแฟคเครือข่ายมากกว่าหากการระเบิดล่าช้าหลังจากป้อนข้อมูล เสมือนกับการจุดชนวน นั่นหมายความว่าการจำลองใหม่จะอยู่ที่เอฟเฟกต์ฟิวส์แทนที่จะเป็นเอฟเฟกต์การระเบิด ซึ่งเป็นอาร์ติแฟคเครือข่ายที่สังเกตได้น้อยกว่า
การคาดการณ์ข้อมูลนำเข้าของผู้เล่นคนอื่น
โดยค่าเริ่มต้น Roblox จะไม่ส่งข้อมูลนำเข้าจากลูกค้าทุกคนไปยังลูกค้าอื่นๆ ว่าจะเหมาะสมกับเกมของคุณหรือไม่นั้นขึ้นอยู่กับการออกแบบเกม:
- สำหรับการเคลื่อนไหวของมนุษย์ขั้นพื้นฐาน พฤติกรรมค่าเริ่มต้นหมายความว่าการเคลื่อนไหวของตัวละครผู้เล่นคนอื่นจะไม่ถูกคาดการณ์จากสถานะเซิร์ฟเวอร์ที่ถูกต้อง ดังนั้น ตัวละครผู้เล่นอื่นจะไม่คาดการณ์ผิดแต่จะถูกเรนเดอร์อยู่ในอดีตเล็กน้อย
- ในเกมแข่งรถ ในทางกลับกัน พฤติกรรมค่าเริ่มต้นหมายความว่าลูกค้าจะไม่ทราบว่าผู้เล่นคนอื่นกำลังใช้การเร่งหรือข้อมูลนำเข้าส่งอื่นๆ ดังนั้นรถอื่นอาจปรากฏอยู่หลังผู้เล่นในท้องถิ่น แม้ว่าจะจริงๆ แล้วพวกเขาอยู่ข้างหน้า เพื่อบรรเทานี้ คุณสามารถเก็บข้อมูลนำเข้าใน attributes บนเซิร์ฟเวอร์และทำงานกับ attributes ที่ซิงโครไนซ์นั้นในฝั่งลูกค้าผ่าน RunService:BindToSimulation() ตามตัวอย่างโค้ดต่อไปนี้และเทมเพลต Racing วิธีนี้ให้คุณใช้ attributes เป็นข้อมูลนำเข้าสำหรับการจำลองของคุณเพื่อให้มีการจำลองข้อมูลของผู้เล่นที่สมบูรณ์แบบ
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local module = {}
module.storePlayerInput = function(player:Player, humanoidRootPart:BasePart)
local inputContext:InputContext = player.PlayerGui.InputContext
local throttle = inputContext.DefuseAction:GetState()
humanoidRootPart:SetAttribute("Throttle", throttle)
-- เขียนข้อมูลนำเข้าอื่นๆ ลงใน attributes...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- ส่งต่อข้อมูลนำจาก เซิร์ฟเวอร์ไปยังลูกค้าทุกคน
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- เขียนข้อมูลนำเข้าสำหรับผู้เล่นในท้องถิ่นเป็น attributes
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- ใช้ attributes เป็นข้อมูลนำเข้าสำหรับเกม
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- นำข้อมูลการเร่งไปใช้กับรถของผู้เล่น
end
end
end)
end)
return moduleการดีบัก
มีเครื่องมือและเทคนิคใหม่ๆ ที่คุณสามารถใช้ในการดีบักเกมที่มีการอนุญาตจากเซิร์ฟเวอร์
เครื่องมือแสดงภาพการอนุญาตจากเซิร์ฟเวอร์
การกด CtrlShiftF6 (Windows) หรือ ⌘ShiftF6 (Mac) จะเปิดเครื่องมือ แสดงภาพการอนุญาตจากเซิร์ฟเวอร์ ของ Studio ซึ่งจะแสดงข้อมูลสำคัญหลายอย่าง:
| รายละเอียด | คำอธิบาย |
|---|---|
| อัตราความสำเร็จในการคาดการณ์อินสแตนซ์ | เปอร์เซ็นต์ของอินสแตนซ์ที่คาดการณ์อย่างถูกต้องในช่วง 8 วินาทีที่ผ่านมา |
| อัตราการยอมรับข้อมูลนำเข้า | เปอร์เซ็นต์ของข้อมูลนำเข้าสำหรับผู้เล่นทั้งหมดที่มาถึงตามเวลาที่เซิร์ฟเวอร์ ข้อมูลนำเข้าใดๆ ที่มาสายจะทำให้ตัวเลขนี้ลดลง |
| ตัวขั้นสูงระหว่างลูกค้าและเซิร์ฟเวอร์ | จำนวนเฟรมระหว่างลูกค้าและเซิร์ฟเวอร์ รวมถึงระยะเวลาที่ลูกเข้าร่วม ตัวเลขนี้แสดงความเสถียรของการเชื่อมต่อของคุณกับเซิร์ฟเวอร์ |
| RCC heartbeat FPS | อัตราเฟรมของการจำลองบนเซิร์ฟเวอร์ หากตัวเลขนี้ลดลงต่ำกว่า 59 เซิร์ฟเวอร์ไม่สามารถติดตามการจำลองได้ และคุณภาพเกมจะลดลง |
| จำนวนอินสแตนซ์ที่คาดการณ์ | จำนวนอินสแตนซ์ที่ลูกค้าของคุณ คาดการณ์ |
| จำนวนเหตุผลที่ข้อมูลนำเข้าสูญหาย | จำนวนครั้งที่เซิร์ฟเวอร์ได้ลบข้อมูลนำเข้าแต่ละเหตุผล:
|
รัศมีการจำลอง
เมื่อพึ่งพาการคาดการณ์อัตโนมัติ (Enum.PredictionMode.Automatic) คุณสามารถแสดงรัศมีการคาดการณ์รอบตัวละครผู้เล่นของคุณได้โดยการเปิดใช้งาน Are Regions Enabled ในการตั้งค่าของ Studio (AltS บน Windows; ⌥S บน Mac) กระบอกสีเขียวจะแสดงช่วงรอบตัวละครของคุณที่มีการคาดการณ์อินสแตนซ์ และรัศมีของมันจะขยายและหดตัวตามลักษณะการทำงานของอุปกรณ์
