Jetboard Joust Devlog #53 – Camera Obscura

In the unlikely event that you’ve been reading this blog from the beginning you might remember that the camera in Jetboard Joust has always been something of a bugbear for me.

Way back in the early stages of development I wasted countless hours trying to get a camera action that I was happy with but, after failing miserably (once I started to add enemies the camera tracking solution I described in the blog post above proved woefully inadequate), decided to go for the simplest solution of locking the camera to the player centre-screen and leaving it at that.

Thing is – this kind of worked. Maybe because the camera is horizontal-only and the player’s horizontal motion smoothly accelerates and decelerates with no sudden changes? The game was perfectly playable and there were no sudden camera movements that looked out of place or anything.

But… it was just a bit boring and I always thought I could make it better, so as I’m nearing ‘playable alpha’ stage I decide once more to revisit. I set up a separate ‘Camera’ class (rather than putting all the camera code in the main game class as I had before) and attempted to approach things a bit more methodically.

Though it probably seems blindingly obvious I had a bit of an epiphany on realising that camera motion is really comprised of two separate elements, the camera target (ie what you want to be the centre of attention) and the way the camera moves to that target.

I decided to tackle the camera motion itself first. Many online articles talk about Lerp smoothing as the de-facto way to deal with this so this seemed a good place to start. Rather than working with Lerp smoothing on the camera right away I tried applying it to some sprite motion so it was much easier to see what was going on. The results of this really require a separate post so I’ve explained the process in a small tutorial with some example code here. The net result was an extended Lerp class that moves an object smoothly towards a target no matter how often (and by how much) the target position changes.

Now I had a way of making the camera track smoothly I set about working on the camera target (what the camera is moving towards). This was a lot harder! My thought process here had three key stages…

1. Player Velocity
One of my main concerns about the camera being centred all the time was that, if the player was moving quickly, lookahead was limited to half the screen width. In ‘Defender’ (the main inspiration for Jetboard Joust) the camera seems to be focussed something like 25% of the screen width if front of the player so I started with similar approach. I took a maximum lookahead value, worked out the player’s current velocity relative to the maximum possible velocity, and then placed the camera target the same proportion of the maximum lookahead value in front of the player.

The end result was OK but, as it doesn’t take long for the player to accelerate to top speed, the camera was moving far too quickly to its farthest position. Jetboard Joust plays differently to Defender in that there is more ‘dogfighting’ style gameplay when dealing with enemies (rather than hurtling in one direction as fast as possible) and somehow the camera movement needed to reflect this. So I tried a different approach…

2. Directional Duration
For my second attempt I set up a variable that gradually increased/decreased in value depending on how long the player was travelling in a particular direction. So, from stationary, if the player was travelling left it would take approx five seconds for the value to reach -1.0 (the minimum possible) and the inverse for travelling right. I then multiplied the maximum lookahead value by this variable and set the camera target appropriately. If the player changes direction I set the variable to zero for a quick turnaround.

This worked much better than my first attempt – the camera seemed to track fairly naturally with a long lookahead distance if the player was travelling any distance left and right and not switching erratically in the midst of battle. There were a couple of problems with this approach though, I didn’t like the obvious lag as the camera tried to ‘catch up’ with the player when accelerating from stationary and too often I’d end up forced to do battle at the edges of the screen.

3. Enemy Location
For my third attempt I adopted an entirely different tactic. I totally ignored player movement and instead positioned the camera entirely based on the location of nearby enemies (limiting this at a maximum value to ensure the player remains onscreen). I tried two approaches here:

3a. Enemy Position
Firstly I tried positioning based on how many enemies were to the left or right of the player. So I’d start by dividing 1.0 by the number of enemies within range and then adding or subtracting this number to a variable based on whether each enemy was to the left or right of the player. The result was a number between -1.0 and 1.0 which gave an indication of where the action was relative to the player – this number was multiplied by the max lookahead value to give the camera target position.

This strategy had some interesting results in the way the camera swung towards the action but overall it was too unreliable, I’d often find my attention was on a particular enemy and the camera would suddenly swing away as there were more enemies behind me or something. So I tried something simpler…

3b. Closest Enemy
This time, based on the logic that 99% of the time the closest enemy would be the one the player was concentrating on in a dogfight I simply targeted the camera at the closest enemy to the player (whilst ensuring the player remained onscreen).

The result of this was strangely compelling! Although the camera lurched left and right far too much it made the gameplay seem more intense, dynamic, and somehow just more ‘fun’. I was really surprised about how much the gameplay was enhanced by this simple approach – I just needed to find a way to tone it down a bit.

4. All Of The Above
So what I did next was use all of the above approaches and apply a weighting to each of them, hoping that this would provide some kind of ‘happy medium’. Fortunately, with a bit of tweaking, it did!

I still felt I could improve things further though so I made this weighting dynamic. I fixed the weighting based on player velocity at 25% and based the remaining 75% on either the closest enemy (if any enemies are within range) or the directional duration.

Another tweak I made was to slightly bias the ‘closest enemy’ calculation based on the direction the player is facing so that enemies in front of the player are considered ‘closer’ (I reduce the distance by 75%) than those behind. This makes the camera more likely to stick on enemy the player is currently engaged with.

The result is a camera that doesn’t lag, gives a long lookahead if the player is covering a lot of distance (and no enemies are present), and pans to the centre of the action when a dogfight is in place. I think it works pretty well.

I’m particularly pleased with the way the camera snaps to the closest enemy. I don’t know if it’s still too motion-sickness-inducing (maybe) but it feels much more visceral, seems to help with gameplay, makes it easier to get combos, and, in certain situations, just makes the player feel like a complete badass! So I think I’m going to park the camera now and wait for user feedback before I go tweaking it further…

Dev Time: 2.5 days
Total Dev Time: approx 96 days


Camera Motion Based On Player Velocity

Camera Motion Based On Duration Of Movement

Camera Motion Based On Closest Enemy

Camera Motion Based On All Of The Above #1

Camera Motion Based On All Of The Above #2

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: