Hour 23. Game 3: Networked Bomberman Clone
What You’ll Learn in This Hour:
Handle networking in a real game
Use the TileMap and TileSet tools to create a map
Combine the Sprite and AnimationPlayer for your animations
Use AutoLoad to make a script global
In this hour, we will create our final game. It is a clone of the Nintendo Entertainment System-era legendary game Bomberman, but with a killer
feature the original didn’t have in its time: networked multiplayer. The game consists of multiple players evolving in a top-down labyrinth with the
ability to plant bombs to break walls and kill other players, increasing their scores. This game is pretty classic, and making a single-player
version of it shouldn’t be too much trouble after what we’ve learned from games 1 and 2, so it will allow us to focus more on the networking
aspects.
Concept and Design
Each player is controlled by its own instance of the game. Ideally, each instance is on its own computer with a real human player. But if you lack
spare computers and/or friends with whom share your joy of Godot, it’s perfectly fine to start the game multiple times on your own computer
and switch from one window to another to control each player in turn.
As said earlier, players evolve in a tiled-based labyrinth, so they can move vertically and horizontally with the arrow keys and plant bombs with
the space bar. Once planted, a bomb will wait two seconds before blowing up. Given it’s a tiled-based game, the bomb detonates following
vertical and horizontal tiles (it doesn’t propagate in diagonal tiles).
The detonation propagates until it has reached two tiles or hits a solid wall. On the other hand, softer walls are destroyed by the explosion, and
players are killed.
When killed, a player returns to its beginning position, and its killer gets 100 more points (unless the player committed suicide, thus getting −50
points)
FIGURE 23.1
Screenshot of the final game.
Given that it’s a multiplayer game, we need to build a main menu to choose whether we want to start the game as a server or to join an existing
game as a client. Finally, we need a lobby in which players wait once they join until the server decides it’s time to start the game.
Bootstrap the Project
You know the drill: create a new project and add the classic folders (scene, scripts, etc.) to have a nicely organized project.
FIGURE 23.2
Our project directory.
Before we start jumping on the code, you should copy the fonts and Sprite resources from the Hour 23 folder. Of course, when reading this
tutorial, don’t hesitate to have a look at the scripts or open this project with the Godot editor if you get stuck.
Setting Up the Scenes
As usual withGodot, we first divide our game into small scenes, allowing us to break down complexity and improve modularity:
TABLE 23.1 Scenes Needed for the Game
Scene
Description
Menu and
Lobby
Given how simple the menu and lobby scenes are, we can combine them into a single scene and choose which one to display (or
none once the game is started).
This scene consists of a
Control node (this is the base node for UI) with two
Control children (one for the menu, one for
the lobby), each one composed of a mix of
Label,
Button, and
Input.
Arena
This is our main scene where the instantiated player scenes evolve and the bomb and explosion scenes are created. We also have
to handle the labyrinth here, which is the perfect use case for a
TileMap node and the starting position for players with
Position2D nodes. Finally, this scene is responsible for displaying the score for each player through a simple
Label.
Player
The player scene consists of a
KinematicBody2D with a
CollisionShape2D, which allows us to easily move it and handle
its collisions. We also need a
Sprite and an
AnimationPlayer to display the player on the screen and animate it when it is
moving. Finally, a
Timer node is used to avoid the player spamming bombs.
111111111
22222222Bomb
Given that the bomb won’t move once planted, we can represent it with a
StaticBody2D with a
CollisionShape2D. Just
like for the player, we use
Sprite and
AnimationPlayer nodes to help players spot when a bomb is about to blow up.
Speaking of this, a
Timer allows us to configure when the explosion occurs.
ExplosionBecause an explosion propagates by following the tiles, we can rely on the arena’s tilemap to handle collisions. This means our
explosion scene has to display a per-tile explosion animation using a
Sprite and an
AnimationPlayer.
As we saw in Hour 21, one of the big advantages of the Godot high-level network system is that it allows us to first focus on developing a
single-player game, then add multiplayer capability to it by declaring how nodes should synchronize together. To follow this idea, we’ll start by
making a single-player Bomberman _game, then we’ll switch our focus to making it multiplayer.
Making the Scenes
Let’s look at how to make the scenes.
Creating Player, Bomb, and Explosion Scenes
Start with the most important scene: the player. Often, it is one of the most complex scenes of the game. However, our game is still small, so
there is no reason to panic. Hour 9 told us that for a 2D platform game,
KinematicBody2D is the best root node for player-controlled
scenes, given that it isn’t affected by physics while it still allows us to detect a collision and move it accordingly if needed.
As you already know, a
KinematicBody2D needs a collision shape to work, so we’ll add a
CollisionShape2D and configure its
Shape property. Given that our player will move on tiles, we may be tempted to use a rectangular shape the size of a tile. However, this causes
continuous collisions that often end up with our node becoming stuck. So, it’s a much better solution to use a circle collision shape that’s
slightly smaller than the size of a tile.
Speaking of which, we should choose the tile size of 32 pixels, so we will scale all of our Sprites and collision shapes to this size.
We will also add as a child to the root node: a
Timer node to control the rate of fire of the player. We’ll set its Wait Time to one second,
and, given that this timer is triggered by code when a bomb is planted, select the One Shot property and make sure Autostart is disabled.
NOTE
Collision Shape and Scaling
A reminder from Hour 9: be careful when scaling collision shapes. Make sure they are uniform and have nonnegative values. Otherwise,
strange things will happen with your physics.
It’s time to make the player visible. We could use the simple
AnimatedSprite for this task, but to add a bit of variety, we’ll choose the
more powerful
Sprite and
AnimationPlayer combo this time. We aren’t in Kansas anymore.
First, add the
Sprite node as a child of the root, then configure it using the Texture property. Click on load and select
“sprites/player1.png.” At this point, you should realize that the image we loaded is not a single Sprite but a spritesheet (an image containing
one next to other, multiple Sprites), so configure the
Sprite accordingly with the Animation:Vframes and Animation:Hframes properties
(in our case, the spritesheet contains one row and three columns, so Vframes = 1 and Hframes = 3).
Now we can use the Frame property to pass through the animations and choose the default one. As you can see, our spritesheet is composed
of three Sprites: an idle pose and two steps that compose a primitive walking animation (Figure 23.3).
FIGURE 23.3
Sprite configuration on the player node.
Using the Frame property by hand (or even scripting its update withGDNative) is a no-go when you can use a shiny tool like the
AnimationPlayer. Add this node (make sure it is a direct child of the root) and start configuring its animations.
Click on the
AnimationPlayer, and the animation menu will open on the lower part of the editor. Click on
to create a new animation
and call it idle. Now use the Scene Tree viewer to select the Sprite node. In the Inspector, find the Animation:Frame property and make sure
its value is 0. Now click on
, a popup that asks you if you want to add a new track to your animation. Click “create.”
FIGURE 23.4
Idle animation for the player.
You have created your first animation composed of . . . one frame. This seems silly putting it this way, but this means you can now easily
improve the one-frame idle animation. Once the walking animation is triggered, you’ll need to trigger this simple idle animation, or the player
will walk forever.
Speaking of the walking animation, it is the same thing as idle animation, except you click two times on the
button next to the
Animation:Frame property (once per frame). This time, be careful to configure the Length and Step property of your animation; otherwise,
you will end up with something clunky (typically frame 1 at 0 seconds, frame 2 at 0.1 seconds, then frame 1 again 0.9 seconds later when the
111111111
22222222animation loop’s given default Length is 1 second). Also, don’t forget to set the track to
Discrete; otherwise, the default continuous mode
blends your two frames together and you’ll end up with a single frame playing continuously (Figure 23.5).
FIGURE 23.5
Walking animation for the player.
Finally, you should have a third and last animation to do the respawn on. Do the same steps, but this time, don’t change the Sprite, but make it
blink (it will help the player spot where his character has respawned). To do so, use the
next to the Visibility:Visible property. Note that
this is where using an
AnimatedSprite for our animation would fall short (Figure 23.6).
FIGURE 23.6
Respawn animation for the player.
TRY IT YOURSELF
Creating the Bomb Scene
The bomb scene is pretty similar to the player scene:
1. Create a
StaticBody2D node as root and rename it “Bomb.”
2. Add a child
CollisionShape2D and activate its Disabled property. The idea is that a player plants a bomb on the tile on
which it currently stands, so you’ll want to wait a bit before enabling the bomb physics to let the player leave the tile and avoid
glitches.
3. Add a
Timer, rename it “EnableCollisionTimer,” and configure it with Wait Time = 0.5, One Shot = True, and Auto Start =
True.
4. Create a
Sprite and set the Texture to the “sprites/bomb.png.” Like for the player, it is a 3 × 1 spritesheet that makes the
bomb get redder the closest it is to exploding.
5. Finally, create and use
AnimationPlayer, configure a default animation lasting 2 seconds, and change the bomb color at
0, 1, and 1.5 seconds. Activate the Playback:Active chec
k
box to start the animation when the node gets created.
Note that we don’t need to create a
Timer to control when the bomb will blow: we will obtain similar results by connecting to the
animation_finished event provided by our
AnimationPlayer.
TRY IT YOURSELF
Creating the Explosion Scene
The explosion scene is even simpler:
1. Create a
Sprite node as root, call it “Explosion,” and assign to it the “sprites/explosion.png” image. This time, the
spritesheet is a 3 x 7 representing a seven-frame center and side and end explosion animations.
2. Add an
AnimationPlayer and create three animations: “explosion_center,” “explosion_side,” and “explosion_side_border,”
each 0.35 seconds long with steps of 0.05 seconds.
Creating the Arena Scene
We start with a
Node2D as root of the scene (don’t forget to rename it “Arena” for the sake of clarity). Strictly speaking, this node is not
really useful, but it allows you to better divide the subnodes.
To construct the labyrinth, you can create a scene for each type of block, then instantiate and place them by hand on the Arena scene.
However, this is a really cumbersome task, and we have a much better tool to achieve this: the
TileMap node. As it name indicates, this
type of node allows you to easily select and place tiles on your scene.
But before you can use a
TileMap, you’ll need to build a
Tileset that defines different tiles to be placed. For our game, we need three
types of tiles:
BackgroundBrick: represents the ground where the players will walk
SolidBrick: represents the wall of the arena; the player cannot cross them, and explosions are stopped
BreakableBrick: blocks players like SolidBrick; player is destroyed once hit by an explosion
111111111
22222222Create a new scene, call it “scenes/tileset-source.tscn,” and add a root
Node2D. Now, we are going to add
Sprite children nodes,
with each of them representing a tile. Make sure to give a meaningful name for each of these nodes, as the tiles will keep them once imported
into the
TileMap.
For each
Sprite, load “sprites/bricks.png” in the Texture property (once again, it’s a 3 × 1 spritesheet, so correct Vframes and Hframes
accordingly) and configure the Frame property so that each tile is different.
On top of that, add a
StaticBody2D with a
CollisionShape2D (with a
RectangularShape2D of 32 × 32) child to the SolidBrick
and BreakableBrick (Figure 23.7).
FIGURE 23.7
TileSet source scene.
Now open the menu Scene –> Convert To . . . –> TileSet and save the tileset as “scenes/tileset.res.”
Back to the arena scene. Add a
TileMap child node, configure the
Tileset property to load the shiny new tileset, and start painting the
map with tiles (Figure 23.8).
FIGURE 23.8
Drawing the tilemap.
Once you’re happy with the results of your map, move on by adding spawn positions for the player. Add a
Node2D and name it
“SpawnPositions.” Inside it, add four
Position2D where you want your players to start. (Of course, they should be placed on a
BackgroundBrick, since you don’t want a player to get stuck in a wall.)
Now, create another
Node2D and name it “Players,” which is the node where you’ll register all player scenes to easily iterate through them
(more on this in the scripting part).
Finally, create a simple
Label node named “ScoresBoard” that displays the score of each player.
FIGURE 23.9
Scene Tree of the player, bomb, explosion, and arena.
NOTE
Make the Draw Order Right
Even in 2D, the Z axis is useful to determine what is displayed on top of what. Here, we want the z value for the arena scene to stay at the
default value of 0, then set it higher for bomb, player, and explosion, in this order.
Scripts and Input
To control the player, first attach a script to the player scene root node, save it as “scripts/player.gd,” and start tweaking it (see Listing 23.1).
LISTING23.1 Player Movements with Kinematic Body — player.gd
Click here to view code image
const WALK_SPEED = 200
var dead = false
var direction = Vector2()
var current_animation = “idle”
func _physics_process(delta):
if dead:
return
if (Input.is_action_pressed(“ui_up”)):
direction.y = - WALK_SPEED
elif (Input.is_action_pressed(“ui_down”)):
direction.y = WALK_SPEED
else:
direction.y = 0
if (Input.is_action_pressed(“ui_left”)):
direction.x = - WALK_SPEED
elif (Input.is_action_pressed(“ui_right”)):
direction.x = WALK_SPEED
else:
direction.x = 0
move_and_slide(direction)
111111111
22222222rotation = atan2(direction.y, direction.x)
var new_animation = “idle”
if direction:
new_animation = “walking”
if new_animation != current_animation:
animation.play(new_animation)
current_animation = new_animation
It shouldn’t be a surprise now: create a _physics_process() function to control the movement of the player. Use the move_and_slide()
function provided by the
KinematicBody2D, which automatically takes care of moving the player and handling collisions for you. Update
the animation if needed (because re-setting the animation at each frame would make it only play the first frame of it).
Continue by creating a _process function to allow the player to plant bombs (Listing 23.2).
LISTING23.2 Player Planting Boms
Click here to view code image
var can_drop_bomb = true
var tilemap = get_node(“/root/Arena/TileMap”)
func _process(delta):
if dead:
return
if Input.is_action_just_pressed(“ui_select”) and can_drop_bomb:
dropbomb(tilemap.centered_world_pos(position))
can_drop_bomb = false
drop_bomb_cooldown.start()
sync func dropbomb(pos):
var bomb = bomb_scene.instance()
bomb.position = pos
bomb.owner = self
get_node(“/root/Arena”).add_child(bomb)
func _on_DropBombCooldown_timeout():
can_drop_bomb = true
Like its name suggests, connect _on_DropBombCooldown_timeout() to the player DropBombCooldown timer.
Bomb and Explosion
Start by creating “scripts/bomb.gd” and connect it to the bomb scene (see Listing 23.3). This script does three things: first, it enables the
CollisionShape2D once the
Timer has finished, then it waits for
AnimationPlayer to end spawn explosion scene instances
according to the type of tiles present on the
TileMap, and last, but not least, it finishes by destroying itself (because it blew up,
remember?).
LISTING23.3 Bomb Explosion Expanding Algorithm
Click here to view code image
extends StaticBody2D
const EXPLOSION_RADIUS = 2
onready var explosion_scene = preload(“res://scenes/explosion.tscn”)
onready var tilemap = get_node(“/root/Arena/TileMap”)
onready var tile_solid_id = tilemap.tile_set.find_tile_by_name(“SolidBrick”)
func propagate_explosion(centerpos, propagation):
for i in range(1, EXPLOSION_RADIUS + 1):
var tilepos = center_tile_pos + propagation i
if tilemap.get_cellv(tilepos) != tile_solid_id:
# Boom !
var explosion = explosion_scene.instance()
explosion.position = tilemap.centered_world_pos_from_tilepos(tilepos)
get_parent().add_child(explosion)
else:
# Explosion was stopped by a solid block
break
func _on_AnimationPlayer_animation_finished( name ):
…
# Propagate the explosion by starting where the bomb was and go
# away from it in straight vertical and horizontal lines
propagate_explosion(position, Vector2(0, 1))
propagate_explosion(position, Vector2(0, -1))
propagate_explosion(position, Vector2(1, 0))
propagate_explosion(position, Vector2(-1, 0))
…
111111111
22222222The important point here is how you use
TileMap’s map_to_world(), get_cellv() and find_tile_by_name() to retrieve the tiles to
instantiate an explosion scene.
Speaking of explosions, it’s time to create and attach its script “scripts/explosion.gd.” The idea is roughly the same as in bomb.gd: use
TileMap to retrieve the tile in which the explosion takes place, making sure this tile is a BackgroundTile. Then retrieve the players’ nodes (now
we are happy to have this “Arena/Players” node) and check for each to see if it is standing on the exploding tile (Listing 23.4).
LISTING23.4 Explosions
Click here to view code image
extends Node2D
onready var tilemap = get_node(“/root/Arena/TileMap”)
onready var animation = get_node(“AnimationPlayer”)
func _ready():
# Retrieve the tile hit by the explosion and flatten it to the ground
var tile_pos = tilemap.world_to_map(position)
var tile_background_id = tilemap.tile_set.find_tile_by_name(“BackgroundBrick”)
tilemap.set_cellv(tile_pos, tile_background_id)
# Now that we know which tile is blowing up, retrieve the players
# and destroy them if they are on it
for player in get_tree().get_nodes_in_group(‘players’):
var playerpos = tilemap.world_to_map(player.position)
if playerpos == tile_pos:
player.damage()
Just like for the bomb, we should also add a function connected to the
AnimationPlayer’s end of animation to free the Explosion node
once it is no longer needed.
TRY IT YOURSELF
Single-player Bomberman
With a bit of polish, your _Bomberman _game will be playable:
1. Add a player scene to the Arena/Players node.
2. Configure the arena scene as the main scene of your God
o
t project.
*3. Hit run and start playing your _Bomberman.
FIGURE 23.10
Our game running in single-player mode.
4. You can now add a second player to your scene and correct the player.gd script to handle different keys depending on which
Player node is currently processed. Now you can have a real duel.
Enter Multiplayer
As you saw earlier, you cannot start the game right on the arena scene if your game is multiplayer: you have to let the player first choose if he
wishes to host a game or join one. So, you should create the lobby scene, which cumulates the main menu with the lobby menu where users
are listed. This is mainly the GUIwidget placing and signaling connection, and doesn’t have much to do with multiplayer, so we leave it to you
(just copy/paste “scenes/lobby.tscn” and “scripts/lobby.gd” from the Hour 23 folder).
FIGURE 23.11
AutoLoad configuration for gamestate.gd.
On top of that, we should keep information about the players (e.g., nicknames) before the Arena scene is created (so we cannot store that
under “Arena/Players/”).
One solution is to set the lobby as the main scene, then store players’ information in a script connected to the root node of this scene. This is
doable; however, it’s more elegant to store that information in an AutoLoad and have them available in all scenes, even if you decide to
remove the lobby scene (for example, if you start the game as a dedicated server).
Create a new “scripts/gamestate.gd,” then select Project > Project Settings > AutoLoad, load the newly created script, and make sure
Singleton is set to Enable (Figure 23.12).
111111111
22222222FIGURE 23.12
AutoLoad configuration for gamestate.gd.
Now, open the gamestate.gd script and copy the host_game() and join_game() functions from Listing 23.1. Those functions are called when
the user clicks on the host or join buttons, along with hiding the main menu to show the list of players waiting for the game to start.
As you saw in Hour 22, a player joining or leaving the game triggers a signal that connects to a function to keep the list of players up to date
(Listing 23.5).
LISTING23.5 Gamestate Network Signals Handling
Click here to view code image
var players = {}
func _ready():
get_tree().connect(“network_peer_connected”, self, “_player_connected”)
get_tree().connect(“network_peer_disconnected”, self,”_player_disconnected”)
get_tree().connect(“connected_to_server”, self, “_connected_ok”)
get_tree().connect(“connection_failed”, self, “_connected_fail”)
get_tree().connect(“server_disconnected”, self, “_server_disconnected”)
func _player_disconnected(id):
# Each peer get this notification when a peer diseapers,
# so we remove the corresponding player data.
var player_node = get_node(“/root/Arena/Players/%s” % id)
if player_node:
# If we have started the game yet, the player node won’t be present
player_node.queue_free()
players.erase(id)
func _connected_ok():
# This method is only called from the newly connected
# client. Hence we register ourself to the server.
var player_id = get_tree().get_network_unique_id()
# Note given this call
rpc(“register_player_to_server”, player_id, player_nickname)
# Now just wait for the server to start the game
emit_signal(“waiting_for_players”)
func _connected_fail():
_stop_game(“Cannot connect to server”)
func _server_disconnected():
_stop_game(“Server connection lost”)
Here, the _stop_game() function disables the network and switches back to the main menu, but that is up to you.
More interesting is the RPC call on register_player_to_server() function. The idea is to have the joining client call the server to communicate
his nickname. In return, the server tells him if he can join the game (if the game is not yet started and if there are less than four players). It turn,
the server calls the clients with RPC to notify them of the newly joined one and vice versa (Listing 23.6).
LISTING23.6 Gamestate Register New Player
Click here to view code image
master func register_player_to_server(id, name):
# As server, we notify here if the new client is allowed to join the game
if game_started:
rpc_id(id, “_kicked_by_server”, “Game already started”)
elif len(players) == MAX_PLAYERS:
rpc_id(id, “_kicked_by_server”, “Server is full”)
# Send to the newcomer the already present players
for p_id in players:
rpc_id(id, “register_player”, p_id, players[p_id])
# Now register the newcomer everywhere, note the newcomer’s peer will
# also be called
rpc(“register_player”, id, name)
# register_player is slave, so rpc won’t call it on our peer
# (of course we could have set it sync to avoid this)
register_player(id, name)
slave func register_player(id, name):
players[id] = name
Eventually, the user on the server will lose patience and hit the start game button. This sends an RPC to everybody (including himself) on the
start_game() procedure (Listing 23.7).
LISTING23.7 Gamestate Start Game
Click here to view code image
sync func start_game():
111111111
22222222# Load the main game scene
var arena = load(“res://scenes/arena.tscn”).instance()
get_tree().get_root().add_child(arena)
var spawn_positions = arena.get_node(“SpawnPositions”).get_children()
# Populate each player
var i = 0
for p_id in players:
var player_node = player_scene.instance()
player_node.set_name(str(p_id)) # Useful to retrieve the player node with
a node path
player_node.position = spawn_positions[i].position
…
player_node.set_network_master(p_id)
arena.get_node(“Players”).add_child(player_node)
i += 1
…
emit_signal(“game_started”)
Here, we create the arena scene and add to it a player scene instance per peer connected. Be careful to configure each player scene instance
with a different master corresponding to its peer; otherwise, only the server will be able to play.
Synchronization in Player, Bomb, and Explosion
If we now try to run the game in multiplayer, we can start a server, connect the client, and even create the Arena scene on each connected peer
with all our on players on it.
But the hard truth hits us as soon as we start sending inputs to our game: there is no synchronization between peers on the player, bomb, and
explosion scenes. Now is the time to fix this.
Concerning the player scenes, we already configured them to be owned by their respective peers. This means we can easily use a
is_network_master() call to run the code responsible for processing inputs on the master peer only (Listing 23.8).
LISTING23.8 Player Controlled Only by its Master Peer
Click here to view code image
func _physics_process(delta):
if not is_network_master():
return
if not dead:
if (Input.is_action_pressed(“ui_up”)):
…
rpc(‘_dropbomb’, tilemap.centered_world_pos(position))
…
func _process(delta):
if not is_network_master() or dead:
return
if Input.is_action_just_pressed(“ui_select”) and can_drop_bomb:
…
move_and_slide(direction)
_update_rot_and_animation(direction)
# Send to other peers our player info
# Note we use unreliable mode given we synchronize during every frame
# so losing a packet is not an issue
rpc_unreliable(“_update_pos”, position, direction)
sync func _dropbomb(pos):
var bomb = bomb_scene.instance()
bomb.position = pos
bomb.owner = self
get_node(“/root/Arena”).add_child(bomb)
slave func _update_pos(new_pos, new_direction):
position = new_pos
direction = new_direction
_update_rot_and_animation(direction)
Now the trick is to separate the code responsible for handling inputs from the one that actually updates the node’s state (this is, what we do for
dropbomb()). Note that sometimes it’s easier to create a function specially dedicated to the slave synchronization: here, we work directly on
the master position and direction properties, then use _update_pos() on the slaves. This has two advantages: first, it avoids creating
temporary values to store the new direction and position before calling the RPC, then it allows us to use rpc_unreliable to call the slaves (the
call is done on each frame, so we don’t care if we lose synchronization from time to time), given that the master always keeps the real value of
those properties.
What about the bomb and explosion scenes, you may ask? Well, good news first: our bomb scene is totally deterministic across the peers, so
we don’t have to do anything for it. Regarding the explosion scene, given that it is created by the bomb, you can be sure it will spawn in the right
place and blow at the right time. The trick is that, given how the players are synchronized with an unreliable RPC, you shouldn’t check for
collision with them on each peer (otherwise, one peer may be lagging and a player may be considered killed when others aren’t killed).
The solution is simply to delegate the explosion scene’s player collision check to the server, which will RPC the player.damage() for
synchronization (Listing 23.7).
LISTING23.7 Explosion Player Collision Controlled by Master
111111111
22222222LISTING23.7 Explosion Player Collision Controlled by Master
Click here to view code image
func _ready():
…
if not is_network_master():
return
# Now that we know which tile is blowing up, retrieve the players
# and destroy them if they are on it
for player in get_tree().get_nodes_in_group(‘players’):
var playerpos = tilemap.world_to_map(player.position)
if playerpos == tile_pos:
player.rpc(“damage”, owner.id)
Finally, you can add a sync keyword to our Player.damage function, and the game should be complete. Congratulations!
NOTE
Who’s Master, Who’s Slave?
Remember, if not configured explicitly, master/slave properties inherit their parent, and the default root scene master is the server. So in our
game, even if we create bombs with the Players node with various masters, we always add them to the arena scene, which is controlled by the
server. In the end, it’s the server that is master of all the bomb (and explosion) scenes.
TRY IT YOURSELF
Become a Cheater
It’s easy to exploit when networking, and we will illustrate this by abusing the sync Player.damage() procedure:
1. Copy your game project in another folder.
2. Open the “scripts/player.gd” file and modify the _process() function to do an abusing RPC call of the player.damage()
procedure (see Listing 23.9).
3. Start the original game as server (you can spawn some clients as well) and start the modified one as client.
4. Now, try planting a bomb with the malicious server. The other players get killed instantly (and each peer, including the server).
What happened? As we discussed in Hour 22, sync and remote can be called by any peer (slave or master, it doesn’t matter),
so you should be careful on what you allow them to trigger.
LISTING23.9 Player Adds Kill
Click here to view code image
func _process(delta):
if not is_network_master(): # don’t do the exploit on our own player node !
if Input.is_action_just_pressed(“ui_select”):
rpc(“damage”, id)
…
Summary
In this hour, we completed another game from scratch! This time, we used the powerful
AnimationPlayer that can use anything in your
scene to create animation, and we also mixed the
TileMap with
KinematicBody2D players to re-create the NES-era style of
gameplay. On top of that, we added to the game network capabilities and multiplayer, and saw how to modify its scenes to do synchronization
in a deterministic way while trying to keep away from cheaters.
Q&A
Q. My client cannot join the server.
A. Make sure you have configured the same port on both client and server. If the two are not on the same computer, you should verify the IP
address as well and make sure there isn’t a firewall messing with them. (Did we tell you networking is a complicated thing?)
Q. Can I configure client networking before a server one?
A. You can configure networking in any order. If client is ready before server, it will poll for it until it is ready. However, keep in mind that a
timeout will occur if the server takes too long to respond.
Workshop
See if you can answer the following questions to test your knowledge.
Quiz
111111111
222222221. What can I animate with a
AnimationPlayer?
2. What do I need to use
TileMap?
Answers
1. Every property in any node; it’s that powerful.
2. You need a
Tileset that is generated from a scene composed of a
Sprite. You can also add collision nodes and shapes to
handle physics. However, keep in mind that you cannot assign scripts to these nodes.
Exercises
There are some parts of the game we left on the side to simplify things. Now would be a good time to implement them:
1. Add variety among the players by giving each a different color.
2. Handle respawn feature: once hit, a player should move to its initial position, then a blinking animation triggers, during which it cannot
move.
3. Create the scoring system: hitting a player gets 100 points, killing yourself makes you lose 50 points. And remember, each feature
should be synchronized across the network and cheater-free.
111111111
22222222