Lockstep Rollback Netcode Demo for GameMaker
A downloadable demo project
This project is an implementation of Deterministic Lockstep with Rollback made in GameMaker Studio 2. The project is open sourced under the MIT license.
The methodology used is described here: https://medium.com/@meseta/netcode-concepts-part-3-lockstep-and-rollback-f70e929...
Architecture
This demo consists of four components:
- The protocol specification (protobuild scripts)
- The connection object (obj_connection)
- The lockstep object (obj_lockstep)
- The player and input objects (obj_player, obj_input)
1. Protocol specification
This project uses Meseta's Protobuild system to construct binary protocols. Once a protobuild packet is specified, a packet can be encoded from or decoded to ds_maps for easy data handling.
Defining a packet
In the following example, a "player_input" message is defined. This would be run once at the start of the program in order to define the "player_input" message, subsequent code would be able to use it for encoding and decoding messages.
protobuild_add_msg("player_input", scr_cb_player_input); protobuild_msg_add_value("player_input", "id", PROTOBUILD_TYPE.u32, 0); protobuild_msg_add_value("player_input", "frame", PROTOBUILD_TYPE.u32, 0); protobuild_msg_add_value("player_input", "hin", PROTOBUILD_TYPE.s8, 0); protobuild_msg_add_value("player_input", "buttons", PROTOBUILD_TYPE.u8, 0); protobuild_msg_add_value("player_input", "aim", PROTOBUILD_TYPE.u16, 0);
protobuild_add_msg() add a message definition. The second argument scr_cb_player_input is the index of a call-back function that will be called when protobuild recieves this message.
Each protobuild_msg_add_value() adds a new value to the message definition. The provided arguments define the value's name (which is used to locate the correct value from an input ds_map), its datatype, and its default value.
Encoding a packet
Once a message is defined, a packet can be constructed using the definition. For example:
// create the data map var pinput = ds_map_create(); ds_map_add(pinput, "id", player_id); ds_map_add(pinput, "frame", current_frame); ds_map_add(pinput, "hin", input_hin); ds_map_add(pinput, "buttons", input_buttons); ds_map_add(pinput, "aim", input_aim); // cerate temporary buffer var buff = protobuild_buffer("player_input"); protobuild_encode_from_map(buff, "player_input", pinput); network_send_raw(socket, buff, buffer_get_size(buff));
Alternatively, a packet can be encoded by directly passing the arguments. Note: this does not support the default values
var buff = protobuild_buffer("player_input"); protobuild_encode_direct(buff, "player_input", player_id, current_frame, input_hin, input_buttons, input_aim); network_send_raw(socket, buff, buffer_get_size(buff));
Decoding a packet
Protobuild provides two decode functions that can be used to handle incoming network packets. Protobuild will decode the packet, and trigger any callbacks provided in the protocol definition. For example, in a network recieve event, the following can be added to trigger the callback function for each recieved message, passing the message contents as a ds_map in the first argument. The ds_map will have the same keys as defined in the message definition.
protobuild_decode_to_map(async_load[? "buffer"], async_load[? "size"]);
Alternatively, an array can be passed instead. The array will have the values in the same order as defined in the message definition
protobuild_decode_to_array(async_load[? "buffer"], async_load[? "size"]);
2. Connection
This demo is built assuming the use of a UDP relay/broadcast server (a server that accepts incoming UDP packets and re-broadcasting the data to all other connected clients). This approach is described in: https://medium.com/@meseta/netcode-concepts-part-2-topology-ad64f9f8f1e6
The approach implemented in the demo can be used with peer-to-peer connections, but it is left as an exercise to the user to implement their own P2P connection scheme. As the client ID is identified in the incoming packet, there is no need link P2P sockets to a specific player, all connections can be pooled to the same recieve function.
A simple UDP broadcast server executable (for windows) is included for demonstratuion purposes. No configuration is necessary, connect to it on port 11000/udp.
Connection state machine
The connection state machine can be found in the obj_connection object. The state machine has the following states:
STATE.relay_connect: In this state a new connection is created, and callbacks are assigned to handle data an connect/disconnect state. The state machine immediately falls through to the next state.
server_socket = netcode_create_connection(server_addr, server_port, network_socket_udp, netcode_relay_rx, netcode_relay_cn, netcode_relay_dc);
STATE.relay_connecting: This is a wait state. The state machine stalls in this state until the callback netcode_relay_cn script moves the state.
STATE.relay_connected: This is the operating state.
STATE.finished_hang: This state hangs. The callback netcode_relay_dc script moves the state here.
Connection Async Networking event
The connection async event triggers callbacks registered by the netocde_create_connection() function according to the type of async network event.
Multiple sockets can be registered, the Async Networking event will select a matching one from the list, and its relevant callback function triggered.
3. Lockstep
The obj_lockstep object is responsible for handling the lockstep method, as well as the network frame timings. Lockstep allows for network frames to be different from simulation frames. It is possible (and perhaps usual) to run the network frame rate slower than the simulation frame rate, allowing for smooth movement and graphics but at a reduced network rate.
The create event holds some key information about the frame rate, in particular the following variables:
frame_skip: sets how many game frames to skip per network frame. Note: although this is a "skip" value, the actual mechanism uses real time, and therefore, the network frame rate is given by (frame_skip+1)/room_speed
input_delay: sets how many network frames player inputs are delayed for. Note: again, this is a "skip" value, the real input delay is given by (frame_skip+1)*(input_delay+1)/room_speed
predict_max: when a client does not recieve input, it enters prediction mode, which makes predicted network frames (in this implementation, it simply assumes continued directional input, and no jumping input). This variable sets the maximum predicted steps before the game lags to wait for more inputs. When valid inputs are recieved while the game is in prediction mode, the game state is rolled back and re-simulated forward.
Lockstep State Machine
The obj_lockstep uses a type of delta-timing as a means to ensure game clients run at the same step rate regardless of machine speed. Its state machine has the following states:
GAMESTATE.registering: In this state, connected clients send the registration packet, sending their (randomly generated) client ID. As soon as enough players connect, each player is assigned a player number depending on the numerical order of their (randomly generated) client IDs. This mechanism allows all clients to automatically determine and assign player numbers without there needing to be a single authoritative host. Note: player ID number collisions are not handled.
GAMESTATE.resync: In this state, clients send a player_sync packet, which contains a full game state for their player character. This is necessary at the start of the game to synchronize state and spawn in players. Note: this state can be used as part of a desync detection, which is not provided in this demo.
GAMESTATE.go: This is the active game state. At each game step, new player input is collected and transmitted, the game simulation is advanced, and if necessary, any prediction or rollback steps are applied as necessary depending on the state of the network.
GAMESTATE.finished: This is a hang state for use at the end of the game.
Player and Input objects
In order to allow network frames to run independantly of game simulation frames, user events are provided in obj_player and obj_input, and are called by obj_lockstep which orchestrates the network frame timings.
User events
The following user events are provided:
User Event 0: used in both the obj_player and obj_input objects to latch inputs in. Inputs are read at the network frame rate.
User Event 1: used to make prediction frames (when no input is available).
User Event 2: used to save the player state to allow later rollback
User Event 3: used to restore player state when rolling back
Step event
The step event contains some code to control exactly how many step events can execute. The number is tightly controlled by obj_lockstep to avoid desynchronization using the variable steps_to_run.
This also means that rollback steps must ensure that any unexecuted steps are executed, so some of the event scripts will force step event execution via a call to event_perform().
Caveats
This demo implements deterministic lockstep with input delay and rollback for educational purposes only. No guarantee is provided that are no desync conditions, and no desync detection is provided. No guarantee is provided that there are no memory leak conditions. Using this implementation for production purpose at your own risk.
Frequently Asked Questions
How do I run this?
- Download and import the .yyz into GameMaker Studio 2.
- Build the project
- Download and run the udp_relay_demo.exe, which acts as a UDP relay server.
- Run two instances of the game that is built. You will see that both clients connect to the relay, and each spawn players. The players can be moved around using the left, right, and up keys.
Both game clients and the UDP relay must be running on the same machine. This is a demo project only. To get it working in a real game project, you will need to create a connection scheme as well as proper server-client connections.
Can I use this in my project?
Yes, I have open-sourced this code under the MIT license (license file is included), you may include it in your own project (including commercial projects) under the MIT license terms. You may find the license file included in the project.
How do I add this to my existing project?
Since GMS2 doesn't currently have a means to import specific objects from one project to another, and since this is a demo project and not an extension, you can use Lazyeye's GMTransfer program to merge the demo's scripts and objects into your existing project: https://lazyeye.itch.io/gmtransfer
Why does the input lag?
The project default has input delay of around 250ms. This can be reduced in code using the input_delay variable inside obj_lockstep.
How do I create a game that can host sessions instead of using the UDP relay?
Unfortunately that is outside the scope of this demo project. I have deliberately avoided going into that implementation to keep the project focused on the details of lockstep/rollback. Please take a look at some gamemaker networking tutorials.
Can I host the UDP relay program somewhere on the internet?
You are free to do so, no warranty is provided for the udp_relay_demo.exe, that tool is provided as-is to help test or demo the project.
Status | Released |
Category | Assets |
Author | meseta |
Made with | GameMaker |
Tags | GameMaker, Multiplayer, netcode, sourcecode |
Code license | MIT License |
Download
Click download now to get access to the following files: