Using THNK for a platformer
THNK is not a multiplayer games framework.
THNK at its core is an authoritative games framework. It's a way to build games that follow the authoritative client-server architecture. A game built with THNK is ready to be turned multiplayer by an adapter extension, but is not an inherently multiplayer game.
Therefore, a preview without any adapter will work, and the game will run in singleplayer just fine. When building with THNK, you will start by building the game in singleplayer, and add the multiplayer after through the adapter extensions.
Let's start building in singleplayer!
Let's make a platformer!
First, we'll add a platformer character (with the default controls on) and a few platforms using GDevelop's default behaviors. This is akin to how you would do it in GDevelop - nothing new so far!
If you start a preview, you should be able to jump around on your platforms.
Adding animations to the player
Let's add animations to the player! This is a fairly basic task in GDevelop. But remember, in THNK, events must be in one of two categories: Server or Client. Before adding animations, we need to decide in which section it belongs.
When you add events to your game, you must always ask yourself a few questions:
- Does it require information that the client should not know?
- Is it something that affects other players?
Here, player animations:
- Uses the position of players visible on-screen (to determine if they are moving or not), information that the client has anyway
- Only affects how the local game looks like, not the games of other players
Therefore, it should be in the client section!
Technically, you do not need to have animations as client events, but it is in practice often a better idea. If it was on the server, it would need to be synchronized with the clients. This is problematic for two reasons:
This is bad for bandwidth - each time an animation would change, the change would have to be sent to the clients. With a lot of objects and clients, this can quickly overload the network over transmitting something that the client in a way already knows, as it can infer it from the movement of other objects already. You should always try to reduce the communication between client and server to the minimum to avoid issues on lower speed connections.
This might make animations not match what is displayed. Since the server would handle the animations, the animations would only be updated on server ticks, which may be set at a lower rate than the rendering frame rate. For a few frames, the animation might not match what you would expect until the server sends an update.
Adapting the platformer for multiplayer
Giving each client a player object
We now have a simple singleplayer platformer with animations. Let's add in some multiplayer! First, we'll need to tell THNK what to do when another player connects. We want each player to have their own player character. Therefore, on a player connection, we need to create an object for that player, and delete it when the player disconnects. We will link the object to the player: That will allow us to know what instance belongs to which player. That way, we will know which instance of the platformer character to delete when a player leaves.
You might have noticed that you do not specify to which player you link the object to. That is because THNK has player picking: the "On Connection" event picks the player that connected to be used by the next actions.
If you wish to link an object (or generally use any THNK action that uses the picked player) on a specific other player or outside of a player-picking condition, you can use the action "Pick player" action to specify the player to affect.
If you had an initial instance of the player on the scene, you should remove it! This instance would just be a "zombie" - since it is not linked to any player, none will be able to control it...
Connections and disconnections are, obviously, a server concern, therefore we need to handle them within the server events.
Since we want the clients to be able to see the player objects, we need that object to be synchronized from the server to all clients. To do so, it's very simple - simply add the synchronize behavior (from THNK) to your player object!
Adding player controls for the clients
While the player might be controlling fine in single-player, as it stands, the movement would not synchronize to other clients in multiplayer, despite the synchronize behavior. Why is that?
The reason is that the behaviors run on both the client, and on the server. Since the server cannot read a player's input though, as that is a client task. The platformer behavior will not receive any inputs as far as the server is concerned, and therefore other clients will not see any movement.
Clients still can move their own character as the client code, which does have access to player inputs, also runs the platformer behavior. Only the server can move objects for all players, hence this movement will not be synchornized.
In THNK, all player interactions with the game state, from player movement to using an item in the inventory, needs to be sent as a command from the client to the server. It is then the server's job to validate and process the command, updating the game state accordingly for everyone to see.
In this case, all we need to do is send the inputs from the clients and feed them to the platformer behavior. The platformer behavior will process the way the object should move and update the position automatically, taking care of the "validation and updating game state" part of message handling.
The naive way
Sending a command from the client and receiving it on the server can be done with the corresponding action and conditions:
The smart way
We have those events so far:
They may technically work, but... they are flawed.
Receiving events in a normal event is more readable, but it only allows to process one message per server tick, as the condition is only called once. Letting messages go unhandled and to be handled on the next tick likely will cause problem if many players send this messages a lot. To handle all messages received in a single server tick, use a while event to handle events while some are available to be processed:
The second issue is the kind of message we are sending. When sending messages, you should always keep a few things in mind:
- The connection may be unstable, causing messages to be sometimes delayed
- The traffic between server and client should be as minimal as possible, as to not use more bandwidth than available
Those events are being sent every frame where a key is pressed. That is a lot of messages, and this takes a lot of bandwidth! If for some reason a message is not received by the server before each tick, e.g. because of short disconnects due to connection instability, then as far as the server is concerned, it is as if the player keeps on pressing and unpressing the button, where, in fact, they are still holding it.
To fix that, let's take another approach: We'll only send 1 message when pressing and 1 when releasing the button. That way, we can send way less messages, and if the connection is or becomes unstable, the server will assume the client is continueing to do what they were doing instead of assuming they stopped doing anything.
This solution is more complex, but will provide a much better experience for players.
Of course, we only do this here because it makes sense for this continuous input player movement requires. For example, using a potion in an RPG should be processed only once when requested, in contrast - continuously consuming potions every server tick as long as the client doesn't ask to stop is not good game design :p