19 March 2010

Code of the Ninja: 2D Camera

If you haven't already, read the Code of the Ninja: Introduction

Welcome back, Code Ninjas!

It's a bit anachronistic to use the word "camera" in reference to 2D games. The concept of the viewable area as the view through a director's camera really only took off with Super Mario 64, whose 3D worlds required the player to be actively mindful of the viewpoint. Four entire buttons on the Nintendo 64 joypad were dedicated to camera control (though they often found other uses), and Super Mario 64 even went so far as to characterise the camera as a Lakitu floating on a cloud, following Mario wherever he went.

However, we live in a post-3D world, and it's justifiable to consider the view in a classic 2D sidescroller to be a "camera". In this Code of the Ninja, we'll be looking at how to implement a natural feeling camera in a sidescrolling game.

Game Maker includes built-in camera functionality. Just about anyone who's used it will be familiar with the "views", and the view_object variable. When view_object is set to the id of an instance, the view will follow that object around automatically. You can adjust some border and speed settings, as well.

For a lot of simple games, this works out just fine. But for anything like Sonic or Mario, which require a bit more flexibility in their camera, it's a better idea to write new camera scripts and ignore Game Maker's built-in object following altogether. (You'll still need to define a view, though, of course. Otherwise the entire room will be shown, scaled to fit the window.) So make sure the view_object is set to none, and let's begin.

Camera Follow

We'll make a new script called "CameraFollow()". If you're going to only ever follow one object in your game, such as the player, you could just call this script in the player object. However, oftentimes we'll want to change which object is followed (perhaps keeping an eye on the boss in a boss fight, for instance). That means it's better to write CameraFollow() to take an argument of which instance to follow, and call it in a persistent control object (you can make a dedicated Camera object, or just call it in whichever existing control object you already have, such as the HUD or an Input handler).

Also, CameraFollow() must be called after the target object has already moved. The best way to make sure of this is to call CameraFollow() in the End Step Event.

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

We'll be adding more features to this script as we go, but I've started with this simple version that simply keeps the target object in the centre of the screen. You can try it now, and it should work.

How's does it work, though?

First, we find the horizontal centre point of the view as it currently stands. That's view_xview (the left edge of the view) plus half of view_wview (the width of the view). We store this value in cameraCentreX. Then we do the same thing to find the vertical centre point, and store it in cameraCentreY.

Note: Some games, such as Sonic the Hedgehog, don't use a perfectly centred view. They bias the camera slightly upward, to show more of what's beneath the player. If you wish to do the same thing, you can replace (view_wview/2) and (view_hview/2) with custom values; or, alternatively, you can add bias values on top of the existing calculation, which may be necessary if your view width or height change during the game (for widescreen toggling purposes, etc).

Next, we find how far away from these desired centre points the target object's (argument0's) x and y positions are, by subtracting the centre point values from the target object's x and y. The difference between them - the offset - we store in cameraOffsetX and cameraOffsetY.

Note: We use floor() on the object's x and y at this point because x and y are often at noninteger (subpixel) values, but the view in Game Maker doesn't render at such positions. Instead, it rounds view_xview and view_yview off. Unfortunately, as rounding sometimes results in rounding up and othertimes rounding down, this can cause jitter. All this is avoided by flooring the object's x and y before using them in any calculations.

Finally, we simply add these offset values to the view x and y position, in effect moving the view by the exact same amount the player moved away from the centre point. (It may seem a roundabout way to have done this, but it's being set up for more complicated functionality later on.)

Staying Inside

Before we add new features to our script, though, there is one problem with it we need to patch up. Unlike Game Maker's built-in object following, this code allows the view to exceed the room boundaries. Depending on how you design your game, this might be a bad thing.

The solution? Create a new script called CameraLimit(). It should be called from CameraFollow(), after everything else.

script: CameraLimit()

if view_xview > room_width-view_wview view_xview = room_width-view_wview;
if view_xview < 0 view_xview = 0;

if view_yview > room_height-view_hview view_yview = room_height-view_hview;
if view_yview < 0 view_yview = 0;

Note: In this version of CameraLimit(), I've used the room dimensions. You can use any custom values you want - there's no strict reason why you can't exceed the room dimensions, even using negative numbers. In fact, since Game Maker doesn't let you resize a room while you're in it, the only way to dynamically change the limits is to use your own variables. Why change the limits? Imagine a boss fight in Sonic - the view is extremely limited, to keep the boss on the screen, but of course the actual room (which contains the whole zone) hasn't really changed size.

Free Zone

Now that our camera is properly chastened and stays within its designated confines, we can add a new feature to CameraFollow(). We're going to add a "free zone" - a region in the centre of the screen (of any size you wish) in which the character can move freely before the camera bothers to try and follow.

Why add such a thing? There are probably many reasons, but the major one is that centring the view so strictly on the player can cause it to move around too much when the player is making a small jump, or merely turning around. It's best to have a little buffer area, so that the camera doesn't seem to jerk so drastically.

How do we add this in? First, you need to decide how large this free zone should be. I'm going to use 8 pixels in either direction horizontally, and 32 in either direction vertically. You can use anything you think is reasonable, and it doesn't even have to be symmetrical.

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//free zone
if cameraOffsetX > 8 cameraOffsetX -= 8; else
if cameraOffsetX < -8 cameraOffsetX += 8; else
cameraOffsetX = 0;

if cameraOffsetY > 32 cameraOffsetY -= 32; else
if cameraOffsetY < -32 cameraOffsetY += 32; else
cameraOffsetY = 0;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

CameraLimit();

That takes care of the free zone. All you have to do is subtract the size of the free zone from the camera offset if the camera offset is larger than the free zone, or set the camera offset to 0 if it's smaller than the free zone (so it won't move at all). There are multiple ways to code this; I chose a simple, if long-winded, method.

Speed Limiting

Next, we need to add speed limiting. Sometimes (but not all the time) you want the camera to only move a maximum number of pixels per step. This can be used for a sense of speed, as the camera lags a little behind the player (as sometimes happens in Sonic 2), but it can also be used to scroll the camera from one target object to another when the targets are quickly switched. If there was no limit on the number of pixels the camera could move per step, the view would immediately switch and the player might not understand what happened.

I've chosen 16px as the speed limit here. Let's add the speed limiting (again, there are several ways to code this):

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//free zone
if cameraOffsetX > 8 cameraOffsetX -= 8; else
if cameraOffsetX < -8 cameraOffsetX += 8; else
cameraOffsetX = 0;

if cameraOffsetY > 32 cameraOffsetY -= 32; else
if cameraOffsetY < -32 cameraOffsetY += 32; else
cameraOffsetY = 0;

//speed limit
if cameraOffsetX > 16 cameraOffsetX = 16; else
if cameraOffsetX < -16 cameraOffsetX = -16;

if cameraOffsetY > 16 cameraOffsetY = 16; else
if cameraOffsetY < -16 cameraOffsetY = -16;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

CameraLimit();

Now we've added the speed limit, we can match Game Maker's built-in object following point for point. Now to add some even more powerful stuff.

Looking Around

In Sonic, Mario, and countless other platformers, you can look up and down, shifting the view slightly to see what's above and below you. In Super Mario World, you can use the L and R buttons to shift the view left and right, as well. Let's add these abilities.

In the player control scripts, when looking up and down, or even left and right, you'll need to add to and subtract from variables which CameraFollow() will use to shift the view. I'll call these cameraShiftX and cameraShiftY.

For instance, pressing Up would subtract 2 from cameraShiftY every step, until it reached the maximum shift you desire. Pressing Down would do the opposite, adding 2 until the maximum shift was reached. In the case of neither button, cameraShiftY would slowly return to 0. (For Super Mario World's L and R shifting, the horizontal shift doesn't drift back to normal upon letting up the button, though. It remains shifted until the player shifts it back.)

Some games actually shift the view horizontally depending on the direction the player is facing. I find this annoying, myself - when turning around and making a jump, the whole screen starts moving, making it harder to line up where to land. But this, too, can be done with the same cameraShiftX variable.

Now, to take the shift into account, all we have to do is change the lines in CameraFollow() that determine the offset. Replace them with these:

script: CameraFollow()

...
//determine offset
cameraOffsetX = floor(argument0.x + cameraShiftX) - cameraCentreX;
cameraOffsetY = floor(argument0.y + cameraShiftY) - cameraCentreY;
...

By adding cameraShiftX and cameraShiftY to the target object's x and y when determining the offset, the camera is technically not following where the player is, but where the player is looking. When the player isn't looking around, cameraShiftX and cameraShiftY return to 0, which is the same as following the player itself.

Re-centring Upon Landing

In Sonic the Hedgehog, the camera behaves differently when Sonic is in the air as opposed to running along the ground. In the air, Sonic has a generous vertical "free zone" before pushing the camera around. But on the ground, the camera keeps him at dead vertical centre, so that when he runs over hilly terrain, the camera follows properly. (The camera behaves the same, horizontally, in either state.)

This is simple enough. You can just add a check in the CameraFollow() for whether he's airborne or not, and exit the vertical free zone calculation if he's on the ground.

Note: Though it works well enough to simply check if Sonic is in his air state, it's a better idea to add another flag in the target object, called GroundCamera, which you set to false when he jumps, springs, or falls, etc, and reset to true when he lands. Why a second flag when his state would do? In the case of Knuckles, when he glides and slides into the ground, even though he's technically landed, the camera doesn't return to normal until he stands up. Thus, it's better to have fine control over the mode the camera is in, independent of the actual state of the character.

If that's all we do, though, we'll be left with a problem. When Sonic lands from a jump, the camera jerks immediately to focus tightly on him. That's no good - it's too much of a jerk to put up with comfortably.

There are two ways to fix this. They both involve reducing the vertical speed limit of the camera to 6 instead of 16 after Sonic lands, so that the camera catches up slowly enough that it doesn't cause violent motion.

You can't simply leave the vertical speed limit at 6 all the time. Sonic often runs downhill, and his vertical speed will well exceed 6. The camera would never catch up if it couldn't go faster than 6 pixels per step! So it's necessary to determine whether Sonic has just landed or not.

The first way is to check his speed. If his vertical speed is less than 6, make the speed limit 6. If it's more than 6, make the speed limit 16. Chances are his vertical speed will be very low after landing on the ground. This method is similar to how the 16-bit Sonic engine does it.

The second way is to set a flag called JustLanded to true when Sonic lands (you also have to set it back to false when he jumps). While it's true, the vertical speed limit should be 6, and while it's not, the vertical speed limit should be 16. The second the camera catches up with Sonic, you can reset JustLanded to false. How can we tell when the camera catches up to Sonic? Check if abs(cameraOffsetY) is less than or equal to 6 (i.e., Sonic isn't more than 6 pixels above or below the vertical centre point). In any step where where that's true, the camera will catch up.

script: CameraFollow()

...
//speed limit
if cameraOffsetX > 16 cameraOffsetX = 16; else
if cameraOffsetX < -16 cameraOffsetX = -16;

var cameraLimitY;

if argument0.JustLanded cameraLimitY = 6; else
cameraLimitY = 16;

if abs(cameraOffsetY) <= 6 argument0.JustLanded = false; else
if cameraOffsetY > cameraLimitY cameraOffsetY = cameraLimitY; else
if cameraOffsetY < -cameraLimitY cameraOffsetY = -cameraLimitY;
...

Jump To A Point

Now that that's all working, there's one last thing to add. Because our camera has a speed limit, when the level starts, you'll have to wait for the camera to scroll to where the player is before you can start playing. This kind of sucks.

The remedy is a script called CameraJumpTo(). You can call it to immediately centre the view around any point you specify. Call it as the game begins to focus on the player.

script: CameraJumpTo()

view_xview = argument0 - (view_wview/2);
view_yview = argument1 - (view_hview/2);

CameraLimit();

The script takes two arguments: the x and y to point at.

Example GMK

For an example GMK, click here.

Well, that's it for custom 2D camera. Until next time, happy coding, fellow Code Ninjas!

If you use my code or scripts in your game or engine, no credit is necessary. But I'd love to hear about your project if you do! Just drop me a comment below, or e-mail me at us.mercurysilver@gmail.com