joshbright.com

Game Dev Diary (Day 3)

Published on: Sunday, December 1, 2024 at 8:00 PM PST

Written by Josh Bright

These are just notes to document how I have setup games in Godot to be able to switch between maps. There’s quite a few moving pieces here so I hope that I can explain things well enough for others to understand.

Node Setup

Player Camera

First thing, is currently when we walk around, the camera stays in the same spot. To fix this, we just need to add a Camera2D as a child to our Player scene. Just by adding this camera, it will now follow the player around, with also lets us walk around the map.

The World Manager Node

This node is a permanent node that stays within the main game tree the entire time. It has a script attached to it which handles the changing of the maps, which those maps will be the only child of the world manager node. This node needs to be a Node2D based node, as we need the child map to have a position in the game. Otherwise, this is just a normal Node2D with a script.

The script for the World Manager initially handles the loading of a map, and a helper function that calls into the current map to find one the positions.

Map Node

Each map will have another Node2D node that will act as the main parent node of the map. This will also have a script attached to it, so that it can handle setting up the teleporters with signals, and a function to find a position.

TileMap

Godot has recently updated how TileMaps work with a new node called the TileMapLayer node. This should make it easier to manage situations where you have multiple tilemap layers and being able to see them easily in the tree. Before, layers were part of the TileMap node itself, so you had to drill down into some menus to see them. For now, we’re just going to have a single layer that is the ground, but i’d expect in the future we will be using some more layers.

Teleporters

A teleporter is an Area2D node, with a CollisionShape2D attached. When a player enters this area, it will emit a signal that the world manager will be listening for, that will trigger the change of the map.

Positions

One last part of the map loading is a list of Marker2D nodes that define various positions on the map, mainly to set where the player should be positioned when they switch to the map from another map.

Here is a screenshot of what a map looks like with all these setup.

Map Example

The Main game script

Lastly, we are going to have a script attached to the main node of the game. This is mainly going to help communicate between the World Manager, and the Player nodes.

Changing Maps

Now that we have a description of the various nodes that need to do something when we switch maps, lets walk through the two scenarios that we need to deal with.

Starting a new game

When we start a new game, we aren’t really moving from one map to another, so we need a way to load things up from scratch. Note that things are a bit rough right now, but as time goes on it will get a bit cleaner. Here’s the main script in all its glory:

Main scene

extends Node2D

var is_new_game = true
var current_map_name = null
var start_map_name = "World"

@onready var world_manager = $WorldManager
@onready var player = $Player

func _ready():
	world_manager.load_map(start_map_name)

# more code here

When the game starts, the first thing that happens is that the game calls into the World Manager, and tells it to load the starting map, “World”.

World Manager scene

The world manager load_map function has been called, here is the code for that function:

# more code

var maps = {
	"World": load("res://maps/World.tscn"),
	"Town": load("res://maps/Town.tscn"),
}

func load_map(map_name):
	# check if we have a map loaded already
	if current_map:
		# if so, remove it
		current_map.queue_free()
		current_map = null

	var new_map = maps[map_name].instantiate()
	call_deferred("add_child", new_map)
	await get_tree().create_timer(0.1).timeout

	current_map = new_map
	current_map.setup_teleporters()
	current_map.connect("teleporter_hit", _on_teleporter_hit)
	map_loaded.emit(map_name)

The first thing we need to do is check if we already have a map loaded or not. In a fresh game case, we don’t. Next off, we find the map in our maps dictionary, and instantiate it. Next, we add that new map as a child to the world manager node. This is what will make the map show up on the screen. We add this node as a child using call_deferred, which ensures that the map is added after the next physics ‘tick’ happens. We add a small delay after adding the child just to be sure that the map is added before we continue. This may not be the best way to deal with this issue, but, it generates no errors and seems to work, so we’ll go with it for now.

Lastly, the world manager will call some functions that are on the Map script to get things setup, and then will emit a signal saying that we have loaded the map. When starting a new game, we aren’t using any teleporters or any of that, so the rest of what happens we can skip. After a map is loaded, the World Manager will emit a signal that the Main scene is listening for, so we next will go there.

Back to Main scene

Up until now, we have really just added a map scene to the game, but the player is not where they should be. This is where we fix that issue. The function listening for the map loaded signal is given a map name, which is the map that was just loaded.

func _on_world_manager_map_loaded(map_name: Variant) -> void:
	var player_pos = null
	if is_new_game:
		player_pos = world_manager.get_marker_position("GameStart")
		is_new_game = false
	else:
		# find the position based on our last map
		player_pos = world_manager.get_marker_position("From" + current_map_name)

	# we can now change the current map name to our newly loaded map
	current_map_name = map_name

	# position player
	player.position = player_pos

The first thing we check here is if we are loading a new game, or if we are switching between a map to a new map. In our case, its a new game, so we set our player position to the GameStart position. This calls into the World Manager, which that then calls into our current map scene. We then set our current_map_name to our new map, and set the position of the player to our GameStart position. We then set our is_new_game variable to false now that we’re done loading things.

The switching of Maps

Now, lets see what happens when we move from here to a new map. Walking onto a teleporter from our newly created map, will start a chain of events that essentially unloads the old map, creates the new map, and then finds out where on the new map the player should be positioned at. Lets start with the player walking onto a teleporter.

Map scene

The script for the Map scenes has a function that listens for any of the teleporters to emit the body_entered signal. We then just emit a new signal from there of the teleporter name. Remember, we also have access to the teleporter due to us binding that variable when we connected the signal. If we didn’t bind the teleporter variable, the _on_teleporter_hit would only have the body of what entered the teleporter to work with.

# Where we bind the teleporter variable
func setup_teleporters():
	for teleporter in teleporters.get_children():
		teleporter.connect("body_entered", _on_teleporter_hit.bind(teleporter))

func _on_teleporter_hit(_body, teleporter):
	teleporter_hit.emit(teleporter.name)

The World Manager is listening for this teleporter_hit signal, so we now go there.

World Manager scene

In this scene, when a teleporter is hit, we just take the name of the teleporter, and call our load_map function with it. Since we now are currently in a map, but want to load a new one, the load_map function will unload the current map first, and then the loading of the new map is the same as when we started a new game.

The big main difference that is left, is when we need to position the player on the new map. Earlier, since we were starting a new game, we positioned the player at the “GameStart” marker, but now we need to position the player at the “FromWorld” marker. This happens back in the Main scene script.

Main scene

Here is our map loaded function that we looked at above. We will be doing things a bit differently this time through it though:

func _on_world_manager_map_loaded(map_name: Variant) -> void:
	print("Map Loaded: ", map_name)
	var player_pos = null
	if is_new_game:
		player_pos = world_manager.get_marker_position("GameStart")
		is_new_game = false
	else:
		# find the position based on our last map
		player_pos = world_manager.get_marker_position("From" + current_map_name)

	# we can now change the current map name to our newly loaded map
	current_map_name = map_name

	# position player
	player.position = player_pos

At this point in the game, our is_new_game variable is set to false, so we need to find the position based upon the last map the player was at. In this case, we’re moving from World to Town, so the last map is World. We call into our world manager asking for the “FromWorld” position, which the Map scene ultimately handles. We then update our current map name, and position the player in its correct spot.

Final Thoughts

Geez, this update took a lot more explaining than I thought it would take, as there is sort of a chain of actions that need to happen to get things working, but I think that’s also why I found it to be a lot of fun to figure out. At this point, things are setup enough that all I need to do to add a new map is to create the tilemap, add the teleporters, and name them the right thing, and lastly add that scene to the world manager script.

Here is the branch from where things are now. Disregard the post saying day 3 and the branch saying day 2, my first blog post day didn’t involve any code, so we’re 1 off right now (one-off errors happen all over the place it seems, hah!): End of Day 2

Lastly, i’m thinking that this may be a good chunk of content that could be better explained as a video, so stay tuned! I also plan on having a small video here, but im finding that the gifs I make are pretty big when it comes to filesize, but once I get that figured out i’ll add some more examples here.

Next, I think I may tackle adding some dialog boxes to be able to communicate stuff to the player easier.