คู่มือนี้สรุปเทคนิคต่างๆ สำหรับการสร้างเกมมัลติเพลเยอร์ที่มีคุณภาพสูงและราบรื่นโดยใช้ โมเดลการควบคุมเซิร์ฟเวอร์
การสร้างอินสแตนซ์ที่พยากรณ์ (instance stitching)
การ ติดตั้งอินสแตนซ์ ช่วยให้สคริปต์ของไคลเอนต์สามารถสร้าง Instances โดยคาดการณ์ภายในการเรียกคืนของ RunService:BindToSimulation() ไคลเอนต์จะสร้าง Instance ขึ้นทันทีโดยไม่ต้องรอเซิร์ฟเวอร์รอบ‑ทริป; เมื่อลอกลอกสำเนาที่มีอำนาจของเซิร์ฟเวอร์เข้ามา อินสแตนซ์ที่สร้างขึ้นโดยไคลเอนต์และสำเนาที่มีอำนาจของเซิร์ฟเวอร์จะถูกรวมเป็นหนึ่งเดียว จากมุมมองของสคริปต์ของคุณ Instance จะมีอยู่ทันทีและสอดคล้องกับเซิร์ฟเวอร์
การติดตั้งอินสแตนซ์มีความจำเป็นในกรณีที่อินสแตนซ์ต้องมองเห็นและสามารถทำงานบนไคลเอนต์ได้โดยเร็วที่สุด ในขณะที่เซิร์ฟเวอร์จะส่งซ้ำอินสแตนซ์ที่ไคลเอนต์ต้องการ (พร้อมกับผลกระทบใด ๆ ที่พวกเขามีต่อโลก) กระบวนการนี้จะมีค่าล่าช้ารอบนึงเนื่องจากการสื่อสารกับเซิร์ฟเวอร์ ตัวอย่างเช่นการยิงอาวุธจรวดและสร้างข้อจำกัดทางฟิสิกส์ — โดยไม่ต้องติดตั้ง อินสแตนซ์ที่สร้างขึ้นจะปรากฏให้ไคลเอนต์เห็นจากระยะไกล หรืออาจเกิดการสั่นเมื่อข้อจำกัดใหม่ถูกส่งซ้ำไปยังพวกเขา
พฤติกรรมทางเทคนิค
การติดตั้งอินสแตนซ์ทำงานโดยการสร้าง GUID ที่เหมือนกันที่ทั้งไคลเอนต์และเซิร์ฟเวอร์ GUID จะถูกดึงมาจากข้อมูลสี่ชิ้น: ประเภทของ Instance ที่ถูกสร้าง เอกลักษณ์ของแหล่งที่มา (ดูด้านล่าง) เฟรมจำลองปัจจุบัน และเคาน์เตอร์การเรียกต่อสคริปต์ต่อหนึ่งเฟรมที่รีเซ็ตทุกเฟรม
- สำหรับ Instance.new() — แหล่งมาจากสคริปต์นั้นเอง (สองสคริปต์ที่มีข้อความเดียวกันจะถือว่าแตกต่างกัน)
- สำหรับ Instance:Clone() — อินสแตนซ์ที่ถูกคัดลอกแต่ละอันจะใช้ GUID ของอินสแตนซ์ต้นแบบเป็นต้นแบบในการสร้าง
หากไคลเอนต์และเซิร์ฟเวอร์เห็นด้วยในข้อมูลนำเข้า มันจะสร้าง GUID ที่ตรงกันและการติดตั้งจะสำเร็จ
การใช้งาน
ในการใช้การติดตั้งอินสแตนซ์ ให้เรียกใช้ Instance.new(), Instance:Clone() หรือ Instance.fromExisting() ภายในการเรียกคืน BindToSimulation() จาก ModuleScript ที่เรียกใช้ทั้งบนไคลเอนต์ และ เซิร์ฟเวอร์ ก็ไม่มีอะไรที่คุณต้องทำเพิ่มเติมอีกแล้ว; ระบบจะจัดการกับการตั้งค่า GUID และการปรับให้เข้ากันโดยอัตโนมัติ
คุณสามารถตั้งค่าคุณสมบัติที่ไม่ใช่ สิทธิ์การจำลอง ได้อย่างอิสระ เช่น Name, Size, หรือ Parent บนอินสแตนซ์ก่อนที่มันจะถูกรวมเป็นลำดับใน DataModel
การจำลอง (ModuleScript) - สร้างอินสแตนซ์ใน Callback ของ BindToSimulation()
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 -- ส่วนนี้อยู่ใน DataModel; การเปลี่ยนแปลงที่ไม่ใช่สิทธิ์การจำลองจะผิดพลาดหลังจากนี้
-- ส่วนนี้จะมีอยู่ทันทีบนไคลเอนต์และจะต้องปรับให้เข้ากับเซิร์ฟเวอร์
end)
end
return Simulation
Instance:Clone() และ Instance.fromExisting() ติดตั้งได้ถูกต้องเมื่ออินสแตนซ์ต้นแบบถูกส่งซ้ำไปยังทั้งไคลเอนต์และเซิร์ฟเวอร์; ทั้งสองด้านทำการคัดลอกจาก GUID ต้นแบบที่ตรงกันและสร้าง GUID ที่คาดการณ์ตรงกัน
การจำลอง (ModuleScript) - คัดลอกอินสแตนซ์ใน Callback ของ BindToSimulation()
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 ด้านล่าง วัตถุที่เลือกไว้ (พ่อแม่) จะติดตามวัตถุที่จำลองอย่างราบรื่น วัตถุที่เลือกไว้จะอยู่ "ด้านหลัง" วัตถุที่จำลองอยู่เล็กน้อยซึ่งเป็นปกติแต่ในบางกรณีอาจไม่ต้องการ
ติดตามตำแหน่ง BasePart อย่างราบรื่นด้วย Renderer Part
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- วัตถุที่จะติดตามอย่างราบรื่น
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)
แทนที่จะถือว่าแทร็กวัตถุ ให้เก็บ ไอดีอนิเมชัน (หรือ Animation อินสแตนซ์) และถาม Animator สำหรับแทร็กที่มีชีวิตอยู่เมื่อใดก็ตามที่คุณต้องการมีส่วนร่วมด้วย Two API ที่สามารถใช้ได้สำหรับสิ่งนี้:
- Animator:GetTrackByAnimationId() — คืนแทร็กที่ใช้งานอยู่ในปัจจุบันสำหรับไอดีอนิเมชันเฉพาะ หรือ nil หากไม่มีอนิเมชันที่ใช้งานอยู่ด้วยไอดีนี้ ใช้เมื่อคุณรู้ว่าอนิเมชันเฉพาะที่คุณต้องการ
- Animator:GetPlayingAnimationTracks() — คืนแทร็กทั้งหมดที่ใช้งานอยู่ (กำลังเล่น, กำลัง fade out หรือหยุดพัก) ใช้เมื่อคุณต้องการวนซ้ำไปยังทุกอย่างที่ใช้งานอยู่ (ตัวอย่างเช่นเพื่อหยุดการเล่นอนิเมชันทั้งหมดหรือค้นหาแทร็กตามเกณฑ์บางอย่าง)
ModuleScript ชื่อ CustomAnimate ใน ReplicatedStorage:
CustomAnimate
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
การเล่นเสียงและเอฟเฟกต์ภาพ
ในจำลองที่คาดการณ์ได้ อาจเกิดขึ้นได้ที่จะเรียกใช้งานเอฟเฟกต์หรือเสียงสำหรับเหตุการณ์ที่ไคลเอนต์คาดการณ์ว่าจะเกิดขึ้น แต่ไม่เกิดขึ้นจริงในเซิร์ฟเวอร์ ระบบเรนเดอร์ควรเตรียมที่จะ "ยกเลิก" เอฟเฟกต์ที่คาดการณ์ที่ผิดพลาด ตัวอย่างเช่น ไคลเอนต์อาจคาดการณ์ว่ามีการระเบิดของระเบิดมือและเรียกใช้อีฟเฟกต์อนุภาค แต่ถ้าผู้เล่นคนอื่นดีเฟสระเบิดมือ ไคลเอนต์จะต้องซ่อนเอฟเฟกต์อนุภาค
กลยุทธ์ที่ดีสำหรับการเรนเดอร์จำลองที่คาดการณ์ได้คือการซิงค์รูปแบบของสถานะ machine ภายในวงจรการจำลองและทำการเรนเดอร์การเปลี่ยนแปลงในฟังก์ชั่นการเรนเดอร์ ตัวอย่างด้านล่างจำลองระเบิดมือด้วยรูปแบบสถานะแมชชีน:
เครื่องจักรสถานะง่ายๆ для ติดตามระเบิดมือ (ModuleScript)
local Workspace = game:GetService("Workspace")
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)
การออกแบบตาม ความล่าช้า ของเครือข่าย
กลไกการเล่นบางอย่างทำงานได้ดีกว่าในการเล่นมัลติเพลเยอร์แบบเครือข่ายPlayers จะมีความล่าช้าระหว่างการกระทำของผู้เล่นคนอื่นและเมื่อพวกเขาได้รับการป้อนข้อมูลจากผู้เล่นนั้น วิธีที่ดีที่สุดในการสร้างเกมมัลติเพลเยอร์ที่ราบรื่นมากคือการออกแบบเกมของคุณโดยคำนึงถึงข้อจำกัดเหล่านี้
ตัวอย่างเช่น เกมที่มีการเร่งความเร็วที่ช้ากว่าการเคลื่อนที่ของผู้เล่นจะดูราบรื่นกว่าหนึ่งที่มีการเร่งที่สูงกว่าเพราะความแตกต่างในตำแหน่งที่เกิดจากความล่าช้าของการป้อนข้อมูลของผู้เล่นจะน้อยกว่ามากในเกมที่มีการเร่งมุมสูงกว่า
อีกตัวอย่างหนึ่ง กลไกการเล่นที่ผู้เล่นสามารถ กระตุ้น การระเบิดใหญ่ได้ทันทีด้วยการกดปุ่มจะมีอาร์ติแฟคจากเครือข่ายมากกว่าหากการระเบิดนั้นถูกหน่วงหลังจากการป้อนข้อมูล เหมือนกับการจุดชนวน สิ่งนี้ทำให้การจำลองใหม่บนเอฟเฟกต์ชนวนแทนที่จะเป็นเอฟเฟกต์การระเบิดซึ่งเป็นอาร์ติแฟคทางเครือข่ายที่สังเกตได้น้อยกว่า
การคาดการณ์การป้อนข้อมูลของผู้เล่นอื่น
ตามค่าเริ่มต้น Roblox จะไม่ส่งต่อการป้อนข้อมูลจากแต่ละไคลเอนต์ไปยังไคลเอนต์อื่น ๆ ขึ้นอยู่กับการออกแบบของเกมของคุณว่าจะถูกต้องหรือไม่:
- สำหรับการเคลื่อนที่ของมนุษย์ขั้นพื้นฐาน พฤติกรรมเริ่มต้นหมายความว่าการเคลื่อนที่ของตัวละครผู้เล่นคนอื่นจะไม่ได้รับการคาดการณ์จากสถานะเซิร์ฟเวอร์ที่มีอำนาจ และผลลัพธ์คือตัวละครผู้เล่นอื่นจะไม่พยากรณ์ผิด แต่จะเรนเดอร์ในอดีตเล็กน้อย
- ในเกมแข่งรถ ในทางตรงข้าม พฤติกรรมเริ่มต้นหมายความว่าไคลเอนต์จะไม่ทราบว่าผู้เล่นคนอื่น ๆ กดปุ่มหรือป้อนข้อมูลอื่น ๆ ทำให้รถอื่น ๆ ดูเหมือนอยู่หลังผู้เล่นท้องถิ่นแม้ว่าพวกเขาจะอยู่ข้างหน้า เพื่อบรรเทาสิ่งนี้ คุณสามารถเก็บข้อมูลการป้อนข้อมูลของผู้เล่นใน คุณลักษณะ ในเซิร์ฟเวอร์และดำเนินการกับคุณลักษณะที่ซิงค์ไปยังด้านไคลเอนต์โดยใช้ RunService:BindToSimulation() ตามที่แสดงในตัวอย่างโค้ดด้านล่างและแม่แบบ Racing โดยวิธีนี้คุณจะใช้คุณลักษณะต่างๆ เพื่อป้อนข้อมูลในการจำลองของคุณเพื่อให้การป้อนข้อมูลจากผู้เล่นถูกจำลองทุกอย่าง
การเก็บข้อมูลผู้เล่นในคุณลักษณะ (ModuleScript)
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)
-- เขียนข้อมูลการป้อนข้อมูลอื่น ๆ ลงในคุณลักษณะ...
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
-- เขียนการป้อนข้อมูลของผู้เล่นท้องถิ่นเป็นคุณลักษณะ
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- ใช้คุณลักษณะต่างๆ เป็นข้อมูลนำเข้าของเกม
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 ซึ่งจะแสดงข้อมูลสำคัญหลายประการ:
| รายละเอียด | คำอธิบาย |
|---|---|
| อัตราความสำเร็จในการคาดการณ์อินสแตนซ์ | เปอร์เซ็นต์ของอินสแตนซ์ที่คาดการณ์ถูกต้องในช่วงแปดวินาทีที่ผ่านมา |
| อัตราการยอมรับของการป้อนข้อมูล | เปอร์เซ็นต์ของการป้อนข้อมูลของผู้เล่นทั้งหมดที่มาถึงทันเวลาในเซิร์ฟเวอร์ การป้อนข้อมูลที่นานเกินไปจะลดตัวเลขนี้ |
| ช่วงเวลาระหว่างไคลเอนต์และเซิร์ฟเวอร์ | จำนวนเฟรมระหว่างไคลเอนต์และเซิร์ฟเวอร์ รวมถึงเวลาที่เข้าร่วมของไคลเอนต์เสถียรภาพของตัวเลขนี้แสดงถึงเสถียรภาพของการเชื่อมต่อของคุณกับเซิร์ฟเวอร์ |
| RCC เร็ว | อัตราเฟรมของการจำลองในเซิร์ฟเวอร์ หากตัวเลขนี้ลดต่ำกว่า 59 เซิร์ฟเวอร์จะไม่สามารถรักษาจังหวะของการจำลองได้และเกมจะตกต่ำลงในคุณภาพ |
| จำนวนอินสแตนซ์ที่คาดการณ์ได้ | จำนวนอินสแตนซ์ที่ไคลเอนต์ของคุณ đang คาดการณ์ |
| การนับเหตุผลในการยกเลิกการป้อนข้อมูล | จำนวนครั้งที่เซิร์ฟเวอร์ได้ยกเลิกการป้อนข้อมูลสำหรับแต่ละเหตุผล:
|
รัศมีการจำลอง
เมื่อใช้การคาดการณ์โดยอัตโนมัติ (Enum.PredictionMode.Automatic) คุณสามารถมองเห็นรัศมีการคาดการณ์ที่อยู่รอบตัวละครผู้เล่นของคุณได้โดยเปิดใช้งาน Are Regions Enabled ในการตั้งค่าของ Studio (AltS บน Windows; ⌥S บน Mac) กระบอกสีเขียวจะแสดงช่วงรอบตัวละครของคุณที่อินสแตนซ์ถูกคาดการณ์ และรัศมีจะเปลี่ยนขนาดขึ้นลงตามลักษณะการแสดงผลของอุปกรณ์
