I need to make the pathfinding replicate to the client efficiently, and I want the enemies to die when they’re supposed to. (i’m not very sure what is the issue is exactly)
Just like in clash royale, there are red and blue teams that attack eachother. This is marked on the front face of the green parts.
Issues:
- the pathfinding may be a little broken
- it has trouble replicating to the client after 3+ entities are summoned, and it just cant find the target summoned entity on the client. It’s probably because the server and client don’t sync.
I’ve tried many things, such as making a numeric ID system to help sync the server and client, and
only submitting tables to the client once per unreliableremote event per frame.
Server EntityService
local module = {}
-- RELIANT ON A CUSTOM "NETWORKSERVICE"; IF YOU USE OTHER METHODS FOR NETWORKING THEN PLEASE EDIT
--- utils ---
local RunService = game:GetService('RunService')
-- reliances (feel free to edit) --
local ModuleLoader = require(game:GetService('ReplicatedStorage').ModuleLoader)
local NetworkService = ModuleLoader:GetService('NetworkService')
local Remotes = {
AddEntity = NetworkService:GetUnreliableRemoteEvent('AddEntity'),
RotateEntities = NetworkService:GetUnreliableRemoteEvent('RotateEntities'),
RemoveEntities = NetworkService:GetUnreliableRemoteEvent('RemoveEntities'),
MoveEntities = NetworkService:GetUnreliableRemoteEvent('MoveEntities'),
UpdateEntityHealth = NetworkService:GetUnreliableRemoteEvent('UpdateEntityHealth'),
}
-----------------------------------
local types = require(script.Types)
type Entity = types.Entity
type SummonEntityResult = types.SummonEntityResult
local EntityTemplates = require(script.EntityTemplates)
_G.CurrentEntities = {}
local TotalEntitiesSummoned = 0
-----------------------
local config = {
DefaultEntityTemplate = EntityTemplates.Goblin,
ArenaFloorHeight = .5, -- y
Bounds = Vector3.new(15, 20, 18),
}
--- helpers ---
local function assertwarn(condition: any?, warning: string): boolean
if not condition then
warn(warning)
return false
end
return true
end
local function WithinBounds(Position: Vector3, Bounds: Vector3): boolean
assert(typeof(Position) == 'Vector3', 'Position not given or invalid.')
assert(typeof(Bounds) == 'Vector3', 'Bounds not given or invalid.')
return math.abs(Position.X) < Bounds.X
and math.abs(Position.Y) < Bounds.Y
and math.abs(Position.Z) < Bounds.Z
end
local function IsTableEmpty(Table: {any?}): boolean
for _ in pairs(Table) do
return false
end
return true
end
local function CollideEntities(entity1: Entity, entity2: Entity)
local dist = (entity1.Position - entity2.Position).Magnitude
local minDist = entity1.EntitySizeRadius + entity2.EntitySizeRadius
if dist < minDist and dist > 0 then
local dir = (entity1.Position - entity2.Position).Unit
local overlap = minDist - dist
entity1.Position = entity1.Position + dir * (overlap * 0.5)
entity2.Position = entity2.Position - dir * (overlap * 0.5)
end
end
local function ExecuteEntityBehaviour(deltaTime: number)
local entitiesToMove = {}
local entitiesToEliminate = {}
local entitiesToRotate = {}
local entitiesToUpdateHealth = {}
for i, entity in pairs(_G.CurrentEntities) do
local closestEnemy: Entity? = nil
local closestDist = math.huge
for _, otherEntity in pairs(_G.CurrentEntities) do
if not otherEntity or otherEntity.Health <= 0 or otherEntity.Id == entity.Id then continue end
if otherEntity and otherEntity.Position then
CollideEntities(entity, otherEntity)
if otherEntity.BlueTeam == not entity.BlueTeam then
local dist = (otherEntity.Position - entity.Position).Magnitude
if dist < closestDist then
closestDist = dist
closestEnemy = otherEntity
end
end
end
end
if not entity.Deployed or not closestEnemy then continue end
local direction = closestEnemy.Position - entity.Position
local yRotation = math.atan2(-direction.X, -direction.Z)
entitiesToRotate[entity.Id] = yRotation
if closestDist <= entity.Range then
entity:Attack(closestEnemy)
entitiesToUpdateHealth[closestEnemy.Id] = {CurrentHealth = closestEnemy.Health, MaxHealth = closestEnemy.MaxHealth}
if closestEnemy.Health <= 0 then
local id = closestEnemy.Id
entitiesToEliminate[id] = true
entitiesToUpdateHealth[id] = {CurrentHealth = 0, MaxHealth = closestEnemy.MaxHealth}
end
elseif assertwarn(WithinBounds(entity.Position, config.Bounds), 'Target position out of bounds.') then
entity:Step(direction, closestDist, deltaTime)
entitiesToMove[entity.Id] = entity.Position
end
end
for id, _ in pairs(entitiesToEliminate) do
local entity = _G.CurrentEntities[id]
if entity then
entity:Eliminate()
end
end
-- only uploading if table has values
if not IsTableEmpty(entitiesToMove) then Remotes.MoveEntities:FireAllClients(entitiesToMove) end
if not IsTableEmpty(entitiesToEliminate) then Remotes.RemoveEntities:FireAllClients(entitiesToEliminate) end
if not IsTableEmpty(entitiesToRotate) then Remotes.RotateEntities:FireAllClients(entitiesToRotate) end
if not IsTableEmpty(entitiesToUpdateHealth) then Remotes.UpdateEntityHealth:FireAllClients(entitiesToUpdateHealth) end
end
-----------------
-------------
function module:SummonEntity(EntityName: string, Position: Vector3, BlueTeam: boolean?): (boolean, string? | SummonEntityResult?)
return pcall(function()
assert(typeof(EntityName) == 'string', 'Entity name not given or invalid.')
assert(typeof(Position) == 'Vector3', 'Position not given or invalid.')
assert(WithinBounds(Position, config.Bounds), 'Position is not within bounds.')
local FoundEntity = EntityTemplates[EntityName] or assert('Entity template not found.')
local NewEntity: Entity = setmetatable(table.clone(FoundEntity), getmetatable(FoundEntity))
NewEntity.BlueTeam = BlueTeam ~= nil and BlueTeam or config.DefaultEntityTemplate.BlueTeam
NewEntity.Position = Position + Vector3.new(0, config.ArenaFloorHeight + NewEntity.EntityHeight/2, 0)
TotalEntitiesSummoned += 1
NewEntity.Id = TotalEntitiesSummoned
_G.CurrentEntities[NewEntity.Id] = NewEntity
NewEntity:Deploy()
Remotes.AddEntity:FireAllClients(NewEntity.Name, NewEntity.Position, NewEntity.Id, NewEntity.BlueTeam)
return {Entity = NewEntity, TablePosition = #_G.CurrentEntities}
end)
end
function module:DiscardEntity(TablePosition: number): (boolean, string?)
return pcall(function()
assert(typeof(TablePosition) == 'number', 'Entity table position not given or invalid.')
_G.CurrentEntities[TablePosition] = nil
end)
end
function module:GetAllEntities(): {Entity?}
return _G.CurrentEntities
end
local initizalized = false
function module:Init(): (boolean, string?)
if initizalized then return true end
initizalized = true
task.wait(1)
local debounce = false
return pcall(function()
RunService.Heartbeat:Connect(function(deltaTime)
if debounce then return end
debounce = true
ExecuteEntityBehaviour(deltaTime)
debounce = false
end)
end)
end
return module
Client ClashRoyaleEffectHandler
local module = {}
if game:GetService('RunService'):IsServer() then return {} end
-- utils --
local initizalized = false
local ReplicatedStorage = game:GetService('ReplicatedStorage')
-- reliances (feel free to edit) --
local ModuleLoader = require(ReplicatedStorage.ModuleLoader)
local NetworkService = ModuleLoader:GetService('NetworkService')
local Remotes = {
AddEntity = NetworkService:GetUnreliableRemoteEvent('AddEntity'),
RotateEntities = NetworkService:GetUnreliableRemoteEvent('RotateEntities'),
RemoveEntities = NetworkService:GetUnreliableRemoteEvent('RemoveEntities'),
MoveEntities = NetworkService:GetUnreliableRemoteEvent('MoveEntities'),
UpdateEntityHealth = NetworkService:GetUnreliableRemoteEvent('UpdateEntityHealth'),
}
-------------
-- helpers --
local function getEntityModelByName(EntityName: string): BasePart? | Model?
assert(typeof(EntityName) == 'string', 'Entity name not gven or invalid.')
return ReplicatedStorage.ClashRoyaleEffects.Entities:FindFirstChild(EntityName)
end
local function createFolder(Name: string?, Parent: Instance?): (string?, Instance?) -> Folder
local newFolder = Instance.new('Folder')
newFolder.Name = Name or 'Folder'
newFolder.Parent = Parent or workspace
return newFolder
end
-----------
local ClashRoyaleWorkspace = workspace:WaitForChild('ClashRoyaleWorkspace') or createFolder('ClashRoyaleWorkspace', workspace)
local EffectsFolder = ClashRoyaleWorkspace:WaitForChild('Effects') or createFolder('Effects', ClashRoyaleWorkspace)
local EntitiesFolder = EffectsFolder:WaitForChild('Entities') or createFolder('Entities', EffectsFolder)
---------
local CurrentEntities = {}
function module:Init(): (boolean, string?)
if initizalized then return end
initizalized = true
return pcall(function()
Remotes.AddEntity.OnClientEvent:Connect(function(EntityName: string, Position: Vector3, EntityId: number, BlueTeam: boolean?)
assert(typeof(EntityName) == 'string', 'Entity name not gven or invalid.')
assert(typeof(EntityId) == 'number', 'Entity ID not gven or invalid.')
assert(typeof(Position) == 'Vector3', 'Entity position not gven or invalid.')
local newEntity = getEntityModelByName(EntityName):Clone()
CurrentEntities[EntityId] = newEntity
newEntity.Parent = EntitiesFolder
newEntity.Position = Position
local Color = BlueTeam and Color3.new(0,0,1) or Color3.new(1,0,0)
newEntity.SurfaceGui.Frame.BackgroundColor3 = Color
local HealthBar = ReplicatedStorage.ClashRoyaleEffects.Other:FindFirstChild('HealthBar')
if HealthBar then
local NewBar = HealthBar:Clone()
NewBar.Parent = newEntity
NewBar.Adornee = newEntity
NewBar.Health.BackgroundColor3 = Color
end
end)
Remotes.MoveEntities.OnClientEvent:Connect(function(IdTable: {Vector3})
for EntityId, Position in pairs(IdTable) do
assert(typeof(Position) == 'Vector3', 'Entity position not gven or invalid.')
local entity = CurrentEntities[EntityId]
if entity then
entity.Position = Position
else
warn('Entity with Entity ID: '..tostring(EntityId)..' could not be found.')
end
end
end)
Remotes.RotateEntities.OnClientEvent:Connect(function(IdTable: {number})
for EntityId, YRotation in pairs(IdTable) do
assert(typeof(YRotation) == 'number', 'Entity rotation not gven or invalid.')
local entity: Part = CurrentEntities[EntityId]
if entity then
entity.CFrame = CFrame.new(entity.CFrame.Position) * CFrame.Angles(0, YRotation, 0)
else
warn('Entity with Entity ID: '..tostring(EntityId)..' could not be found.')
end
end
end)
Remotes.RemoveEntities.OnClientEvent:Connect(function(IdTable: {boolean})
for EntityId, boolean in pairs(IdTable) do
local entity: Part = CurrentEntities[EntityId]
if entity and boolean then
entity:Destroy()
else
warn('Entity with Entity ID: '..tostring(EntityId)..' could not be found.')
end
end
end)
Remotes.UpdateEntityHealth.OnClientEvent:Connect(function(IdTable: {{CurrentHealth: number, MaxHealth: number}})
for EntityId, InfoTable in pairs(IdTable) do
assert(typeof(InfoTable.CurrentHealth) == 'number', 'Current health not gven or invalid.')
assert(typeof(InfoTable.MaxHealth) == 'number', 'Max health not gven or invalid.')
local entity: Part = CurrentEntities[EntityId]
if entity then
local HealthBar = entity:FindFirstChild('HealthBar')
if HealthBar then
HealthBar.Enabled = true
HealthBar.Health.Size = UDim2.new(InfoTable.CurrentHealth/InfoTable.MaxHealth, 0, 1)
end
else
warn('Entity with Entity ID: '..tostring(EntityId)..' could not be found.')
end
end
end)
end)
end
return module
If you need any extra information, please let me know.
If you have any tips for me to improve my code, please share it! (All help will be appreciated)