0
\$\begingroup\$

I've been developing games as a hobby for a few years now and one thing that I like to do is to replicate games or a specific mechanic about them to understand how they work. I am sorry in advance this question might be a little bit lengthy but I am struggling to explain exactly what I am trying to achieve and the context around it in a more succinct way. A few months ago I decided to replicate something similar to how movement and combat work in League of Legends, and not to get too much into detail I ended up writing a server in C and a game in Unity that was server authoritative that replicated League of legends very well and I was quite happy with it. At that time I didn't predict client movement because Lol also didn't do that, and I thought that predicting pathfinding could be quite challenging to do, but I still had interpolation on the client and stuff like that so movement and combat in general was pretty smooth.

Sorry for the long intro but I thought it had important context for the question itself. Last week I decided to get that project and change it so that I would move with directional movement, like WASD or using a controller, instead of clicking and calculating a path. Implementing that without prediction was easy enough, I started by every time that the direction input changed I would just change that direction to the server which would then apply on every tick a movement based on that direction, and that worked great. I felt like with directional movement implementing prediction would be a lot easy so I decided to do that and I got stuck on a bunch of different problems implementing that without opening opportunities for users to cheat (I don't have any plans of actually making a game with this now but the whole purpose is to implement something as close as possible to a real world scenario so I can get a good understanding on how it works, so I want to avoid room for cheating as much as possible).

I've read a few good articles about the topic, mainly about how Quake does it and one from Gabriel Gambetta about how they do it (I did the same back when I made my first server authoritative implementation). So my first instinct was that for a given input the player would move an X amount of space for a given speed that the client already has, so I could just replicate that formula in the client. The main issue that I got right out the gate is that I couldn't match the exact amount of movement that the client and the server would apply for the player. For example assuming that the user has a latency of 10ms, if at time 0 I tell the server hey, I am moving to direction (1, 0) the server would get that information a time 10 and could start moving at that direction as soon as it gets the message, and would stop when the user sent direction (0, 0). The issue is that if the user sends at time 10 for example that the direction became (0, 0) if we assume a perfect scenario where the latency is exactly 10 like it was before then everything works great, it would receive the direction (0, 0) at time 20 and both would move exactly 10ms in that direction, in reality, that is almost impossible to happen, there is always some instability that makes it so that the interval between those 2 messages don't match the amount of time that the client moved. I could of course include the client time which the client started to move and stopped and move based of that, but I feel like that would allow cheaters to move more than they can. Also in the current game there are things like changing the speed, or getting stunned and rooted, that increases the complexity of this a lot since on cases like this there would always be a de-sync between the server and the client, but I am trying to solve a more basic scenario that don't include this cases first.

I feel like I am lacking some basic idea on how to implement something like this, the articles that I've mentioned seems to assume that for a give input the server would move the player in that direction for a defined amount of space, which is different than my approach where I move while the client is holding towards a specific direction. I've tried implementing something like that, but again I fall into the pitfall of, if a user can say that I should move "1 step" into that direction, how can I make sure that they wouldn't just send a bunch of messages to tell the server to move into that direction and bypass the defined speed for the player?

Can anyone point me to what I could read/watch to get a better understanding on how to implement something like this? Like I mentioned before I feel like I am not grasping exactly how this is implemented and I keep running in circles trying a bunch of different things that end up having the same 2 problems, either I cannot match the amount of movement between the client and the server, or I am sending too information that would allow clients to cheat.

Sorry again for such a big, and potentially not well written, question and thanks!

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

I'll offer some advice, this by no means is a complete answer nor the only answer, but it may give you some food for thought.

Bear with me as I explain this, because although we are using two different network models, the ideas (really, input and synchronisation) are not that different between the two approaches.

Authoritative simulation and timing: Turns

I'm developing a hybrid RPG/RTS using the P2P lockstep network model. There is no authoritative server in this network design pattern. However, given all the necessary inputs from other clients, the output on each client is deterministic and definitive across all clients (with strict use of integer arithmetic in simulation calcs). Even in an authoritative client-server setup, we strive for this level of synchronisation, although the server often has to issue authoritative corrections to enforce this.

It's clear in P2P lockstep -- because it is serverless, very strict and leaves absolutely no room for discrepancies between clients -- that there is only one correct simulation outcome, per frame, even though it is not coming from any designated server, it is still the equivalent of your server's authoritative view of the world. This is the true state of the simulation.

On the other hand, to implement extrapolation, then from an MVC perspective, that will be a completely view(render)-side decision, and this has nothing to do with the true state of the simulation. It is just a pleasant illusion that may or may not be accurate. To derive this illusion, we'll need a record of several gamestates to integrate over, in order to arrive at our best possible guess of what the current position and orientation of the opponent's entity will be.

In my project, processing of the simulation works on a turns system (much like Age of Empires). Turns execute at 10fps (well, ticks per second, or tps), while rendering happens at 60fps. Turns (10tps) are what take player inputs (read that again). Render frames (60fps) then simply do interpolation between the states produced by each of those turns (10tps), using the turn state just calculated and the previous turn state calculated, which we keep available. (You could store three or more turns states to integrate over, for more accurate predictions.) Thus, you will store a history of turns (or gamestates associated with turns) to derive your extrapolations from.

I believe (???) Quake networking used something similar, i.e. a concept of turns which act as an essential basis for timing and processing. I could be wrong. All clients should be aware of which turn is currently in progress (whether via each other, or as informed by a server).

Sending & Applying Inputs

Regardless of whether you're using a client-server or P2P-lockstep approach, you should be sending nothing more than (1) booleans of whether or not the key / button was pressed, or (2) amounts by which mouse or analog stick was moved in each axis. On basis of that raw input only, you can perform the same logic across all clients and the server, or however your topology works.

If these inputs are, frame after frame, applied as deltas to the last known gamestate, across all clients (in my case of P2P lockstep) and all such gamestates remain in sync turn after turn, then all gamestates should continue to remain in sync, indefinitely.

And the packets in which these inputs are sent, is marked with a unique turn number as well as the sending client's ID. In this way, we can easily allocate to the correct turn & client on arrival.

Prototyping network programming models and concepts, easily

The easiest way to begin understanding these problems, is to experiment on them within the confines of your dev system. I suggest modelling the system within a single app or within a client app so you can instantiate multiple times against the same server running on your dev system. If you implement this in a layered fashion, using just asynchronous programming, you can later plug in lower-level network layer support (RUDP, TCP sockets, or even higher level libraries) I got a prototype running and syncing between clients within a few days.

This consists of nothing more than asynchronous programming, based on the concepts of packet loss and packet delay. I'm sure there are other concepts involved, or finer distinctions to be made (such as network congestion, for instance) but for my purposes these have helped enormously in getting a running, synced prototype going without touching any true networking code so far.

\$\endgroup\$
1
  • \$\begingroup\$ That's extremely helpful, thank you a lot for taking the time to write this. I'll try to apply those ideas to my little project and see if I can get something working. Thanks again! \$\endgroup\$ Commented Oct 29, 2024 at 0:12
0
\$\begingroup\$

"1 step" into that direction, how can I make sure that they wouldn't just send a bunch of messages to tell the server to move into that direction and bypass the defined speed for the player?

Updates should indicate the current position, in terms of change from last server update (not to exceed max speed since last update, capped by an arbitrary time limit, say .7 seconds) and velocity vector(not to exceed max speed). The movement steps are then performed by the server timing(incorporating the 1/2 average RTT time delta) and applying a LERP with a small alpha value, varies by FPS say .05 for 60 FPS, as a nonlinear correction factor to allow a catch-up over time. The client will receive the new value from the server and adjust the client current position every step with a larger LERP alpha value of .1 the between the client current position and last server position + local applied changes since 1/2 average RTT before receipt of the update.

By setting the client adjustment higher than the server adjustment, the server and client will converge towards the servers version of position, while honoring client input and not allowing cheating via delayed false updates.

Consistent aberrations across multiple round trips indicate cheating since the client position must tend towards convergence.

Clients with consistantly high average round-trip-times > 200ms should be warned and dropped from the server, for FPS style games.

Rate Limiting

The packets should include a small timestamp or increasing packet number. Validation code on the server will time the arrival differences giving the number of elapsed seconds since the last packet, providing a means to rate limit the server from applying more than the maximum speed allowed.

Queue the packets and apply them in order, effectively delaying new commands until the queue catches up to real-time(you could even halt the client on the next server to client update). The time delay alone should discourage cheating.

The client and server should run the same code.

The client sends its state(position, velocity, firing, jumping, inventory delta...) to the server; where it is validated(collisions, distance since last update within possible range), applied to the server state, and sent back to the client.

In non-realtime or slower games like an RTS, time can be unwound and replayed with the updates in the course of a single server timestep or drawn out across several steps to allow animation to show the correction. This configuration enables the client to predict movement of non-local objects during times of packet loss or jitter.

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for taking the time to reply to my question, there is a lot of good stuff there that definitely helped me understand more about the topic. One thing that I can't get my mind around, on the rate-limiting topic one thing that I was wondering about is if for example a user with an unstable connection sends a few packages over, but since their connection is unstable thet first few packages took a lot longer to arrive compared to the later ones. It might seem to the server that they send more packages than they should in a range of time, is that just unavoidable if we want to rate limit? \$\endgroup\$ Commented Oct 29, 2024 at 0:18
  • \$\begingroup\$ @GuilhermeConti That question is the balancing act of trust. A single or sporadic delays could be network issues or sophisticated cheating, you must decide where that line belongs. There is no correct answer. \$\endgroup\$ Commented Oct 29, 2024 at 0:26
  • \$\begingroup\$ @GuilhermeConti the divisions in my answer are not mutually exclusive, simply the order and organization that my brain could effectively write about them. "The client and server should run the same code." came first if that helps. \$\endgroup\$ Commented Oct 29, 2024 at 0:31
  • \$\begingroup\$ Thanks @agone those are all very helpful insights, I will keep that in mind! \$\endgroup\$ Commented Oct 29, 2024 at 1:44

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.