I am currently working on a enemy system for my hobby project game, and I am struggling to figure out a way to make the system both performant and synced between the client and the server.
What do you want to achieve?
I want to create an enemy AI system using OOP with composition and FSMs. Most importantly, this system needs to be performant and synced (can handle hundreds of enemies at once). I know what I need to do to make the system performant: the client needs to simulate/render enemy movement and also carry out animations (just do what the server tells it to do) and also carry out hitbox detection (to then be verified on the server); the server needs to keep track of all enemies and update them accordingly, and also handle sensitive operations like taking damage and dropping loot on death (also running the FSM I believe).
What is the issue?
The issue is I can’t figure out how to ensure the client and server are both synced with enemy positions whilst maintaining server performance. And this is mostly due to the complex terrain in my game – I see lots of posts about how TDS games make enemy movement systems perform, but because their movement is always predetermined on a flat path, It is so easy to implement strategies for the server to predict enemy positions.
What solutions have you tried so far? Did you look for solutions on the Developer Hub?
I have searched and thought out a number of solutions, and here is why each of them did not work out for me:
I first thought to just have a simple transparent part with a humanoid on the server and move that along with a fully fledged model on the client, but this again involved doing some movement simulation on the server, and I know that I can make the system even more performant by not doing this. Also, I would still have to call humanoid:moveto on the server which, from what I have researched, is very bad for performance. I figured I need a way to move these enemies along complex terrain without using humanoid:moveto since if there are hundreds of enemies, this would be very bad for performance.
I then thought to just have the server send positional updates to the client and then the client just lerps their enemy model to the given position, but the issue is I am not sure how to get the intermediate positions between two points that are far away, and also lerping the model can cause clipping issues with the terrain on the client since it is just linear movement (also note that allot of these issues are to do with the complex terrain in my game).
I tried to use kinetic equations of motion to predict where the client’s enemy position would be at any time on the server by just knowing some factors like where the model is facing, its movement speed and related times. This seemed great because it eliminated the need for sending constant positional updates to all clients, and instead the server can just simply get the rough position of any enemy when it wants to, and It can just send occasional events to, for example, stop movement. But the issue with this is that the equation of motion is just linear, which does not work with complex terrain. See this image for example:
The last thing I have tried using, and the thing I am currently implementing, is just having it so that the client is the network owner of a batch of enemies, and that client is responsible for moving and animating the enemies correctly. The server would still manage critical aspects like health management and dropping loot – the client is just given full ownership to do its jobs mentioned at the beginning, and this makes it a lot easier since there needs to be no sync between client and server. But I don’t want to continue using this for the following reasons: some clients may have very bad network stats like ping which would result in very choppy movements and may affect gameplay for other players, it is definitely not secure since clients can just position enemies anywhere they want (which would ruin a bossfight for example). Also I would like to mention that I have tried to mitigate this terrain issue by raycasting downwards on the client to adjust the enemy’s height accordingly, but i’m not even sure how the server would be able to know this adjusted height position, and I’m not sure how to handle scenarios when the enemy should stop because it runs into a wall instead of just adjusting its height straight to the top of the part.
I watched crusherfire’s video about how he developed his enemy system, and he is able to handle hundreds of enemies simultaneously, although he is setting network ownership of the enemies to the client, which I don’t understand since this is a big security risk? (Timestamp: 5:19. Link: https://www.youtube.com/watch?v=uy93Aa7vbtw&t=390s)
Thank you very much for reading this post, I know it is very long-winded but there is alot of things that I had to address here that are also very hard to explain. If you think you can help me then please let me know. It would be amazing if someone with lots of experience with these kind of systems can help guide me.
First of all, what type of game are you working on? in some types of games you can make some tricks to remove stress from some systems, also why you choose to use OOP, OOP is more redable than performant code, but for AI it’s great. And last thing is you cannot achieve perfect sync of client/server.
There are few solutions:
To sync client and server there is the least invasive methood, which is calculating how much time elapsed from tick() sended on server to current tick() on client, using this time you can teleport enemy to the correct position based on time that have passed
Don’t use humanoids, they are OK for few enemies but not for hundreds, even disabling states doesn’t help, you can use RunService and Lerps to simulate movement
you dont. you just do newLocalPosition = cframe.new(localPosition, targetPosition).Unit * studs * dt * 60 and keep repeating this. this guesstimate of a line of code basically just moves some object towards a target at a constant rate if you couldnt obviously tell.
lerping in this situation is possible as well. each time the target moves, you recalculate the alpha by resetting the time elapsed since the lerp began to zero, and also recalcualting how long it will take to move to the target by using the local fake humanodis walkspeed (studs/sec) and dividing it by the distance (studs/1sec)
create your own fake physics. we know that gravity in roblox is 196.2 studs/sec^2. to get the distance we need to fall, raycast downwards. to get the time, you can rearrange the formula 0.5 * g * t^2 = d in terms of t (sec) where d is distnace (studs) and g is gravity (studs/sec^2)
you can do this entirely on client. and when the server NEEDS to know the exact position of the npc considering terrain, the server can raycast themselves.
this however doesnt pair too well with the lerping method that i described earlier, because doing this would require you to recalculate the lerp alpha when the Y axis of the local object changes
notes:
the Y axis from the .Unit thing must be removed
unit code doesnt work. do me.CFrame *= CFrame.new((them.Position - me.Position).Unit * .1 * dt * 60)
I am making an action RPG game where the play fights against lots of types of enemies, so the system to handle such enemies needs to be performant. Are you saying I shouldn’t of used OOP? Also with your syncing time point with tick() is that not using the same logic I mentioned in the post – using equations of motion? And yes I know I should not use many humanoids, but I need a way to move then without using moveto
I gave you that in my post.
Anchored hrp + Move at a constant rate towards goal + calculate physics (using os.clock and roblox gravity) per local Y axis change or target positional change + keep on relative part when standing by using ToObjectSpace
Only thing i havent considered is collisions which an probably be done by getting the direction each object tbe humanoids listening to, and checking if any of its 4 corners now exceeds the hrps 4 courners, and the pushing the humanoid in some direction
However this behaviour isnt obvious when the humanoid is smushed through blocks for example
ok, there is an option to use trick, player wouldn’t fight 500 of them at once, you should consider making enemies bigger and stronger, then player will feel they overwhelming him, this way from 500 weak enemies you can make 70-100 strong ones, and result would be the same
i put my response into code form. pink is a fake humanoid.
its actually the other way around. on server, the custom humanoid should raycast for its physics. on client, the physics should be an actual humanoid, mimicking the movement of the fake.
Okay I see how you can use the cframe updating this with the delta time, I will try that out. I also see how you can use gravity and raycast downwards. However I have a couple questions: how would the server be able to get the position of the enemy using this method, since the client is the one that is simulating the movement – unless I am missing something about what the server is doing? Also what about moving up slopes in terrain or up stacked parts? I would want the enemy to be able to climb up a part if it is barely any taller than the group, but if it is a certain height, the enemy should not be able to move up and instead just stop moving. I guess you could raycast downwards?? If you could show me the important bits of the code you are using to create the movement in the video you showed me both on the client and the server that would be greatly appreciated
no actually in the system i made, the server is updating its version of the humanoid at a constant rate. custom physics and moveto checks here. and when i finish the system, the server will send the direction, magnitude, and estimated time of the moveto to the client (the client will handle their own physics by just using the default humanoid, making it moveto the replicated position, and then using that humanoid for physics). the server always knows the position of the enemy. the client has a guess based on the direction magnitude and estimated time of the moveto.
so in theory (i programmed it wrong and need to get it working properly) when the npc is moving on server, raycast from the npcs cframe * some forward cframe * up 1 stud, and raycast downwards .8 studs, to see if the npc needs to walk up steps or a slope for example.
mental breakthrough: on server, parent a part to workspace.currentcamera. notice how it doesnt show on clientside, but shows on server side? now, if you wanted to parent a humanoid to there, you can, and you can create a fake humanoid on clientside to mimick the servers humanoid cframe. however, that comes with some expense on serverside probably.
instead, what if you created an unanchored part on the server in currentcamera, and instead of calculating physics and moveto yourself, you can just calculate the moveto.
this would save a bunch of inaccuracy, raycasting, and would give you collisions as well; the part in currentcamera wont replicate to clients, and thus you can just send the client (at minimum) the direction the npc must go, and the length it must go. easily compressible into 1 number
its absolutely genius what i came up w
Okay but are we not still simulating movement on the server side with humanoids? The whole point of making the system performant is to be able to give clients the sole responsiblity of rendering movement and the server just manages the movements via remote events. I mean you could still use a barebones humanoid with disabled states on the server but this must still drain performance. I still need to implement your previous idea of using custom physics but i have not got around to it yet.
Also i just want to ask, is it feasable to use align orientation and body velocitys? I know that one of those two prevent a part from clipping though the group or toppling over, which would be perfect instead of constantly ray casting. I looked in the vesteria source code and saw that they are using the depricated versions, and the performance of the enemy system in that game is what im trying to get to.
you dont necessarily need humanoids. i thought i made that clear in what i said. wtf is u talkin abt?, respectfully.
also if you WERE to have a humanoid in currentcamera on server, it would not render whatsoever. not visually at least.
but i was saying, if you were to parent a part to currentcamera on server, an unanchored one, and made it moveto a target position using a custom moveto function (using the unit formula i wrote in my first post in notes), not only do you get physics, but you also get collisions.
ive never really used roblox physics instances before. the most ive really done w them is a dash system with body velocity, and a swimmign system.
but you can definitely use that instead of constantly raycasting
Okay, but your idea of having a part in the workspace camera and using the movement logic to move that is still handling movement simulation on the server which is what im trying to avoid. in this post: How we reduced bandwidth usage by 60x in Astro Force (Roblox RTS) their first version of their client/server movement system was to parent a transparent part to the server camera and do it that way, but in the end they found a much better way to handle enemy movement by sending smaller vector values to the client and having the server not perform any movement tasks – a solution that i have tried to implement in my game.
they use a custom collision system (+a 2d grid), which is not plausible in your system. you don’t want movement on server apparently, which includes physics. so if one were to handle physics entirely on client there would be a lot of desync. if one were to moveto entirely on client there would be desync. HOWEVER, if someone, ON SERVER, was to apply a moveto to some humanoid in the servers camera, then the humanoid would, obviously, have its moveto cframe applied, but ALSO if its unanchored and cancollide, it would have physics and collisions.
now i see what you’re saying: “but how would i replicate the physics cframing?”
i shouldve expanded my thought process earlier, and im sorry. i missed something very crucial.
so, movement, at least in my head, is defined by a horizontal angle, that is left or right at a maximum of 360 deg, and a vertical angle, which is up and down with a max of 360 deg. to get the direction a humanoid moved over some unit of time, you subtract the previous position from the current position. with direction, you can use the atan2 function, which returns the number of degrees a position is on the graph, from the origin. say you have a cartesian graph, and theres a coordinate (1,1). the angle from the origin (0,0) will be 45 deg, for example.
take this logic on the horizontal and vertical of the humanids new cframe. you would do math.atan2(hum.x, hum.z) to get the angle the humanoid moved horizontally, and hum.y to get the vertical movement. and for the distance traveled you jsut do direction.magnitude.
compress those values into something small, say 1 number, and then send it to the client. the bitstream should look like vertical|horizontal|magnitude or something along the lines of that.
then the client decodes the bitstream, getting the vertical horizontal and magnitude of the new movement. then they multiply their version of the humanoids cframe with ion even know im assuming it would look like: cframe.new(vector3.new(math.cos(horizontal), vertical, math.sin(horizontal)) * magntiude) thats really just a rough guess, but thats the cframe the hunanoid changed by
Hi there, thank you for sending me this resource. The only thing is that he is using path finding service, which I am not going to use for my game due to its performance issues, and also he is moving them exclusivley on the client side – I need to have some sort of system that at least checks the clients movement, or just tells the client what to do regarding an enemies position.
So im guessing you are tweening or lerping the characters movement? How will the AI know where to go? Are you using your own pathfinding service?
It likely has issues because of network ownership on the npc. If your trying to use a bunch of humanoids lerping or tweening the position also has its costs on the server probably even more than pathfinding. You could have movement on the client but the server sending the client where the npc should go. That way it could have track of where the npc is. Relying on the client on where the npc is, is a dangerous practice.
So in summary
Movement → Client
Path Predicting → Server which passes that info to the clients.
Yeah thats pretty much what im trying to get to. The only thing is I dont know how to keep the client and server synced so the server can get the clients position when needed. In my game, there will be lots of complex terrain, so using simple kenetic equations would not be enough (illustrated in post), and i’m not even sure how to go about terrain height adjustment and stuff like that (i’m trying to avoid using humanoid moveto on the client)
Well in that case youll never be able to get all clients be perfectly synced. Simply the client or server will always be in front. On the server if its sending every client data in where to move every second. The client can check, if the npc is too far away then it can teleport them to that place. That way it can make sure and properly predict where the npc is.