Synchronising client and server

  1. What I Want
    Synchronisation between the client and server when observing moving objects.

  2. 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.

  1. 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)

1 Like

What do I change to make a better system

Case: I have a knife rain in game event, managed by the server. The server creates the knife, and gravity forces it along the collision path. When these events knives are created, the server informs the client of a new detector to look out for. When collided with a player, that player informs the server that the rain object has hit the client (this works fine). If instead of colliding with a player, the object collides with a map part for example, the rain object is to weld, which it does, but with significant latency. This causes the rain object to be welded sometimes far from the actual contact point of the rain object.

What do I dooo

Should I have all collisions detected on the client? Could I set the network ownership of my rain object to the nearest client and have that client manage its collision(s) with NPC parts?

Would there not still be visual latency for the time it would take the client to inform the server of the collision?

Video example of my issue

You may want to close this question, since I think you have found an answer your other thread: