How to move a lot of objects smoothly on server and client and at the same time without delay and desynchronization? (Enemy / Units System)

  1. Hello, currently I’m trying to create a game in a Tower Defense genre and the only problem is Enemy / Units System. I want to make a good Enemy / Units System which can hold really much objects (enemies and units) and make their movement very smooth on the client and it should always be in the same position on the server, so basically there shouldn’t be any desynchronization.

  2. Main issue is that roblox moving methods i tried didn’t give me a perfect result I want, there always was a little but meaningful problem.

  3. I have tried very much solutions: Humanoid and Pathfinding uncluding :SetNetworkOwner(), TweenService, RunService, BodyMovers, Much math with while lua cycle, etc, every possible method in my mind.

MORE ABOUT METHODS & PROBLEMS:

1. Humanoid and Pathfinding uncluding :SetNetworkOwner().
Basically Humanoid is the worst way to make this system, because it’s moving method Humanoid:MoveTo() is really bad and laggy. Also when enemy is little and it reaches the target, there is problem like “sliding”, the model simply slides past the desired point. And Roblox Pathfinding is a hard headache, so it’s really very bad decision to use it in my system. Also :SetNetworkOwner() doesn’t really reduce lags and help in total, also as you may understand there was anchored parts in absolutely every version of Enemies System, so :SetNetworkOwner() it probably useless.

2. TweenService.
So the TweenService is a good decision at first sight, but when you try to use it, it becomes a headache. I tried very much variants of System, like moving Enemy on both game sides, so the client moves the enemy separately from the server, Moving Enemy only on the Server/Client, but in Server case it’s replicating not smoothly (the client is bad for obvious reasons).
So what was the problem? An amount of enemies it can hold (I haven’t mentioned the huge number of re-creations of the tween due to the change in the speed of the enemy). 100 Created objects already kill game, it’s moving becomes really bad and they start to teleport.

3. RunService.
So the RunService was one the best decisions i tried, using it i made a system which works separately on the server and client. It was able to move very smooth and without lag and teleports up to 1 thousand of enemies, and i thought that i found the solution, but I was wrong. As you know (I hope you know), RunService is based on your FPS, so .Heartbeat or .RenderStepped Events speed depends on your FPS, that’s caused a new problem, enemies was desynchronized among clients with different FPS, I tried to add more math to calculations so including player FPS could give you the same result on all devices, but it didn’t really help. Also there was a desynchronization event between client with good FPS and server. Once client lagged a bit, enemies was desynchronized forever.

I found a GIF with this system (for nerds: models are mine and they wasn’t stolen from known TD game, i made them for a testings.):

NewEnemySystem

4. BodyMovers.
So in short, Body movers are also make lags and desynchronization when enemies amount getting up to 100. Also they can’t have a constant speed.

5. Much math with while lua cycle.
This method is really bad because moving is not smooth. Also it’s fairly laggy when there is like 500 Enemies Objects.

MORE INFO & MY OPINION:
All numbers are results of tests, so it is not just approximate value. Every method causes a lag when there is a lot of Enemies (especially when we are talking about 1-2k enemies or units objects) and it’s pretty predictable, because everyone’s PC has different power, but I’am talking about client lags, it appears when such amount of Enemies or Units are spawned. So client is getting slowed, that’s also can call a desynchronization between Client and Server, so I thought that we can make this Enemies/Units System on the Server, but there is no way to make it with smoothly moving, so that’s why we have to replicate system on the Client and on the Server separately.

So I really want to ask you, how theoretically I can script this System, which methods I should use to get best optimization and make smooth movement without desynchronization. I really appreciate all answers, thank you for reading my problem and for your answers!

P.S.: Sorry for grammar mistakes, English isn’t my best.

9 Likes

Yes, probably RunService is the best decision for this system, and probably I can fix a desynchronization by using DeltaTime in my calculations, but the only question is how to include it into the code, as well, I still understand quite bad how GetServerTimeNow can help with synchronizating Client and Server together. Also there was a problem with Remote Events / Functions and other game scripts, when I spawned around 2k enemies game became laggy because my PC isn’t best, but problem is that for example towers placement became strange, like I clicked RMB to place tower, but it didn’t work, only after big delay (~10 seconds or more), could it be fixed somehow or no?

My solution is a bit different than any of your ideas, but I think its a lot easier than trying to sync server and client.

So I used a lot of client sided rendering, not using any models or parts at all on the Server, and only using positions and info inside a table. An enemy would look something like

{
["Health"] = 100;
["Name"] = "Enemy";
["Position"] = CFrame.new();
}

Just an example, not how I setup my table at all.

For movement, on the Server there is a Heartbeat event that handles enemy movement, and every 10 heartbeats we fire a RemoteEvent to all clients which contains all the enemies info. This enemy info is actually different from our normal table with all the enemies info, it uses Vector2Int16 and Vector3Int16 to compress numbers into 16 bits to reduce the data being sent, which is very important.

On the client, we loop through our table of enemies, create a new model if one doesn’t exist, and if it does then we tween it to the new position.

This gives us smooth movement, and lets us use high numbers of enemies. I tested about 500 enemies and got 60 fps, normal ping, and about 100kb recv/s data which is pretty good.

If you want more explanation, I also made some videos about it, they’re styled as devlogs so sorry if you only want to watch the important stuff.

This video talks about the client rendering, skip to 1:49 if you just want the important stuff: I Spent 3 Months Making A Tower Defense Game | Vault TD Devlog #1 - YouTube

And this video talks about how I improved it with Vector2Int16 and Vector3Int16, skip to 1:51 for important stuff: Adding a Boss to my Tower Defense Game! | Vault TD Devlog #2 - YouTube

With this method you don’t need to worry about clients being desynced since its still being calculated 100% on the Server, but you don’t need to worry about the lag from models, animations, etc. on the Server.

17 Likes

I’ve watched your video and I can say that your solutions about another game mechanics are really very interestring, as well as your solution about Enemies System, it’s really very good idea to calculate all on the Server and send information to the client, I had this idea in my head but I thought that it won’t be a good decision. But I also have some questions:

  1. How units can be added? Because we should check if they are colliding with enemy and then kill them/lower their health? How would you calculate it? We have only positions, not regions3, although we can create hitboxes in the module and create regions3 to check it, but still, how would you calculate it?
  2. How do you tween enemy models, like which TweenInfo you are using, because its should be probably really fast (if we are firing data event every 10th heartbeat)?
1 Like

So for units, I used FastCast for bullets. FastCast basically lets you create bullets that travel overtime but still uses Raycasts, its just lots of little Rays instead of a single Ray. FastCast has an event which fires every time the Ray moves, and in that event I just do a magnitude check of all the enemies. If the bullet is close enough to the enemy, I used .25, then it damages.

We don’t need to actually check when the enemy is hit, since the magnitude check ensures the bullet is basically touching the enemy.

Unrelated but don’t use Region3 anymore. Its deprecated so if it breaks Roblox won’t fix it, and OverlapParams is an improved version of Region3.

This is the tween I use, its long enough to not ever stop, but quick enough to be at the right position. I use Linear because otherwise the enemy movement will look off.

TweenService:Create(Enemy.PrimaryPart, TweenInfo.new(.2, Enum.EasingStyle.Linear), {CFrame = EnemyPosition}):Play()

Thanks for the information, but I meant units which walk on the path like enemies, so they can collide with enemies and kill them (jeeps from Tower Defense Simulator for example), how would you check this collision?
Also I’ve got a little question about boss in your game, so “GR-NATER” has an ability to throw something like mines to the path points, but how do you play an animation and stop boss’s walking? Since all the models on the client, you should fire another event with this information?

Ah I see, its basically the same thing but every time the enemy moves you check if its near the jeep with a magnitude check and damage if its close.

So for speed, I actually have a Speed value in the tables which lets me change how fast the enemy moves, and I just set it to 0. On the client, I set the animation speed of the walk animation to be the speed, so it stops.

I do fire a remote for the grenade launching, all it does is tell GR-NATER to play an animation, and launch grenades to 6 points picked out by the server.

:BulkMoveTo or Rewamping :man_running: Service method is ur best bet.

EDIT: Ok wth it replaced run with emoji xD

I don’t think that :BulkMoveTo() is a good decision, because it’s can’t make movement smooth and also how would you optimize this method? I don’t think that we can make smooth and optimized movement using it.

I guess using Kdude’s method and maybe modifying it somehow - best available variant.

But does tween creating also stops, or it’s still works and eat your PC’s power?

Also I wanted to ask you what do you think about adding enemy offset into this system? I think it’s possible just by adding one more variable to the enemy table.

So we have variable ["offset"] for the enemy and it means enemy offset regarding enemy’s position, so for example if enemy CFrame is CFrame.new(0,0,0) and offset value is 2, then enemy real position would be something like

EnemyCFrame = EnemyCFrame + (EnemyCFrame.RightVector * offset)

Or maybe better to include enemy offset to the enemy position calculatings? What do you think about it?

You could make it stop tweening if you want to, just by only Tweening if the speed is greater than 0.

It should be possible, adding "offset" to your enemy table would probably be better than calculating the offset into the position.

I don’t know if you’ve solved this yet, but here’s how DeltaTime would be implemented into your code:

runservice.Heartbeat:Connect(function(dt)
    enemy:Move(movementFactor * dt)
end

What this does is it multiplies your movement amount by dt every frame, so that it’s synchronized no matter the fps. Buttery smooth!

1 Like

But deltaTime may be different on client and server, don’t you think it’s will be desynchronized between client with good FPS and server (not to mention a client with bad fps)?

I’m really not too sure about that, you can try on both and see what happens.

Ok so I think that your solution is best here, but I still have little questions.

Do you think it’s possible to make client rendering using RunService? I think it could make this system even more optimized, but the only problem is that RunService based on your FPS, so probably we have make there many calculations to optimize this system. Any Ideas of how to calculate this?

No idea, I’ve never tried separate RunService events with this, and I’m not sure how it’d work. If you do want to optimize my system better, you can use bit packing to send even less data to the client, but its more of a micro optimization.

Okay, anyway big thanks to you for help with this system, you ended my suffering. Also thanks to everyone who tried to help me!

1 Like

sorry for the 12d bump, did you just say to tween a “MODEL”? I’ve been trying to implement something like this and stumbled around this post, I tried the same thing you used and it only moves the primarypart. how come you do not have a problem with this? (or did you use something else.?)

You need to weld everything to the PrimaryPart, or weld it to something welded to the PrimaryPart. Then everything except the PrimaryPart needs to be unanchored. It helps to have a plugin to weld parts together easily, then you just unanchor the model and anchor the PrimaryPart of the model.

I see, taking note of that once I have free time. I’m currently using AlignPosition as a band-aid and i just set the Position value of AlignPosition every 0.25 seconds or so.