Part 4 - "Bots"
May 17, 2019

Attention: This tutorial was written for Godot 3. Not only the information here contains several flaws, networking has changed significantly in Godot 4.

We now have a synchronized game world, with the possibility of multiple players joining the a server and each one controlling its own avatar. A HUD displays the list of connected players and not only that list is updated when someone connects/disconnects, the amount of player avatars is also kept in sync. The tutorial could have been considered completed, but then there came the desire to add a few "bots" into the game world in order to fill the maximum amount of players.

Now, by bot I mean they will move without any player's interaction. But in order to avoid too much complication, we will deal only with "random movements". By that I mean, when a bot is spawned we will get a random spot within the game window and make the bot move into that place. Once there, we calculate another point and repeat.

The Scene

First, let's begin by creating the bot scene. In other words, create a new scene choosing Node2D as its root and name it BotRoot. As with the player scene, drag in the default Godot's icon into the top left corner and make sure the transform->position of the created sprite node is x = 0, y = 0. Repeat the scaling, setting x = 0.6, y = 0.6 so it doesn't get too big within the world. In order to bring some difference between the bots and the players, change the Material property to CanvasItemMaterial then expand it and change the BlendMode from Mix to Add. Save the scene naming it bot.tscn.

Bot Script

Attach a new script into the BotRoot node, naming the file bot.gd. It should contain a single line, extends Node2D. We will begin the script by declaring a few variables:

After those, we declare the values that must be replicated, position and rotation. Wait, "rotation" you say? Yes, indeed. In this case we will rotate the bot's icon to somewhat point in the direction they are moving to. We could hve done the rotation with the player avatars too. That's relatively easy and is left as an exercise to you, dear reader! Anyway, the declaration of the variables:

bot.gd
const speed_range = Vector2(150, 300)
var dest_location = Vector2()
var start_pos = Vector2()
var count_time = 0
var current_time = 0

# Replicated data
slave var repl_position = Vector2()
slave var repl_rotation = 0.0

Random Spot

The very first function that we will implement is one meant to get a random spot within the game world. Instead of caching the screen size, like many do, we will always obtain those values when calculating the random spot because the screen size may change through a window resize. Of course we could deal with this event, but then, let's keep focus on the multiplayer aspect, shall we? All this function will do is calculate a random point (X, Y) as long as 0xScreenWidth\scriptsize 0 \leqslant x \leqslant ScreenWidth and 0YScreenHeight\scriptsize 0 \leqslant Y \leqslant ScreenHeight. We can easily obtain the screen dimensions by get_viewport_rect().size. With that in mind, the code:

bot.gd
func get_random_location():
	var limits = get_viewport_rect().size
	return Vector2(rand_range(0, limits.x), rand_range(0, limits.y))

Motion Variables

Next we need to calculate the motion variables. Since we will have to do that once in the initialization and then every time the bot reaches the destination, we create a function to perform this task. In this function we obtain the dest_location simply by calling the get_random_location() we just implemented. Then we have to calculate the total amount of time necessary to perform the movement. This will be done by calculating the distance from current position to the destination and dividing that by the movement speed, which will be a random value between speed_range.x and speed_range.y. We then reset the current counting time and the starting reference position. Next we calculate the rotation angle and then apply that angle, keeping in mind that if the destination is directly up, the icon would point to the right, so we have to add a quarter circle (Pi/2) to the resulting angle:

bot.gd
func calculate_motion_vars():
	dest_location = get_random_location()
	count_time = position.distance_to(dest_location) / rand_range(speed_range.x, speed_range.y)
	current_time = 0
	start_pos = position
	var angle = get_angle_to(dest_location) + (PI/2)   # Angle is in radians
	rotate(angle)

Bot Initialization

Now we deal with the initialization of the bot, by implementing the _ready() function. In there all we do is calculate a random modulation color, a random position and then calculate the motion variables:

bot.gd
func _ready():
	# Calculate a random modulation color - randf() gives a random number in the interval [0,1]
	$icon.modulate = Color(randf(), randf(), randf())
	# And a random position - ideally this initial position should be placed on the spawn code
	# however simplicity is required for the tutorial
	position = get_random_location()
	# And the motion variables
	calculate_motion_vars()

The Motion

Finally we can deal with the bot motion. This is done from the _process() function. In there we will only perform the calculation if in the server, which indirectly means that the bot is in its network master. The logic consists in updating the current_time variable and then calculating the alpha, which is merely a percent value towards the total amount of time that must be spent. If this alpha goes beyond 1, we then "normalize" it back to 1 (which means 100%) so the motion doesn't overshoot. Then we calculate the new bot position through a linear interpolation between the starting position and the destination position. After this calculation, we check again the value of the alpha. And if 1 we have to calculate new motion variables. Finally we apply the new position and replicate both position and rotation:

bot.gd
func _process(delta):
	if (is_network_master()):
		current_time += delta
		var alpha = current_time / count_time
		if (alpha > 1.0):
			alpha = 1.0
		
		var nposition = start_pos.linear_interpolate(dest_location, alpha)
		
		if (alpha >= 1.0):
			calculate_motion_vars()
		
		position = nposition
		
		# Replicate values
		rset("repl_position", position)
		rset("repl_rotation", rotation)
	else:
		position = repl_position
		rotation = repl_rotation

If you want to test the bot movement before dealing with the spawning code, open the game world scene and add an instance (or more) into the "level". Then test the game. Once done, delete the instances since we will dynamically spawn them.

Initializing the Random

We have used quite a bit of random values within this part up to this point. The problem is that we haven't initialized the random number generation system! Because of that, every time we open the game we will have the exact same sequence of values being generated. So solve this, we have to call the randomize() function once in the game life time. But where, exactly? Well, we can use the gamestate.gd in its initialization, _ready() function. So, let's do that:

gamestate.gd
func _ready():
	randomize()

Spawning the Bots

The way we will do this follows. We create a new remote function named sync_bots() which will tell how many bots must be in the game world. This function should always be called from the server, which will then calculate the amount of bots and then spread the value across the connected players. Once in the "general execution code", we compare the internal amount of bots with argument value. If the internal amount is bigger, then we have to remove bots from the scene. If the internal amount is smaller, then we have to spawn bots. The way to create and remove scene actors from the game world has already been seen, so I will not detail that part of the code. Anyway, we need to keep track of the amount of spawned bots within each connected player. For that we create a new variable in the gamestate.gd named spawned_bots. We will also need some information regarding the spawned bots so we can remove them later. For that we add a bot_info dictionary:

gamestate.gd
var spawned_bots = 0
var bot_info = {}

As with the player_info, we will take advantage of the possibilities given by the dictionary and use one of the fields to provide the information pointing to the scene that will be used as instance when spawning the bot. While in this tutorial we only have one scene, this is meant to show about the possibility. Anyway, we will initialize this dictionary with 15 entries, which would allow us to spawn up to 15 bots. Adding this to the always necessary server's player, we get the total amount of 16 "players". Keep in mind that we will keep things very simple and won't randomly choose bots from this list, but rather add/remove as a "moving index" pointing to the entries in the dictionary. That said, let's initialize the bot list:

gamestate.gd
func _ready():
	randomize()
	
	# Initialize the bot list
	for id in range(1, 16):
		bot_info[id] = { name = "Bot_" + str(id), actor_path = "res://bot.tscn" }

Then we can work on the sync_bots() function:

game_world.gd
remote func sync_bots(bot_count):
	if (get_tree().is_network_server()):
		# Calculate the target amount of spawned bots
		bot_count = network.server_info.max_players - network.players.size()
		# Relay this to the connected players
		rpc("sync_bots", bot_count)
	
	if (gamestate.spawned_bots > bot_count):
		# We have more bots than the target count - must remove some
		while (gamestate.spawned_bots > bot_count):
			# Locate the bot's node
			var bnode = get_node(gamestate.bot_info[gamestate.spawned_bots].name)
			if (!bnode):
				print("Must remove bots from game but cannot find it's node")
				return
			# Mark it for removal
			bnode.queue_free()
			# And update the spawned bot count
			gamestate.spawned_bots -= 1
	
	elif (gamestate.spawned_bots < bot_count):
		# We have less bots than the target count - must add some
		# Since every single bot uses the exact same scene path we can cahce it's loaded scene here
		# otherwise, we would have to move the following code into the while loop and change the dictionary
		# key ID to point into the correct bot info. In this case we are pointing to the 1
		var bot_class = load(gamestate.bot_info[1].actor_path)
		
		while (gamestate.spawned_bots < bot_count):
			var nbot = bot_class.instance()
			nbot.set_name(gamestate.bot_info[gamestate.spawned_bots+1].name)
			add_child(nbot)
			gamestate.spawned_bots += 1

The last thing we have to do now is call this function. We do this from two different events. Once a player connects and then when someone leaves. To make things simple and be sure the game world is loaded on the client when it joins the server, from the game_world.gd initialization function we check if we are on the server. If so, we directly call this function. Otherwise, we request a remote procedure call directly to the server, which will then broadcast this into the players:

game_world.tscn
func _ready():
	# Connect event handler to the player_list_changed signal
	network.connect("player_list_changed", self, "_on_player_list_changed")
	# If we are in the server, connect to the event that will deal with player despawning
	if (get_tree().is_network_server()):
		network.connect("player_removed", self, "_on_player_removed")
	
	# Update the lblLocalPlayer label widget to display the local player name
	$HUD/PanelPlayerList/lblLocalPlayer.text = gamestate.player_info.name
	
	# Spawn the players
	if (get_tree().is_network_server()):
		spawn_players(gamestate.player_info, 1)
		sync_bots(-1)    # The amount doesn't matter because it will be calculated in the function body
	else:
		rpc_id(1, "spawn_players", gamestate.player_info, -1)
		rpc_id(1, "sync_bots", -1)

Lastly, we want to remove bots when someone leaves the game. Since this function is meant to be first dealt with from the server, we call it from there. Now, remember we have added an event that is meant to be handled only on the server? The function we have created connected to this event is called _on_player_remover(). We can simply add a call to the sync_bots() right after calling the despawn_player() function:

game_world.gd
func _on_player_removed(pinfo):
	despawn_player(pinfo)
	sync_bots(-1)       # Again, amount doesn't matter at this point because the server side section will take care of it

Conclusion

Excellent, now we have "fully" synchronized bots within this little multiplayer project! By fully I mean, the correct amount, rotation and movement! However, you may have noticed that the colors don't match on any of the connected peers. That's because we are randomly selecting the modulate color at the initialization of each bot and not synchronizing that in any way! Although that is relatively simple to fix, I will leave it that way as the most important aspects of the multiplayer system have been covered. At least, I hope so.

This tutorial shows a relatively simple system that is also easy to expand to any needs. Because the players visual representation are provided by dictionary fields, it becomes a simple task of just adding more fields into it and/or change how they are affected by the various events in the game or UI to bring more options regarding how thing will be spawned/shown in the game

While the concept of remote procedure calls can be a bit confusing at first, hopefully the explanation in here as well as the commented code snippets are enough for you to understand what they do and how to use them.

EDIT: Now there is a followup tutorial with more networking. It can be found here. It contains the following topics:


Now take the information you just learned (at least I hope you did), expand and adapt into your project needs and bring on some really fun multiplayer to the world!

Happy forging!

Introduction
Previous123
4