In my server script, I have the enemies stats which contains their position, speed, health etc in a table which is being sent to the client every 10 heartbeats or 10 times a second after I fire a remote event (the position is being tweened on the server).
However, on the client script, how would I identify if a mob is newly spawned and not an already existing mob, and how would I assign a model to that specific enemy if it is newly spawned. I also don’t know how I would keep track of all of the enemy models in a way that doesn’t get confused for another enemy of the exact same type/name.
I tried using CollectionService but it was not very helpful
If you need something explaining a bit better please ask, thank you.
the way i did this in my tower defense game was pretty straight-forward:
every time an enemy is created server-sided, the enemy has its own unique id
(you can accomplish this using HttpService’s :GenerateGUID(), or whatever method you want if you were to randomly generate an ID for your enemy)
if you wanted to know how i did my enemy IDs, i just did "[enemyName](space)[enemyID]"
once you’re done creating the id, store it into the enemy’s stats and send it to all clients 10 times a second (like what you were doing in the first paragraph).
on the client-side, i have a table that stores all enemies like this:
local activeEnemies = {
--[enemyID] = enemy
}
--[[
the 'enemy' in this case will be an OOP class that handles the enemy's appearance,
animations, and tweening the enemy model to where the enemy's position is on the
server.
]]
when your remote event is received on the client, create a function that does the following:
if the enemy’s id doesn’t exist in activeEnemies, store it using the pseudo-code i wrote above and have a function for the enemy class that renders its model and animations
if the enemy’s id does exist, update the enemy class with the stats that did changed
if you want to remove an enemy from activeEnemies, just do activeEnemies[enemyID] = nil.
i hope this covers what you wanted to solve, this is just a barebones example. if you need help, by all means, let me know!
I got it working, however the mobs take turns to move, so once a mob has finished its journey to the end, the next move gets teleported to where it was supposed to be. Here is a clip of what I mean: https://i.gyazo.com/8012059fee455cfc75a8e8c1757eefb4.mp4
this code is inside the remote event function.
Here is my code:
if DisplayedMobs[ID] then
local Visual = DisplayedMobs[ID]
if Visual ~= nil then
local MoveTween = TweenService:Create(Visual.HumanoidRootPart, TweenInfo.new(MoveSpeed, Enum.EasingStyle.Linear), {CFrame = EnemyPosition})
MoveTween:Play()
end
else
local Visual = Mobs:FindFirstChild(MobExists.Name):Clone()
if not Visual then warn(MobExists.Name.." model not found") return end
local Map = workspace.Map:FindFirstChildOfClass("Folder")
local RootPart = Visual.HumanoidRootPart
RootPart.Anchored = true
RootPart.CFrame = Map.Path["1"].CFrame
Visual.Parent = workspace.Mobs
DisplayedMobs[ID] = Visual
end
Let me know if you need any further code like the script from the server etc.
Thank you.
PS (Ignore how they’re in the ground and just rotating randomly, I am trying to sort out this problem first)
the reason why the mobs “take turns to move” is that you didn’t loop through all of the mobs and tween them to their server position.
in your case, you can just render the mob if it isn’t stored inside DisplayedMobs. you can scrap the following lines:
local Visual = DisplayedMobs[ID]
if Visual ~= nil then
local MoveTween = TweenService:Create(Visual.HumanoidRootPart, TweenInfo.new(MoveSpeed, Enum.EasingStyle.Linear), {CFrame = EnemyPosition})
MoveTween:Play()
end
here’s how you should move the mobs (i don’t know the full code, im just going off based on the code you sent me):
--this is pseudo-code baby!
--feel free to tweak things if you'd like
local DisplayedMobs = {
[mobID] = {
Visual = --mob model
Position = --the mob's position on the server
--its okay to store the mob's stats inside their table
--since the client is only going to read what the server has sent for said mob
}
}
local function updateMobs()
--call this function inside your remote event function
for mobId, mobData in pairs(DisplayedMobs) do
local visual = mobData.Visual
local goalPosition = mobData.Position
coroutine.wrap(function()
--we're using coroutines here to prevent mobs from "taking turns to move"
--because MoveTween.Completed:Wait() yields the thread until the mob has successfully
--moved to where they should be on the server
local MoveTween = TweenService:Create(Visual.HumanoidRootPart, TweenInfo.new(MoveSpeed, Enum.EasingStyle.Linear), {CFrame = goalPosition})
MoveTween:Play()
MoveTween.Completed:Wait()
end)()
end
end
It works likes a charm, but, I think sending the position on the server does not work, I did use a coroutine.wrap() but it still focuses on one mob. Here is the main code for it.
local Mob = {}
function Mob.Move(Enemy)
if Enemy then
local Map = workspace.Map:FindFirstChildOfClass("Folder")
if Map then
local ReachedEnd = false
local Paths = Map:WaitForChild("Path")
local EnemyModule = EnemyStats[Enemy.Name]
local PosValue = Instance.new("CFrameValue")
local ID_Result = HttpService:GenerateGUID(true)
local EnemyInfo = {
["HP"] = EnemyModule.Health,
["MaxHP"] = EnemyModule.Health,
["Node"] = 1,
["Modifiers"] = EnemyModule.Modifiers,
["Reward"] = EnemyModule.Reward,
["Reverse"] = false,
["Stunned"] = false,
["Rotating"] = false,
["Speed"] = EnemyModule.Speed,
["Name"] = Enemy.Name,
["Position"] = Map.Path["1"].CFrame,
}
local ClientEnemyInfo = {
["Name"] = Enemy.Name,
["Position"] = Vector2int16.new(math.floor(COORD_MULTIPLIER * EnemyInfo.Position.X + 0.5), math.floor(COORD_MULTIPLIER * EnemyInfo.Position.Y + 0.5)),
["Speed"] = EnemyInfo.Speed,
["Stunned"] = EnemyInfo.Stunned,
["Node"] = EnemyInfo.Node,
["ID"] = ID_Result,
}
local Connection
local function onHeartbeat(DeltaTime)
if (tick() >= NextStep) and ReachedEnd == false then
NextStep = NextStep + INTERVAL
EnemyInfo.Position = PosValue.Value
local RequiredPosition = Vector2int16.new(math.floor(COORD_MULTIPLIER * EnemyInfo.Position.X + 0.5), math.floor(COORD_MULTIPLIER * EnemyInfo.Position.Z + 0.5))
ClientEnemyInfo.Position = RequiredPosition
PositionEnemyEvent:FireAllClients(ClientEnemyInfo)
elseif ReachedEnd == true then
end
end
Connection = RunService.Heartbeat:Connect(onHeartbeat)
for Path = 2, #Map.Path:GetChildren() do
local Distance = (Paths[EnemyInfo.Node].Position - Paths[Path].Position).Magnitude
local TimeToWait = Distance*(MoveSpeed/EnemyInfo.Speed)
EnemyInfo.Node = Path
local NextNode = Paths[tostring(EnemyInfo.Node)]
local MoveTween = TweenService:Create(PosValue, TweenInfo.new(TimeToWait, Enum.EasingStyle.Linear), {Value = NextNode.CFrame})
MoveTween:Play()
MoveTween.Completed:Wait()
end
ReachedEnd = true
end
end
end
function Mob.Spawn(Name, Quantity)
if Name and Quantity then
local Map = workspace.Map:FindFirstChildOfClass("Folder")
if Map then
local MobExists = Mobs:FindFirstChild(Name)
if MobExists then
for i = 1, Quantity do
wait(SpawnCooldown)
coroutine.wrap(Mob.Move)(MobExists)
end
end
end
end
end
return Mob
Again, thank you so much for your contribution, it really means a lot. I have been trying to make this work for about 4-5 days.
The issue here is every time you move the one enemy you’re creating a heartbeat for that and only sending that mob’s data. Instead, you should have a table that stores all the enemies currently in the game and fire that table.
Edit: Didn’t realize this a month late, I was working on my game and I was figuring out how to move the enemy’s position on the server. I see you’re using a CFrameValue but I’m trying to do it with just tables, not having any luck, for some reason my brain just doesn’t know how to do it. Anyway, hope this helps if this is still the issue!
When the client logs into the game I send a remote event telling them the current time() and you can use GetNetworkPing to improve accuracy
I then create 1 folder for each enemy in workspace and name the folder the same name as the model I want the client to use
I then save the time the enemy spawned into the folder using time() and there speed and there health
this can be done with Attributes or with NumberValues
now on the client side in a localscript i wait for the server to add folders that represent a enemy using ChildAdded and i use the folders name to pick the correct model to clone
and then the client uses the time and speed to work out how far the enemy has walked
local deltaTime = currentTime - enemySpawnTime
local distance = speed * deltaTime
and then i use that distance to workout where they are along the path and this is good because the server does not need to send events every 10 heartbeats so this method should use a very small amount of network and should allow you to have lots of enemy’s without lagging the game
This is a good method, though does it account for enemies that get effects like stunned, and reverse? Unless I’m overlooking this completely. Also instead of creating multiple folders wouldn’t it be better to use CollectionService and give them a tag whenever the Enemy is created. I would love to see the performance difference between this method and sending a remote every 10 heartbeats. I’m currently sending a remote event every 10 heartbeats and handling everything else on the client, the server is simply used for values.
If you wanted to handle stun and reverse what you need are propertys
time, distance and speed
When the enemy first gets spawned
time gets set to the current time()
distance is set to 0
and speed is set to let’s say 16
and if you want to stun or reverse then you
update the time to the current time()
And update the distance to the distance they where at when they got stunned
and the speed to 0 to make them stop moving or -16 to make them walk backwards
You could use the collection services if you wanted to