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!
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.
When a building exterior is created, its texture is loaded into the
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:
Door class will be explained later.
Here is an example of an inherited scene.
[Example scene for a building exterior]
Note how the center of
Door is in the center of the
DoorArea polygon. The
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.
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.
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
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:
All of the building scenes are instanced as children of the
which allows me to access them in the script attached to
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
nodes above are
Area2D nodes which only provide us with
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.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.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
Now we just need a way to initialize the three variables in instances of the
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).
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.
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
right would give me
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
CityMapscene 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")
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
# 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
Player.gdscript, I separated the logic for setting the animation (based on the current direction) into its own function. It takes a
Vector2input 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.gdto 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
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
CollisionPolygon2Dshape to prevent the player from walking through walls.
- drag the
Doornode such that its center is where the player should spawn in the scene and set up a
CollisionPolygon2Dshape 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
setupon 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
buildingsnode 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 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