Skip to content

Commit cd89b22

Browse files
committed
feat(multiplayer) networked player movement tut
1 parent c78ebb5 commit cd89b22

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed

tutorials/networking/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Networking
66
:name: toc-learn-features-networking
77

88
high_level_multiplayer
9+
networked_player_movement
910
http_request_class
1011
http_client_class
1112
ssl_certificates
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
.. _doc_networked_player_movement:
2+
3+
Networked player movement
4+
=========================
5+
6+
Most modern online multiplayer games operate under what is called a server authoritative client-server model.
7+
In this model, clients connect directly to a game server over a socket connection and pass messages back and forth.
8+
Being server-authoritative means that the clients are "thin" or "dumb" and make no decisions about the state of the game
9+
world themselves. Instead, they merely pass player input to the server and handle the display of whatever the server tells it
10+
that the current state of the game world is. In other words, the server is the arbiter of the game world that all clients must abide by.
11+
12+
Normally, this isn't that big of a deal. A client informs the server of its intent, such as :code:`Player swings sword` and the server
13+
receives this intent, modifies its local game world state, and then broadcasts the relevant new state to all clients. When a client receives
14+
this new state, it updates its own local copy of the game world state and renders it. This asynchronous nature of communication between the
15+
server and clients is what allows players spanning geographical regions to interact with a shared game world in real time. There is one facet
16+
of online multiplayer games that doesn't work so seamlessly in a sever authoritative model. That being player movement.
17+
18+
If you consider how player movement would work if implemented using the appraoch above, it would go something like this:
19+
20+
1. Player sends intent to move left
21+
2. Server receives players intent
22+
3. Server moves player left in its local game world
23+
4. Server broadcasts player's new transform to all clients
24+
5. Clients update individual local game worlds with the player's new transform and renders it
25+
26+
27+
At a glance this may seem fine, however remember that in a networked environment, we have latency to deal with. Even with a good network connection,
28+
transfering data over the internet always has at least *some* inherent latency. For instance, it's not unusual for the time between the player sending
29+
its intent to the server and the time at which a client receives the new authoritative server state to exceed 200ms. If you consider how this would look
30+
from a player's perspective, you'll quickly realize how choppy the user experience can be. Fortunately, people have been making online multiplayer games
31+
for a relatively long time now, and techniques have been developed to combat these shortcomings, while still allowing the server to remain authoritative.
32+
33+
Client-side prediction
34+
----------------------
35+
36+
Client-side prediction is a rather simple technique to relieve the choppiness of sever authoritative player movement from the player performing the movement.
37+
Alone, it's not very useful, but when combined with server reconciliation (as described below) it allows the player to move fluidly while still leaving arbitration
38+
in the hands of the server. To be honest, client-side predicition here is a bit of a misnomer. A more accurate term would be "client-side assumption," as we will see.
39+
40+
Essentially, client-side prediciton boils down to performing the intended movement on the client, assuming that no shenanigans like cheating occur and the the network
41+
connection is stable. Let's take a look at how we can implement basic client-side prediciton for a networked KinematicBody2D. We will expand upon this example as we
42+
introduce the other components of networked player movement.
43+
44+
::
45+
46+
# networked_player.gd
47+
extends KinematicBody2D
48+
class_name NetworkedPlayer
49+
50+
export var speed = 3.0
51+
52+
func move(direction):
53+
if direction.length() == 0:
54+
return
55+
56+
rpc_unreliable_id(NetworkedMultiplayerPeer.TARGET_PEER_SERVER, "_move_server", direction)
57+
move_and_slide(direction*speed)
58+
59+
puppet func _move_server(direction):
60+
if not multiplayer.is_network_server():
61+
return
62+
move_and_slide(direction*speed)
63+
rpc_unreliable("_set_peer_position", position)
64+
65+
remote func _set_peer_position(pos):
66+
if multiplayer.get_rpc_sender_id() == NetworkedMultiplayerPeer.TARGET_PEER_SERVER and not multiplayer.is_network_master():
67+
position = pos
68+
69+
70+
In this example, we send the move intent (direction) to the server and then immediately move the character locally how we assume the server will do so.
71+
When the server eventually receives our intent to move, it will move the character in the authoritative game world as well and then tell the clients what
72+
the new position is. All of the clients except the network master (the one controlling the player) will update their local positions to match.
73+
74+
Server reconciliation
75+
---------------------
76+
Client side prediciton gets us part of the way there for our player's character, however it only solves the issue of choppiness introduced by latency. The
77+
server authoritative state is never applied to the player character of the controlling player, only its peers. We could naively solve this by simple overriding
78+
the position of the controlling player's character as well, just like we already do for the peers' copyies of the character however is we again consider latency,
79+
we realize that this might not be the best idea. Since introducing client-side predicition, we now move the player character locally every time :code:`move` is called,
80+
which is likely every frame or every physics frame. This means that by the time the authoritative position comes back from the server, the local character would have
81+
already moved even further. Resetting the local position to the server authoritative state now would result in the character appearing to teleport backwards, resulting
82+
in a phenomenon known as "rubber banding." It turns out, solving this problem isn't so simple and requires some substantial changes to our code. The teqnique we use here
83+
is known as server reconciliation.
84+
85+
Essentially, before we send out input off to the server, we store it in an input buffer along with an sequence number. We also buffer the input on the server as well and
86+
introduce a polling frequency in order to reduce bandwidth. This is an optimization that is mostly orthogonal to the topic at hand, but it's a good practice none-the-less.
87+
Whenever the server processes an input from the buffer, it broadcasts the resulting state to all clients. Clients store the last known state in a member varibale and ever
88+
physics frame on the controlling client, we reset the character's local position to that of the last known server state and then we loop through all of the inputs in the
89+
input buffer and for each one whose sequence number is greater than that of the last known server state, we reapply it locally. If there was no cheating and the network
90+
connection was stable, the resulting posiiton should be the same as it was before, however if there was some cheating or dropped messages, then the resulting state will be
91+
synchroized to what the server thinks + any predicted movement since. In code, this would look something like this:
92+
93+
::
94+
95+
# networked_player.gd
96+
extends KinematicBody2D
97+
class_name NetworkedPlayer
98+
99+
export var speed = 3.0
100+
export server_tick_interval = 100
101+
102+
var _last_server_state = {}
103+
var _input_buff = []
104+
var _accum = 0.0
105+
var seq = 0
106+
107+
func move(direction):
108+
if direction.length() == 0:
109+
return
110+
var cmd = {
111+
"d": direction,
112+
"seq": seq,
113+
}
114+
rpc_unreliable_id(NetworkedMultiplayerPeer.TARGET_PEER_SERVER, "_buffer_input", cmd)
115+
_buffer_input(cmd)
116+
move_and_slide(direction*speed)
117+
seq+=1
118+
119+
func _physics_process(delta):
120+
# If this player instance is the server, then it
121+
# is the source of truth. It should process the
122+
# buffered input and replicate it back to the client.
123+
if multiplayer.is_network_server():
124+
var curr = OS.get_system_time_msecs()
125+
if (curr - _accum) >= server_tick_interval:
126+
_process_server_input(delta)
127+
_accum = curr
128+
129+
# If this is the entity who sent the input, then reconcile
130+
# with whatever the last known server state is. This might
131+
# contradict what was sent if the sent input was invalid or
132+
# deemed incorrect by the server.
133+
elif is_network_master():
134+
_reconcile(delta)
135+
136+
# Server reconciliation. Directly set the current position to that
137+
# of the last known server state, and then reapply all inputs since then.
138+
# This ensures client consistency with the server state.
139+
# @see https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
140+
func _reconcile(delta):
141+
var last_state = _last_server_state
142+
if last_state:
143+
# set the current position to the last known server state
144+
position = last_state.p
145+
146+
# reapply any input since the last known server state.
147+
var del = []
148+
while not _input_buff.empty():
149+
var cmd = _input_buff.pop_front()
150+
if cmd.seq > last_state.seq:
151+
move_and_slide(cmd.d * speed)
152+
153+
func _process_server_state(delta):
154+
if _input_buff.empty():
155+
return
156+
var last_seq = 0
157+
while not _input_buff.empty():
158+
var cmd = _input_buff.pop_front()
159+
last_seq = cmd.seq
160+
move_and_slide(cmd.d * speed)
161+
rpc_unreliable("_append_server_state, {"p": position, "seq": last_seq})
162+
163+
remote func _append_server_state(state):
164+
if multiplayer.get_rpc_sender_id() == NetworkedMultiplayerPeer.TARGET_PEER_SERVER:
165+
_last_server_state = state
166+
167+
# Keep a buffer of inputs. This is executed on the server and the client
168+
# that sent the inputs. The server will use this buffer for processing,
169+
# while the client will use it for server reconciliation.
170+
puppet func _buffer_input(input : Dictionary) -> void:
171+
_input_buff.append(input)
172+
173+
.. note:: For more information on client side prediction and server reconciliation, check out `the wonderful article by Gabriel Gambetta <https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html>`__.
174+
175+
Entity interpolation
176+
--------------------
177+
The final piece to the networked player character puzzle is *entity interpolation*. Client-side prediction and server reconciliation solve the problem of networked movement for
178+
the character the player is controlling, but they don't help with movement from *other* players. Luckily entity inerpolation can solve this for us rather easily. If we consider that
179+
we are actually receiving character state from the *past*, it might become apparent that all we need to do is lerp to the last known server position and call it a day. This, of course means that peers will always appear
180+
slightly behind real time ("slightly" here depends on latency, but typically no more than around 250ms unless your connection is unstable), but in the case of peer movement, this is usually find. In the end, in most games,
181+
the exact position of a peer player at any given time isn't that important. A coarse estimation is usually enough. We can easily add entity inerpolation to our example code.
182+
183+
::
184+
185+
# networked_player.gd
186+
extends KinematicBody2D
187+
class_name NetworkedPlayer
188+
189+
export var speed = 3.0
190+
export server_tick_interval = 100
191+
192+
var _last_server_state = {}
193+
var _input_buff = []
194+
var _accum = 0.0
195+
var seq = 0
196+
197+
func move(direction):
198+
if direction.length() == 0:
199+
return
200+
var cmd = {
201+
"d": direction,
202+
"seq": seq,
203+
}
204+
rpc_unreliable_id(NetworkedMultiplayerPeer.TARGET_PEER_SERVER, "_buffer_input", cmd)
205+
_buffer_input(cmd)
206+
move_and_slide(direction*speed)
207+
seq+=1
208+
209+
func _process(delta):
210+
# Remote client peers should lerp to to the last known server
211+
# state.
212+
if not multiplayer.is_network_server() and not is_network_master():
213+
_interpolate(delta)
214+
215+
func _physics_process(delta):
216+
# If this player instance is the server, then it
217+
# is the source of truth. It should process the
218+
# buffered input and replicate it back to the client.
219+
if multiplayer.is_network_server():
220+
var curr = OS.get_system_time_msecs()
221+
if (curr - _accum) >= server_tick_interval:
222+
_process_server_input(delta)
223+
_accum = curr
224+
225+
# If this is the entity who sent the input, then reconcile
226+
# with whatever the last known server state is. This might
227+
# contradict what was sent if the sent input was invalid or
228+
# deemed incorrect by the server.
229+
elif is_network_master():
230+
_reconcile(delta)
231+
232+
# Server reconciliation. Directly set the current position to that
233+
# of the last known server state, and then reapply all inputs since then.
234+
# This ensures client consistency with the server state.
235+
# @see https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
236+
func _reconcile(delta):
237+
var last_state = _last_server_state
238+
if last_state:
239+
# set the current position to the last known server state
240+
position = last_state.p
241+
242+
# reapply any input since the last known server state.
243+
var del = []
244+
while not _input_buff.empty():
245+
var cmd = _input_buff.pop_front()
246+
if cmd.seq > last_state.seq:
247+
move_and_slide(cmd.d * speed)
248+
249+
# Entity interpolation. Lerp remote entities to last known server state. This ensures eventual
250+
# remote client consistency with the server state.
251+
# @see https://www.gabrielgambetta.com/entity-interpolation.html
252+
func _interpolate(delta) -> void:
253+
var last_state = _last_server_state
254+
if last_state:
255+
# You can adjust the lerp weight to fit your needs.
256+
position = lerp(position, last_state.p, 0.5)
257+
258+
259+
func _process_server_state(delta):
260+
if _input_buff.empty():
261+
return
262+
var last_seq = 0
263+
while not _input_buff.empty():
264+
var cmd = _input_buff.pop_front()
265+
last_seq = cmd.seq
266+
move_and_slide(cmd.d * speed)
267+
rpc_unreliable("_append_server_state, {"p": position, "seq": last_seq})
268+
269+
remote func _append_server_state(state):
270+
if multiplayer.get_rpc_sender_id() == NetworkedMultiplayerPeer.TARGET_PEER_SERVER:
271+
_last_server_state = state
272+
273+
# Keep a buffer of inputs. This is executed on the server and the client
274+
# that sent the inputs. The server will use this buffer for processing,
275+
# while the client will use it for server reconciliation.
276+
puppet func _buffer_input(input : Dictionary) -> void:
277+
_input_buff.append(input)
278+
279+
.. note:: For more information on entity interpolation, check out `the article by Gabriel Gambetta <https://www.gabrielgambetta.com/entity-interpolation.html>`__.
280+
281+
Conclusion
282+
----------
283+
284+
This tutorial preovided a brief explanation of server authoritative client-server games and explained the challenges that this model brings to player movement, as well
285+
as some of the techniques that can be used to solve these problems. These techniques are commonly used in many online multiplayer games, however depending on your game's
286+
design, other techniques may be required either in lieu of, or in addition to the ones described here. Some of these techniques include lag compensation, which is common
287+
in fast-paced first-person shooters and dead reckoning, prevailent in most online racing games. For more reading on these topics, I can recommend the fantastic book series
288+
by "No Bugs Hare" called *Development and Deployment of Multiplayer Online Games*. Vol I. is available `on Amazon <https://www.amazon.com/Development-Deployment-Multiplayer-Online-Games/dp/3903213055>`__
289+
and the beta versions of the rest of the series is available on `No Bugs' website <http://ithare.com/contents-of-development-and-deployment-of-massively-multiplayer-games-from-social-games-to-mmofps-with-stock-exchanges-in-between/>`__ for free.

0 commit comments

Comments
 (0)