-
What I Want
Synchronisation between the client and server when observing moving objects. -
What my problems are
In my hit detection system, I have the player’s character model watch for any touched events. In the context of one use case, a rain object would fall from the sky and if it hit the player, that client would fire the server, informing of the hit and producing a synced affect for clients and servers.
My problem is when the rain objects DO NOT collide with a player. Right now I manage non-player contacts with a simple server sided .Touched event. The problem is that the collision does not synchronise with the clients. This causes a brief moment where after visual contact, the part still falls and can affect my players.
- Code
I’ve already tried to rewrite this system once since separating the tasks between client and server have found this issue.
function HitDetectionRegistry:Register(options)
assert(options.Type, "Detector.Type is neccessary when registering a detector")
local detector = {
Type = options.Type,
Class = options.Class,
ExcludeFromGarbageCollection = options.ExcludeFromGarbageCollection or false,
DriverFunctions = options.DriverFunctions or {},
Args = options.Args or {}
}
if options.Type == "Touched" then
detector.Once = options.Once
detector.HitPartThrottle = options.HitPartThrottle or TOUCHED_HITPART_THROTTLE
-- Sanitize and flatten DetectorParts
local function extractBaseParts(parts)
local baseParts = {}
local function recurse(obj)
if obj:IsA("BasePart") then
table.insert(baseParts, obj)
elseif obj:IsA("Model") or obj:IsA("Folder") then
for _, child in ipairs(obj:GetDescendants()) do
if child:IsA("BasePart") then
table.insert(baseParts, child)
end
end
end
end
for _, item in ipairs(parts) do
recurse(item)
end
return baseParts
end
detector.DetectorParts = extractBaseParts(options.DetectorParts or {})
assert(#detector.DetectorParts > 0, "Touched detector requires at least one BasePart after sanitization")
for _, driverFunc in detector.DriverFunctions do
if HitDriverFunctions[driverFunc] and HitDriverFunctions[driverFunc].context == "Player" then
detector.ClientRegistered = true
break
end
end
local function connectTouched(p)
local conn
conn = p.Touched:Connect(function(hitPart)
local player = Players:GetPlayerFromCharacter(hitPart:FindFirstAncestorOfClass("Model"))
if player and detector.ClientRegistered then
return
end
-- Temporarily disconnect to debounce
conn:Disconnect()
if not detector.Once then
task.delay(TOUCHED_CONNECTION_DEBOUNCE, function()
detector.connection = connectTouched(p)
end)
end
local contactPoint = p.Position:Lerp(hitPart.Position, 0.5)
local validHit = validateHit(nil, p, hitPart, contactPoint)
if validHit then
print("Server actioning DriverFunctions")
ActionDriverFunctions(detector, p, hitPart, contactPoint)
end
end)
return conn
end
detector.Connections = {}
for _, part in ipairs(detector.DetectorParts) do
DetectorLookup[part] = detector
local connection = connectTouched(part)
table.insert(detector.Connections, connection)
end
if detector.ClientRegistered then
DetectorInformer:FireAllClients(detector)
end
...
This is the clients listener script
-- === SERVICES ===
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- +++ HELPERS +++
local Character = require(script.Parent.Character)
local HitValidator = require(script.HitValidator)
-- === CONSTANTS ===
local Player = Players.LocalPlayer
local DetectorInformer = ReplicatedStorage.Remotes.DetectorInformer
local ClientHitDetected = ReplicatedStorage.Remotes.ClientHitDetected
local EVENT_THROTTLE = 0.1
local TOUCHED_HITPART_THROTTLE = 0.2
-- === STATE ===
local ConnectedParts = setmetatable({}, { __mode = "k" })
local TouchedHitpartThrottle = setmetatable({}, { __mode = "k" })
local TriggeredOnceDetectors = setmetatable({}, { __mode = "k" }) -- [detector] = true
local Detectors = {}
local DetectorLookup = {} -- [BasePart] = detector
local function HitDetected(detectorPart, hitPart, contactPoint)
ClientHitDetected:FireServer(detectorPart, hitPart, contactPoint)
end
local function CharacterSetup(character)
local descendants = character:GetDescendants()
for _, instance in descendants do
if instance:IsA("BasePart") and not ConnectedParts[instance] then
ConnectedParts[instance] = true
instance.Touched:Connect(function(hitPart)
if table.find(descendants, hitPart) then return end
-- Determine the detector
local detector = DetectorLookup[hitPart]
if not detector then return end
-- Throttle
local now = tick()
local last = TouchedHitpartThrottle[hitPart]
if last and now - last < TOUCHED_HITPART_THROTTLE then
return
end
TouchedHitpartThrottle[hitPart] = now
-- Handle Once logic
if detector.Once and TriggeredOnceDetectors[detector] then
return
end
-- Mark this detector as triggered if it's a Once detector
if detector.Once then
TriggeredOnceDetectors[detector] = true
end
local contactPoint = instance.Position:Lerp(hitPart.Position, 0.5)
HitDetected(hitPart, instance, contactPoint)
end)
end
end
end
local function SetupDetector(detector, unregister: boolean)
if unregister then
-- Implement the Unregister func
else
for _, part in ipairs(detector.DetectorParts or {}) do
if part:IsA("BasePart") then
DetectorLookup[part] = detector
end
end
table.insert(Detectors, detector)
end
end
local function UnregisterDetector()
end
local function GarbageCollectDetectors()
end
DetectorInformer.OnClientEvent:Connect(SetupDetector)
task.spawn(function()
if Player.Character then
CharacterSetup(Player.Character)
end
end)
Character:RegisterOnAddedHook(CharacterSetup)