For the game I’m working on, I want to make sure that combat abilities (projectiles, explosions, etc.) look smooth enough so that players can actually avoid them. From what I’ve read so far, the only way to achieve this is by having the effects on the client.
However, there seems to be some potential problems that I would like to know how to address before I continue.
First of all, there could be a huge desync between the server and the client if I were to make the effects respond to changes, meaning they are not linear (they can be modified or canceled). For example, if a player wants to activate an ability that fires a projectile, what I would have normally done is (ignoring cooldowns):
Player presses a key, does local checks and fires a RemoteEvent with ability name, tick() and other arguments.
Server checks if the ability can be activated and if yes, calls the corresponding function.
In the ability function, an invisible hitbox is usually created depending on the ability and FireAllClients() is called on a RemoteEvent with data on the spell.
Clients create the effects based on data received and take time difference into account.
This would work well for effects that are always done the same from start to finish, but what if a projectile changed colors each time it damaged an enemy (damage is verified serverside)? Or if a projectile selected a new target after each hit? Remote event calls would quickly become messy.
Another problem that bothers me is how I would take players that join later into account, since some effects could be long-lasting such as those for AOE spells. New players would have missed the event call, so what would be the way to let them know that they need to create and sync the effects for active spells?
Make a table inside the server with the active spells, Once a player joins make it so it fires a RemoteFunction to get this table. Then act apon these spells.
This would only partially solve the second problem because the effects also have a state, so if I created explosions using TweenService, how would I skip to where the Tween should be, assuming that many tweens are played one after the other? Would it be better to just ignore spells that were cast before the player joined? I wonder if this is even something I should care about, but I would love to know if there is a way.
You shouldn’t really be worrying about resuming them for players that join after they start. More likely than not, that player wont even load until long after the spell is done. If you really want to though, you can synchronize unix time (or use tick, will be +/- 200ms) and resume based on the time elapsed since the effect was fired.
The issues you described here are inherently messy because you’re trying to compensate for not having necessary data on time. This is fine for predictable stuff like “what position would this projectile be in, with a given initial position and velocity?” But as you’ve seen, it starts to break down once the clients try to predict data they don’t have access to (eg, if the spell has actually hit into any players, who do not move predictably). So if you try to accelerate the spell’s position to compensate for lag, you risk it being desynced from the server telling the client what it hit into, which can be annoying for players to have to dodge, and leads to situations where it looked like it hit but it actually didn’t. Other factors to consider is when the the spell finally diffuses; presumably, this spell would be destroyed if it hit into a stationary wall. What happens when the clients predicted they would travel through a wall (since they don’t handle collisions, the server does), but it actually smacked against the wall serverside, destroying it? From the clients’ perspective, it would look like it passed through it, only to snap back and play some diffuse particle animation.
Granted, you could have a special case done on the clients’ side to locally play the spell diffuse animation if it hits into a stationary wall, since it’s highly unlikely the server would disagree about where it is. But the point is, speeding up projectiles to compensate for lag is a deceptively simple thing to do that involves a lot of questions.
This is why I’d recommend you dispense with the lag compensation and just have the server give the remote events as it happens and accept any natural lag you may run into. It would simplify your codebase and sidestep a few of the types of issues I discussed here. After all, you can only do so much with what the clients’ pings can give you. Unless your game is really fast-paced, the ~150 ms difference in time probably isn’t gonna be hurting anything. And if somebody has a terrible ping for it to matter, they have to accept they’ll have a terrible online experience, no matter what online game they’re playing.
New players would have missed the event call, so what would be the way to let them know that they need to create and sync the effects for active spells?
You could go the route of handling state entirely within scripts (ie having tables that track projectiles, then handing them off to new players). This might be easier on you if you actually created the projectile instance serverside, then passed the instance reference to clients for them to locally manipulate it, such as moving it around and giving it visibility. That way you wouldn’t need to have more code for reconciling the local versions of the projectile with the serverside version of it. Then have some code that would regularly update its position / other state that would fire to all clients at given time steps.
if I created explosions using TweenService, how would I skip to where the Tween should be?
Just a side thought, but if you have very long tweens you could always use the often neglected TweenService:GetValue. You could use a syncing remote event that gives the two position pairs for newly joining players to use GetValue on. (You would also need to give the amount of time since the projectile was last updated for the alpha to give GetValue). Then you could use CFrame:Lerp on the alpha value it gives you, along with the two position/CFrame pairs it received from the server to interpolate between. But if you’re tweening one small tween after another, I don’t think it’d be worth your time.
Woah, this was an awesome answer, really more than what I was expecting. I guess you’re right, that we just have to live with lag and that its impact on gameplay isn’t the end of the world. Thank you so much for your time!