My goal for the past week was

  • Make buildings that the player cannot walk through
  • allow the player to enter doors and load a corresponding indoor scene
  • allow the player to exit and be restored to the correct location on the overworld map.
  • be generic enough that the player can also go through doors within the indoor scene that will take them to other indoor scenes, or a new location on the overworld map.

There seem to be many possible solutions to this, and I was dissappointed by the lack of tutorials for what seems to be a pretty common paradigm. I suppose everyone figures out their own janky way of doing it and then realizes that their solution is actually much more specific to their game than they originally thought.

I ended up redoing this several times and will go over here what eventually ended up working for me. This “teleport” system should be suitable for many top down 2D games. Please let me know if you have suggestions or advice!

Building Setup

This scheme requires that the scene files for buildings adhere to a naming scheme, and that each building scene (outdoor and corresponding indoor scene) adheres to a specific structure. To better achieve this, I created two scene files as “template” scenes for the outdoor and indoor building scenes. All building scenes then inherit one of these. If you aren’t familiar with inheritance in Godot, I found this doc very helpful.

Exterior

Example of the scene tree for an exterior building When a building exterior is created, its texture is loaded into the Sprite node. The Walls CollisionPolygon2D node is given a polygon that corresponds to the area the player should not walk through. Likewise, DoorArea is shaped such that when a player is inside the polygon they can press a button to enter the door. The origin of Door should be the location the Player will spawn when it comes out of the building.

Door has a simple one-line script:

extends Door

The Door class will be explained later.

Here is an example of an inherited scene.

Example outdoor scene of a tanning salon [Example scene for a building exterior]

Note how the center of Door is in the center of the DoorArea polygon. The center of TanSalon is at the very top of the collision polygon. This center defines the z_index of the building when it is placed in a larger map context. When the player walks above this point, it will render behind the building, otherwise it will render in front.

Also note that for most top-down 2D games, a simple collision shape with a rectangle would work for the walls and door area. I’m using polygons because I want my buildings to have this skewed perspective.

Interior

Indoor scenes are set up in the same way, as inherited scenes from a similar template. The only difference is an additional collision shape, since we will now need to define our collision polygon around the four outerwalls which contain the room.

Although the structure of building scenes is enforced by inheriting the template scenes, note that additional nodes can be added to customize. For example, additional sprites like furniture can be added as child nodes and additional doors can be created. Inheritance only enforces a background sprite and at least one door be created.

Naming Convention

While not technically required, I find it much easier to use a strict naming convention for all the scenes. All interior scenes should have the same name as the corresponding exterior scene with the suffix “Interior”. For example, the tanning salon scene may be TanSalon.tscn and its corresponding indoor scene would be TanSalonInterior.tscn.

Additionally, the root node of the building scene should be renamed to it’s filename, as can be seen with the “TanSalon” example above.

Adding Buildings to the main map

My CityMap scene has the following tree:

CityMap scene tree screenshot

All of the building scenes are instanced as children of the buildings node, which allows me to access them in the script attached to buildings with get_children() (which can be used to set all of their z_indexes at once).

After instancing a building I can simply drag and drop it to the proper place on the map.

Connecting the door areas with signals

We need a signal to tell us when the player is in a door area, so that we can switch the scene when they press a button to enter the door. The DoorArea nodes above are Area2D nodes which only provide us with body_entered and body_exited signals, so we will have to keep track of when the player is actually in the body.

To maintain this state, I decided to use an autoloader called Glob for global variables and an autolader called SignalMan for signal management as suggested in these forum answers:

When the player is in a door area, a boolean is_active_door is true. When the player recieves input to interact, it checks is_active_door and calles the enter_door() function on the Root node which will change the scene according to the active door variables which get set by SignalMan.

# SignalMan.gd

extends Node

signal door_area_entered(scene, door, door_facing_direction)
signal door_area_exited()


func _ready():
	var _conn
	_conn = connect("door_area_entered", self, "_set_active_door")
	_conn = connect("door_area_exited", self, "_remove_active_door")

# Set information necessary for scene transition if player enters nearby door
# scene: the filename of the scene to transition to
# door: the name of the door node within the scene to enter
# door_facing_direction: the direction the player should face when they enter scene
func _set_active_door(scene:String, door:String, door_facing_direction:Vector2):
	Glob.active_door_scene = scene
	Glob.active_door_scene_door = door
	Glob.door_dir = door_facing_direction
	Glob.is_active_door = true

# Remove the active door so that the player does 
# not enter any doors when pressing "Interact"
func _remove_active_door():
	Glob.is_active_door = false

The next step is to have door areas emit signals that can be picked up by SignalMan. Recall that every interior and exterior door extends a custom Door class.

# Door.gd

extends Area2D
class_name Door

var door_facing_direction
var scene
var door
	
func _ready():
	var _conn 
	_conn = connect("body_entered", self, "_body_entered")
	_conn = connect("body_exited", self, "_body_exited")
	
func _body_entered(body):
	if body.name == "Player":
		SignalMan.emit_signal("door_area_entered", scene, door, door_facing_direction)
		
func _body_exited(body):
	if body.name == "Player":
		SignalMan.emit_signal("door_area_exited")

With this script, no signals need to be manually connected through the editor, and signals get picked up by SignalMan upon entry and exit of a Door node. Now we just need a way to initialize the three variables in instances of the door class. I do this by adding a “setup” function to Door.gd. In a script attached to the root node of a scene, we can call this setup function on any child door node to initialize those variables.

func setup(door_dir:Vector2, door_scene:String, door_door:String):
	self.door_facing_direction = door_dir
	self.scene = door_scene
	self.door = door_door

So, whenever we make an indoor or outdoor building scene, the only script we need to add is a function call to setup the door, passing along information for

  • the filename of the corresponding scene that the door goes to
  • the direction the player should be facing when it enters that scene
  • the name of the door node in that scene that the player should spawn next to (in case there is more than one).

For example:

extends StaticBody2D

func _ready():
   get_node("Door").setup(Vector2(-1,0), 
   "res://scenes/buildings/TanSalon/TanSalonInterior.tscn", 
   "Door")

We can call this setup function for any number of doors in our scene.

Door Directions

I have constants defined in Glob for the four cardinal directions. I imagine there are multiple ways to do this, but I use Vector2s which correspond to the inputs that make my player move. For example to go up-left-diagonal, one would press the up and left keys which would give me a vector (-1,-1). Going right would give me (1,0).

Name Conversions

In my code, I don’t provide absolute filenames like this, I have helper functions and constants defined in Glob, for example:

  • A function that takes the name of an outdoor scene and returns the corresponding indoor scene.
  • A function that takes the name of an interior scene and the name of a door node in that scene, and returns the name of the node in the CityMap scene that corresponds to that door.

    When we return to the overworld map scene by going through a door in an interior scene, we can’t just get the “Door” node on that scene. It would be something like “World/CityMap/buildings/TanSalon/Door”.

  • Constants that define vectors for the four cardinal directions.

So the above function call may actually look more like this

func _ready():
    get_node("Door").setup(Glob.LEFT, 
	   Glob.make_interior_scene_name(self), 
	   "Door")

Scene switching

We have everything we need to detect when scenes should change and what they should change to, now we just need to actually switch the scene. I achieve this with a function in the script attached to the root node of my Main scene. The root node Root has a child node World. For the duration of the game, Root will always be in scope and the children of World will be switched out to change the active scene. The player must call this function whenever input is given.

# Player.gd
...
if Input.is_action_just_released("interact"):
    if Glob.is_active_door:
	root.enter_door()
...
# Root.gd
...
func enter_door():
	SignalMan.emit_signal("door_area_exited")

	var world = get_node("World")
	
	# Remove old world
	for node in world.get_children():
		world.remove_child(node)
		node.call_deferred("free")
		
	# add new world and player
	var scene = load(Glob.active_door_scene).instance()
	var player = load(PLAYER).instance()
	world.add_child(scene)
	world.add_child(player)
	
	# Put player in correct position
	var door = scene.get_node(Glob.active_door_scene_door)
	player.position = door.get_global_position()
	player.set_player_animation_direction(Glob.door_dir)

😄😄😄

A few last helpful tips

  • You can specify the indoor scene name as a format string, i.e.:
    const SCENE_INDOOR = "res://scenes/buildings/interiors/%sInterior.tscn"
    

    Then when you specify the indoor scene to use, simply construct the filename as SCENE_INDOOR % building.name

  • In my Player.gd script, I separated the logic for setting the animation (based on the current direction) into its own function. It takes a Vector2 input where x = -1 for left, 1 for right, y = -1 for up, 1 for down and diagonals are expressed as a combo of the two. This allows me to set the player direction from Main.gd to ensure the player is facing the right way when it comes out of a door to a new scene.

  • One thing that I learned about and didn’t end up needing yet are collision masks. This was an excellent tutorial on that: https://www.youtube.com/watch?v=IfPnpKcg47Y

Whew

To recap, after implementing this solution, to add a new building, I:

  • create an exterior and an interior scene that inherit from their corresponding templates.
  • create the base image for the outdoor and indoor scene and add it to the sprite texture
  • set up a CollisionPolygon2D shape to prevent the player from walking through walls.
  • drag the Door node such that its center is where the player should spawn in the scene and set up a CollisionPolygon2D shape to detect when the player is near the door
  • Add more doors if necessary
  • Change the root node to the name of the building and attach a script.
  • In the script call setup on each door node in the scene, passing it the relevant information about the scene the door leads to.
  • Instance the exterior of the building as a child of the buildings node in my map scene, and place it where desired!

With this scheme, doors can lead to any scene and can be unidirectional. Additionally, moving a building in the overworld map will not require additional changes to make the player come out at that building.

That’s it for now folks. As I add more buildings I may end up tweaking this solution a bit, but I probably won’t revisit this topic in the log here unless its a significant change.

Gif of player walking around, entering building

[Gif of player entering and exiting a building]

It’s still a little rough, but a visual indicator of whether or not a player can enter a door shouldn’t be too hard to add, as well as a scene transition animation in the enter_door function of the Root node.