Setting Up A Switching Camera for Top-Down RPG Games in Godot

Setting Up A Switching Camera for Top-Down RPG Games in Godot

End Result

The end result is demonstrated below.

end result video

Introduction

I will talk about how you implement a camera in a top-down RPG game in Godot. Before going through, however, you need to check that you satisfy some requirements and environments listed below:

  • Godot 3.2: Alright, this is not strictly required but it's the latest stable version of Godot 3. This tutorial covers Godot 3. If you've got version 2, there might be some huge differences.
  • C#: This tutorial mainly uses the new C# feature of Godot, which is still to be considered experimental at this point. If you use GDScript, it still should click how you implement a similar logic in that domain.

Also, this method has drawbacks as well as benefits, which is to be discussed at the end of this article.

The Premise

It's actually a bit hard to setup the camera for Godot and it really depends on your needs. My first goal was to implement a camera that will collide with boundaries but that complicated things a lot. Then something clicked. Basically, my game could be divided by section, which utilizes the resolution of the game as constant. To visualize what I'm saying, see the image below:

sections of the whole world

In this image, I split my world into two sections, which are marked with blue rectangle (and pointed at with blue arrows if you cannot see it). These sections are only as large as the resolution of the game. So the logic is simple, when the player collides with one section, switch the camera to that section.

Setting Up Components

The main tip for Godot is to save everything as scene for reusability purposes. Some scenes are not meant to be launched directly. I call these scenes components. These are meant to be injected into a scene and used.

In this case, I need two exact components:

  • An Area2D component called Section.
  • A Camera2D component called Camera.

Whenever the player collides with Section, it will emit two signals for Area2D, named body_entered and body_exited. In body_entered signal, we will invoke a method that loads Camera component on runtime, add it to the tree as a child and set it as the current camera. In body_exited signal, we will do the reverse.

section component scene

This is how Section component looks like. The scene tree is formed as below:

- section: Area2D
-- collision: CollisionShape2D(visible=false)

Notice how collision node's visible property is set to false by default. That's because it would cover entire game world with a blue mask while we develop the game and make it quite harder to see and manipulate the world. Also, collision's boundaries are set exactly to the game resolution as it's seen by the green rectangle above.

section emits two signals on itself:

  • body_entered to _InitCameraSignal() on Section component, which means on itself
  • body_exited to _UninitCameraSignal() on Section component, which means on itself

On the other hand, Camera component is actually a very simple scene, containing only a Camera2D node named camera as root. See below:

camera component scene

The things you need to be sure is to set Anchor Mode to "Fixed TopLeft" so that the camera's origin should be at top-left, which makes it easy to place on game world, and Current to "On" to immediately focus on that camera when the game loads.

The Code

Section component will deal with setting the camera when player collides. Let's start from scratch.

using Godot;
using System;

public class Section : Area2D
{
    private PackedScene cameraScene;
    private Camera2D camera;

    public override void _Ready()
    {
        // to be implemented
    }

    private void _InitCameraSignal(Node body)
    {
        // to be implemented
    }

    private void _UninitCameraSignal(Node body)
    {
        // to be implemented
    }
}

cameraScene property will be used to load Camera component on the fly and camera property will be used to mutate its properties, such as current property of Camera2D.

Whenever a scene loads a section, it will call _Ready method, let's see how that goes.

public override void _Ready()
{
    GD.Print(String.Format("Preparing section: {0}", Name));
    // load Camera component
    cameraScene = (PackedScene)ResourceLoader.Load("res://components/Camera.tscn");
}

When Section is ready, it will load Camera scene once so that it will never do that again. Loading a resource, especially in software types like games where you need to compute and render every pixel on the screen based on user input and do that probably 60 times per second, can be quite resource-draining. That's why we need to load it once and reuse it when in need. Now it's time to write body_entered emitter, _InitCameraSignal method:

private void _InitCameraSignal(Node body)
{
    GD.Print(String.Format("Invoking camera enter signal for area named {0}...", Name));
    // if the body that entered is player and camera is null
    if (body.Name == "player" && camera == null)
    {
        // instantiate camera
        camera = (Camera2D)cameraScene.Instance();
        // add it as child to Section component
        AddChild(camera);
        // set it as current so that Godot will use that camera
        camera.Current = true;
    }
}

Checking for the name of body is vital here because we'd like to filter the others out. The thing to be careful here is that our player must be named player in the tree so that this method can actually work.

And let it run only when the camera is null. Setting the camera null will make more sense with _UnintCameraSignal method below:

private void _UninitCameraSignal(Node body)
{
    GD.Print(String.Format("Invoking camera exit signal for area named {0}...", Name));
    // if the body that exited is player and camera is not null
    if (body.Name == "player" && camera != null)
    {
        // it's not the current camera anymore
        camera.Current = false;
        // remove it from the section
        RemoveChild(camera);
        // set the camera to null
        camera = null;
    }
}

Here we also set the camera to null because we would not like to have more than one camera at a time in our scene. Setting it to null will also result the camera to be freed from the memory after some time.

The final code can be viewed below:

using Godot;
using System;

public class Section : Area2D
{
    private PackedScene cameraScene;
    private Camera2D camera;

    public override void _Ready()
    {
        GD.Print(String.Format("Preparing section: {0}", Name));
        cameraScene = (PackedScene)ResourceLoader.Load("res://components/Camera.tscn");
    }

    private void _InitCameraSignal(Node body)
    {
        GD.Print(String.Format("Invoking camera enter signal for area named {0}...", Name));
        if (body.Name == "player" && camera == null)
        {
            camera = (Camera2D)cameraScene.Instance();
            AddChild(camera);
            camera.Current = true;
        }
    }

    private void _UninitCameraSignal(Node body)
    {
        GD.Print(String.Format("Invoking camera exit signal for area named {0}...", Name));
        if (body.Name == "player" && camera != null)
        {
            camera.Current = false;
            RemoveChild(camera);
            camera = null;
        }
    }
}

Adding Sections in the World

Now the only thing left is to add Section components to our game world.

injecting section to the game world at compile time

We will repeat this many times until we cover all the game world. After adjusting the position of the sections, it will look like below:

sections in the scene tree and the 2d view

And it will change the camera to the other section as we collide with another section.

Final Words and Discussions

This provides a camera that is locked in a particular section. This way, you will not need to deal with the boundaries of the world and how the camera will interact with that. That caused me a lot of headache, so instead of doing that, I thought this method is simpler and more useful in terms of reusability.

This method, however, also brings some considerations and drawbacks as well. One that comes to mind is that we constantly initialize a Camera2D and remove reference (setting it null) as we switch sections, which might be okay for small to middle sized worlds but as the world gets bigger, initializing constantly will bring a resource cost to the table. Also, in big worlds, cameras that are nullified, that have lost reference, will be cleaned after some time by the garbage collector, which will lead to some peaks as GC starts working that will result in frame skips as the player moves on.

This method also does not consider how the entities on unseen sections will react. So, if your game has enemies that has player detection, they might follow player from out of camera. I wanted to keep this as simple as possible, so I did not want to also cover that. Possibly the simples solution for your most basic demo game would be to design the game world with that's in mind. For instance, keeping enemies far from where the player will enter the Section and keep player detection radius as narrow as possible.

That's all. Eray's out.