Filler post: creating a crafty custom camera



Hello again. If - for some reason - you're avidly following the project, you may have noticed it's not out yet. Put simply, this is because our eyes have been bigger than our stomachs for a certain final boss.

Versus everything before it, it's a big technical challenge - partly because of our small team, but also because it involves some unique, deep-rooted mechanics that've forced us to rethink (and, annoyingly, reimplement) various in-game objects.

We're going to be talking about one of these objects here: the camera. Be warned, mild spoilers ahead; this is a discussion of a final-boss-specific thing after all. We won't be discussing any code in this post, though there will be a flowchart or two.

Background

Every stage in Feral Flowers is a 40x23 grid of tiles from which there is no escape. This is something that's enabled us to design quickly; there's not that much space in there, so most stages only involve one or two challenges. Boss stages obviously take a little bit longer, given their unique behaviours, and the various dynamic elements in the environment as the fight progresses. The final boss has been a totally different beast, though, because the camera moves. Shocker.

Everyone's favourite board game

Godot, our chosen engine, comes with a pretty good 2D camera - with features such as tracking, zoom, rotation, smoothing, bounds, and offsetting (for eg. shaking the screen, something we definitely do lots of). It does, however, fall short in a couple of areas; with low-pixel games in particular, the tracking can cause jitter. Even the docs say: "[Camera2D] is a simple helper to get things going quickly, but more functionality may be desired". So be it…

Requirements

The final boss stage is (mostly) based on an alternating pattern of letting the player run free (a 'free phase'), then constraining them to a box to do some fighting (a 'fixed phase'). While running free, there are times when we want to constrain the camera in a single axis. That's fine; just stop one of the axes from moving. However, what we really want to do is arbitrarily change the camera's bounds - ie. the furthest it can go in each cardinal direction - without jerking the camera back to said bound if it happens to be out-of-bounds at the point the bounds are changed.

Additionally, in these free phases, we don't want the camera to be absolutely centred on the player all the time. This gives the feeling of "the world moving around the player" - which, at worst, can be a tad vomit-inducing.

So, we'll need to react smoothly to any of the following:

  • Entering a fixed phase, so the camera locks in place
  • Entering a free phase, so the camera starts following the player again
  • The player being followed by the camera while they move at a constant velocity in either axis
  • The player stopping movement, or reversing their direction, in either axis
  • Changing the camera's bounds

That's rather a lot of things to keep an eye on. Additionally, the scripting for the final boss stage itself has been getting rather bloated - so ideally we want to keep this camera code separate, and have the stage 'direct' the camera on each frame as appropriate. What ever will we do?

Extending Camera2D

Godot's docs (as mentioned above) recommend not using its provided camera at all if you've got big tings to be doing. However, there are a couple of tings it does which we'd prefer not to bother implementing ourselves:

  • 'Drag center' positioning: we'd prefer to just tell the camera where the player is and have it centred around them in one way or another
  • Offsetting: this enables us to 'nudge' the camera by a few pixels without interfering with its canonical position - useful for shaking the camera
  • Note that it also deals with bounds, but setting these when the camera is positioned outside of them will cause it to jerk - so we'll roll our own

So let's extend Camera2D's functionality, rather than completely replacing it. Godot makes this easy - we can just attach a script to it:

Normally, the Camera2D instanced in every level isn't scripted. Also, the 'Stage' vs. 'Level' nomenclature will haunt us forever.

Now that's squared away, let's figure out how we're going to deal with the rest of our requirements. Note that this was rather painstaking to figure out; what follows is a simplification of our route to the finish.

Scrolling to a fixed point

On first glance, this is easy enough: every frame, just move the camera a fraction of the difference between its position and said point. In most modern games, this "as the crow flies" approach is usually suitable.

Keep in mind, though, that we generally want to keep things pixel-perfect. Remember that jitter we mentioned earlier? Well, that needs to be accounted for in a couple of ways:

  • In order to stop various objects from jiggling around (notably the player, should they be on a sub-pixel), we need to keep the camera's position integer-rounded.
  • Because we've got so few pixels to work with, scrolling at anything other than a right-angle (or a 45° diagonal) introduces said jitter because of the rounding.

So, we adopt a few policies:

  • Trivially, round the camera's position vector to integers after any update to them.
  • Keep it Manhattan: only move in cardinal directions, at least per axis (ie. moving both up 1 and right 1 per frame looks fine).
  • Move at a fixed speed from start to finish.

What does this look like in practice? Well, if the camera travels at a speed of 1 pixel per frame in either axis, let's move between (0,0) and (7,-4):


Green numbers = frames elapsed. In total, we travelled 4√2 + 3, which you needed to know.

There's one more thing to mention here: in order to keep things a bit simpler, we don't pay attention to the camera bounds when scrolling to fixed points. This isn't too much of a problem in our case, as all of these points are constant, so can be predetermined to be within the bounds. This cheat is also useful for reacting to the bounds being changed (but we'll get onto that later).

Following the player: a simple implementation

The simplest task we can accomplish here is to get the camera following the player at all times. That's easy enough: set the camera's position to the player's position on each frame. This only gets us as far as "having the world move around the player", though, and we've got a fair few more requirements to meet.

Next, let's deal with the bounds: another easy win here (for now). After setting the camera's position, we can just clamp it to the bounds - that is, ensure the camera's X position is within the left & right bounds, and the Y position within the top & bottom bounds. This said, we need to ensure the bounds account for the camera normally being centred on the player - so the actual bounds used will be half a screen more constrained in either axis. So the actual bounds of the camera are a bit smaller than the bounds we've defined for the edges of the visible screen:


So far, though, all we've done is reimplement things that Godot's Camera2D already does. If we change the bounds while the player is being followed, the camera will immediately be clamped to the new bounds, jerking the camera. So, it's time to start customising proper.

A flexbox, if you will

Our first task is to stop it feeling like the world is moving around the player. We can do this by giving the player an invisible box to move around in before the camera starts following them; the camera can then catch up with them in the same manner as our 'scrolling to a fixed point' implementation above, but with a moving target.

Once the camera catches up with the player, we can then check whether they're still moving - and if so, we can continue following them as before. After this, should they change direction, stop for a while, or take the camera to a bound, we stop following them - and the cycle continues. All of this happens while continuing to clamp the camera to the bounds.

(a) resting state, (b) player moves, escaping the box, (c) camera catches up to player, (d) camera follows player while they maintain speed, (e) player either stops or changes direction, and we reactivate the box. 

Dealing with bound changes

Things get even more complicated here; the camera's bounds can be changed by the final boss stage's script. When the time comes to move the camera, we need to check whether it's out-of-bounds before doing anything else.

As we said though, if we're currently focusing on a fixed point, we're ignoring the bounds. If that's not the case - and thus we're in a free phase, with the camera following the player around as above, we need to take corrective action without jerking the camera.

If all of these conditions are met, then, we can start moving the camera in the opposite direction of the broken bound; eg. if we're outside the top bound, move South. Until that's done, we don't attempt to follow the player; you'll recall we're constantly clamping the camera's position to the bounds while following them, so this'd undo all of our hard work in stopping camera jerk.


Putting it all together

We've now defined out all of the behaviours we're going to need. Time to put everything in its proper order - here's that flowchart you were promised (click for a closer view):


Tons of fun. There are a couple of tiny implementation details that we've not gone into with the flowchart (notably, rounding isn't mentioned) - honestly, they'd have introduced a ton of complication for little gain. What you see above is, eh, 95% true to what's actually happening in-game. On the subject:


Later gator

Hopefully not much longer now. On the other hand, Itch says these long-form, technical posts "are not for changelogs, updates, or announcements", so yeah. Get outta here ya little scamp.【星川】

Get Feral Flowers

Buy Now$5.00 USD or more

Leave a comment

Log in with itch.io to leave a comment.