End Result
The end result is demonstrated below.
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:
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:
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.
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 itselfbody_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:
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.
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:
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.