code of the Ninja();

// in search of swift, efficient, and invisible code

2011-03-02

Partially Erasing Surfaces in Game Maker

Hey there, Code Ninjas!

One of the cool things about Game Maker 8 is the ability to export PNGs with an alpha channel for transparency. There's no separate functions for doing so, though; if you want to be sure the exported image is in PNG format, you have to make sure the extension is explicitly ".png", like so:

It's especially cool to create partially transparent surfaces and export them. After creating a surface, you can use the draw_clear_alpha() function to make it completely transparent:

One thing that's annoying, though, is the way that drawing with a partial alpha to a surface works. Instead of blending with the colour underneath, the colour is completely replaced, alpha and all. Effectively this punches "holes" in the surface image.

You'd think this could exploited to create some kind of eraser tool. Draw pixels with an alpha of 0 to the surface to erase pixels that are already there, leaving fully transparent pixels behind.

This doesn't work, though, for some reason. Very low alpha values such as 0 or 0.01 function exactly as you'd normally expect when drawing to the screen, even though higher values such as 0.7 differ when using surfaces.

So much for the ability to erase pixels from a surface using that method... But there is another way.

Set the blend mode to bm_subtract before drawing to the surface and you'll effectively be able to erase from the image:

This trick may come in handy on occasion, especially when making games where the user is allowed to paint custom textures for things - an erase tool is essential.

Until we meet again, happy coding!

2011-03-01

Looking Up and Down

Hello again, Code Ninjas!

In most 2D platformers the player has the ability to shift the camera a short way by holding up or down on the D-pad. In Sonic games, it works great but there is minor flaw that's hard to notice and really doesn't cause any problems, but I thought it would nice to show how to fix it anyway.

The Problem

The problem arises when the player looks up or down near the top or bottom boundaries of the level (or whatever current boundaries the camera is limited to). In order to understand the problem, we need to look at the basic idea behind shifting the camera.

If there were no ability to look up or down, the process would be really simple. The camera would simply follow the player's position directly, with a simple check to make sure it doesn't leave the level boundaries. In order to shift the camera up and down, though, an extra step is needed. An offset is added to the player's Y position before the camera follows it. By increasing or decreasing the offset value when the up or down buttons are pressed, the camera will shift vertically relative to the player's position.

Clearly there need to be maximum and minimum values that the offset can't exceed, otherwise the player could continue to scroll the camera freely until it reached the top or bottom of the level. Normally this behaviour is not desired, so limits are set that will keep the player visible on the screen.

Imagine for a moment, though, that there aren't. Say the player is looking up - the offset decreases and decreases. It will continue to decrease even after the camera stops at the top of the level, because the offset doesn't know when to quit. The camera is limited, sure, and the player never sees beyond the boundary of the level, but the offset value is still decreasing away.

Now what would happen if the player stops pressing up? The offset value will start increasing until it returns to 0 (no shift at all). But depending on how long you've been pressing up, you'll have to wait a few moments before the camera starts to visibly scroll back to the neutral position. After all, the offset has been invisibly counting away for an undetermined amount of time, and that extra time has to be made up for when it tries to return.

This example with no upper/lower limit for the offset illustrates the problem I'm talking about, and that we're going to fix. It's less noticeable when limits are present for the offset, because the value can't continue to increase or decrease indefinitely, but it's still there. If you've got a copy of any Genesis Sonic game, try looking up or down near the top or bottom of a zone and see for yourself. Scandalous, isn't it?

It may not be the worst problem in the world, but it can be fixed, so let's give it a go.

The Solution

There are actually two solutions. One would be to reduce the offset by the appropriate amount when the player lets up from pressing up or down, but that's not the solution I'll describe. Why not? Because that method would require the detection of when up or down is released, which - while certainly possible - is harder to slot right into the way the code already works in the original Sonic.

Let's look at some basic code for handling the offset, and then we'll apply the fix to it.

The problem occurs in case -1 and case 1: stopping at the shiftLimit isn't good enough, because at the top and bottom of the level, we need to stop increasing or decreasing early.

How close to the top or bottom of the level must one be in order for the undesired behaviour to occur? Close enough that the distance between the top/bottom of the camera and the top/bottom of the level is less than the shiftLimit.

This suggests the solution. Instead of using the shiftLimit alone, we should use whichever happens to lesser - the shiftLimit or the difference between the view boundary and the level boundary.

(The reason why shiftOffset has to be subtracted from view_yview is so the difference won't change once the screen starts scrolling and the offset starts to change.)

And that's all that's needed. It may not be much, but persnickety folks might enjoy the game more!

If you enjoyed this and the previous Code of the Ninja, be sure to come back tomorrow for one more before I slip back into the shadows. =P

2011-02-28

Checking Multiple Joypad Buttons

Welcome back, Code Ninjas!

I apologise for not posting in such a long time (I know ninjas are supposed to be silent, but not that silent), but I've been working on a couple of projects that I hope will soon suprise and delight.

Right now, though, I want to talk about an improvement to my earlier Joypad code. I've been interacting a lot recently with disassemblies of Sonic the Hedgehog, and it's a great learning experience. Regardless of what might have happened to the Sonic series over the years, Yuji Naka's programming remains an inspiration to me. Studying his code has taught me plenty of little tricks, not least because the Genesis is very limited by today's standards and it took a lot of skill to squeeze great results out of it.

Anyway, last time I had described a system that updates a variable called JoyCurrent each step with the current state of the joypad, with each bit representing one button. There was also a second variable called JoyPrevious in which JoyCurrent is stored right before JoyCurrent gets updated. And finally, a third variable called JoyPast, only there to smooth out problems with cheap joypads that occasionally glitch up.

There were three scripts: joy() for checking if a button is down; joy_pressed() for checking if a button is down now but not one step ago; and joy_released() for checking if a button is up now but not one step ago (or even two steps ago, in the case of the aforementioned glitchy controllers).

But I've since discovered a serious deficiency with the joy_pressed() and joy_released() scripts. They can't check for more than one button at a time without causing problems. Let me explain.

The Problem

Since JoyCurrent and its related variables contain bits that are either on or off to represent the state of buttons on the joypad, checking the state of a button is as easy as testing any given bit with code like this:

where argument0 is a power of two such as 1 (for testing the first bit), 2 (for testing the second bit), 4 (for testing the third bit), and so on. It's best to define these values as constants so that they can be sensibly named after the buttons, like A, B, LEFT, or START.

Anyone paying enough attention can see that you can test for more than one button simultaneously just by passing a value as argument0 that has more than one bit on. For instance, you could pass a value like 65535 to test if any button was down, or you could use binary OR to test any combination such as LEFT | RIGHT.

Now, the way the script was written it will return true if any bit of argument0 matches up with one in JoyCurrent. If you want to be sure all the bits match, then you'd have to write something like this:

This is all very well and good, but it falls apart when we get to joy_pressed(). This is how it was written:

Now suddenly, because of that boolean and, the value being returned is degraded - it's now only useful as true or false and doesn't give us as much information. Worse, the following happens:

Imagine you want to check whether A, B, or C are pressed, like in the old Sonic games where any of the three buttons makes him jump or Spin Dash. You don't care if one of the buttons is already down when another is pressed - you still want to detect the new press. The way my code was written, this is impossible with only one call to joy_pressed() because if any of the bits is on in JoyPrevious, the new press won't be detected. The only solution would be to make multiple calls something like this:

which is just tacky and consumes more processor time. It would be far better to be able to type:

and have it be done with. (Of course, A | B | C could be a constant called JUMPBUTTON or something, too, to make it even nicer.)

Well, then, how can we change the code so that this is possible? I'm glad you asked that.

The Solution

At the end of the script joy_step() (the one that updates JoyCurrent and JoyPrevious), we need to update two new variables, JoyPressed and JoyReleased (not to be confused with the scripts that have similar names!) These should be global variables, declared in joy_init().

These variables are destined to behave just like JoyCurrent, only for pressed and released. Just like how you can test to see if buttons are down by checking JoyCurrent as joy() does:

you'll be able to check which buttons are newly down or up in one simple comparison by rewriting joy_pressed() like so:

and joy_released() like so:

(At this point these scripts are all so simple you might not even want to make them scripts at all, but merely type anywhere you would have typed joy_pressed(BUTTON), but it's up to you.)

This sounds great, and it will solve all of the problems I mentioned above, but I haven't told you yet how to update JoyPressed and JoyReleased at the end of joy_step(). It requires a little bit of explanation, though, so we can understand the underlying principles. Otherwise, it would get confusing and complex if you ever need to expand upon it.

First, let's look at a visual representation of our variables. I'm assuming only 8 buttons for convenience. Here's a state with no buttons down:

JoyPrevious: - - - - - - - -
JoyCurrent:  - - - - - - - -

Let's press the first button (we'll call it A).

JoyPrevious: - - - - - - - -
JoyCurrent:  - - - - - - - A

Now let's, without advancing a step yet, add a third variable to this visual guide, temp. It's contents will be JoyPrevious binary AND-ed with JoyCurrent (i.e. ).

JoyPrevious: - - - - - - - -
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - -

As far as temp is concerned, nothing has happened! But what happens when we do advance one step, without letting go of A?

JoyPrevious: - - - - - - - A
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - A

JoyPrevious becomes JoyCurrent, JoyCurrent remains the same, and temp finally notices what's going on. Clearly, temp is no good for checking buttons that are newly down, because for one temp has only detected the new press one step late, and for two if we continue to hold A temp will not revert to 0. Merely using binary AND isn't enough. We need to do one more calculation - binary XOR. Let's go back to our previous step:

JoyPrevious: - - - - - - - -
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - -

and add a fourth variable, called JoyPressed. It's contents will be temp binary XOR-ed with JoyCurrent (i.e. JoyPressed = temp ^ JoyCurrent;).

JoyPrevious: - - - - - - - -
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - -
JoyPressed:  - - - - - - - A

By binary XOR-ing temp and JoyCurrent, JoyPressed contains only bits that are different between them. In the next step, the magic happens:

JoyPrevious: - - - - - - - A
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - A
JoyPressed:  - - - - - - - -

Now JoyPressed has reverted to 0, meaning it accurately represents buttons pressed - bits will only trigger for one frame when their corresponding button is pressed. The same thing will happen even if A is released instead of held down:

JoyPrevious: - - - - - - - A
JoyCurrent:  - - - - - - - -
temp:        - - - - - - - -
JoyPressed:  - - - - - - - -

And, if a new button is pressed while another is held down, it will still be detected as a new press:

JoyPrevious: - - - - - - - A
JoyCurrent:  - - - - - - B A
temp:        - - - - - - - A
JoyPressed:  - - - - - - B -

Fantastic! Let's add another variable, JoyReleased, that is temp binary XOR-ed with JoyPrevious instead of JoyCurrent (i.e. JoyReleased = temp ^ JoyPrevious;) and advance one step while releasing B (but not A).

JoyPrevious: - - - - - - B A
JoyCurrent:  - - - - - - - A
temp:        - - - - - - - A
JoyPressed:  - - - - - - - -
JoyReleased: - - - - - - B -

The same principle operates as with JoyPressed. We just solved the problem. Hooray! The actual code at the end of joy_step() would look something like this:

Really the only thing to be done now is make sure that cheap joypads don't cause false press and release events simply because the signal is interrupted for a step once in a while. This is easily done by binary OR-ing JoyPrevious and JoyPast together to create a sort of "buffered" previous state when checking for presses, and binary OR-ing JoyCurrent and JoyPrevious together for a buffered current state (and using JoyPast in place of JoyPrevious where it used to appear in the line) when checking for releases. For example:

Conceivably you could also, instead of doing everything in 2 lines, store more information like so:

This way you could check JoyDown or JoyUp to see whether a button is down but not pressed, or up but not released, which might be useful. Hey, you never know.

That takes care of today's subject. I'll be posting again soon. Until then, happy coding!

2010-09-13

The Nitpicker's Guide to Sonic Genesis - Part II

Hello again, Code Ninjas! It has been quite a while since Part I, but never fear - slowly but surely I will give Sonic Genesis the drubbing it deserves. Welcome to The Nitpicker's Guide to Sonic Genesis - Part II.

Code Flaw #002: The Demos Are Totally Nerfed

Original:

GBA:

The demo is completely different in the GBA version, and obviously much worse (Sonic is hurt twice, seems disoriented, and demonstrates less of the level). Why should this be? If we were uncharitable, we might chalk it up to the GBA team recording new demos which betray their underdeveloped skill level. However it is much more likely that the game is using the same demo data, but changes in the physics have thrown it off.

You see, the demo "movies" in the old Sonic games were not actual videos of the action - that would have taken up so much space it would have been prohibitive. Instead, the game itself is running, but with two changes: 1) the game resets when the player hits the start button or when 30 seconds have elapsed, whichever comes first; 2) the Sonic object isn't receiving input directly from the joypad, but from a chunk of data read sequentially from the ROM. This chunk of data was made by logging the button presses while someone played the level (for more information about recording joypad input "movies", see this post).

So, if the physics or level layout changes, the recording won't be appropriate anymore. It's as if your joypad were simultaneously plugged into two Sega Genesises (Geneses?), one of which contained a Sonic 1 cart, and the other, Sonic 2. You might be playing Sonic 1 beautifully, but anyone watching the Sonic 2 game will wonder why Sonic is suddenly acting like a drunkard. (They won't notice a difference in Tails' behaviour, though - wantonly flinging himself into harm's way is business as usual for him. =P )

The team who made Sonic Genesis should have recorded new demos after the physics were in place to avoid this issue. But, one can hardly blame them for not doing so; it would, after all, have involved playing the game, and I wouldn't wish that on anybody!

Fixing The Problem In Your Own Hack

This issue with screwy demos isn't confined to Sonic Genesis. ROM hacks will have the same problem if the physics, controls, or layout are changed, even slightly. I encountered it myself when making Sonic: The One Ring, and needed to find a way to record new demos that would be compatible with the ROM. I've turned what I learnt into a tutorial and utility - go here for the complete story. See, I'm not just complaining about Sonic Genesis just to be mean - I'm using it to frame programming tips to help you guys out! Aren't I nice?

Well, not that nice since I'm now going to throw in a few freebie Bonus Flaws out of spite!

Bonus Flaw #003: Wrong Credits Footage

In addition to having broken demos, the "demos" seen during the game's credits are broken, too. But none so badly as the Labyrinth Zone one: this time, not only does the control movie not sync up because of different physics, it's totally inapposite because the wrong area of the level has been loaded!

The famous underwater section where Sonic is pulled through the tunnel by the current, catching on to the breakable poles and avoiding spikes, has been inexplicably replaced by some other region of the zone.

Bonus Flaw #004: Marble Zone Button Keeps Turning Up Like A Bad Penny

This is pretty hilarious. It's supposed to load different graphics depending on the zone ID, but they can't even get that right.

Bonus Flaw #005: Underwater Palettes Incorrect

Sonic and the Badniks don't look right...

Original:

GBA:

...but objects like doors and blocks fare even worse.

(And why does Scrap Brain Act 3 have such a horrible dark blue background? It's supposed to be a lovely, rich purple.)

Bonus Flaw #006: Missing/Incorrect Background Tiles

This flaw really does take the cake. This isn't a matter of not being able to properly port a complicated game's physics to a new platform, or something relatively forgivable. It's a simple matter of gratuitous incompetence and unconcern for the product.

Well, try to keep your lunch down, Code Ninjas - I know it's not easy after that rogue's gallery. Until next time!

2010-03-19

2D Camera

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!

2010-02-22

Text Boxes

Welcome back, Code Ninjas!

Last time, I talked about sinusoidal motion, a way to make certain movements and animations look more natural. I mentioned that it could be used to make an opening animation for text boxes. This time we'll be looking at text boxes themselves.

We've all seen text boxes. They're the windows full of dialogue that appear when you talk to people in video games.

It's easy enough to slap a single box of text onto the screen in Game Maker. But most text boxes consist of several pages of dialogue, and the player advances through them by pressing a button.

You could achieve this with an array of strings, like this:

Create Event (TextBox object):

//define the pages of text
//(# is the newline character in GML)
Page[0] = "Hello there, traveller!";
Page[1] = "This is a bomb shop! I stock all#sorts of different explosives.";
Page[2] = "No smoking, please!";
//set the page index
PageIndex = 0;

Draw Event (TextBox object):

//draw the string on the screen
draw_string(TextX,TextY,Page[PageIndex]);

Step Event (TextBox object):

//check for a press of the A button
if JoyButtonPressed(A);
{
  //increase the page index
  PageIndex += 1;
}

This is an okay method, but it has an annoying problem: You have to manually cut the dialogue into pages yourself, as well as place the newline characters.

Imagine you have a game where you want the text box to be resizable. Or, not all text boxes are the same size (as in Final Fantasy VII). Or, you've already written all the dialogue for your RPG, and then decide to change the size of the font or the text box. The method above would suck in these cases - you'd be stuck reworking all your strings every time something changed.

There really needs to be a way to just write the dialogue all in one piece, and let the game take care of the rest: deciding when to break lines, and cutting it into individual pages.

Well, let's see what we can do...

Making Pages

This time we'll give the TextBox object only a single string of dialogue. This will be the source text that the pages are made out of. Also, in order to make the pages contain the right amount of text, the TextBox object will need to know about its size.

Create Event (TextBox object):

//define the source text
//(which can be from the calling object, or loaded from a text file, whatever)
DialogString = "...";
//set dialog offset to 1. This is the position in the source text to start reading from.
DialogOffset = 1;
//set dialog length to the length of the source text. This is the position in the source text to stop at.
DialogLength = string_length(DialogString);

//set position of text box
x = 40; y = 300;
//set size of text box
width = 560; height = 100;
//set size of border (horizontal and vertical)
xborder = 8; yborder = 4;
//determine the size of the text area (text box minus the borders)
textwidth = width-xborder*2;
textheight = height-yborder*2;
//set the height of individual lines
linespacing = 23;

//make the first page of text to show
MakePage();

Step Event (TextBox object):

//check for a press of the A button
if JoyButtonPressed(A);
{
  //make the next page of text to show
  MakePage();
}

We call the MakePage() script every time the player presses the button, to construct the page of text that they'll see next. We also call it once in the create event, so that there's an initial page showing.

MakePage() basically bites off a chunk of the DialogString source text and puts it into a new string, CurrentPageString, which is the string that will be drawn.

script: MakePage()

//set up some temp variables
var numLines,line,letter,word;
line = 0; word = "";
//set the font to the current font so that the font measuring scripts work right
draw_set_font(TextBoxFont);
//empty the CurrentPageString, so we can refill it with text from DialogString
CurrentPageString = "";

//get the number of lines that fit in the box, based on line spacing and height of box
numLines = textheight div linespacing;
//show error message if no lines fit in box
if numLines = 0
{
  show_error("No lines fit in the text box!",1);
}

//main loop
do
{
  //read a letter from the source text
  letter = string_char_at(DialogString,DialogOffset);
  //increase the offset by one since you read one letter
  DialogOffset += 1;
  //is the letter the escape char?
  if letter=="^"
  {
    //change letter to return
    letter = "#";
    //increase the line count to full
    line = numLines;
  }
  //add the letter to word
  word += letter;
  //if the letter was a space, hyphen, or return (or the end of the source text was reached), the word is complete
  if letter==" "||letter=="#"||letter=="-"||DialogOffset>DialogLength
  {
    //check to see if word alone exceeds the textbox width
    if string_width(word)>textwidth
    {
      show_error("Single word is too long for the textbox!",1);
    }
    //check to see if word added to current pages's text is too wide
    if string_width(CurrentPageString+word)>textwidth
    {
      //add a return to go to the next line, and increase the line count
      CurrentPageString += "#";
      line += 1;
      //if this was the last line...
      if line = numLines
      {
        //return the offset to the beginning of the word in order for the next page to start at the right point
        DialogOffset -= string_length(word);
        //blank out the word so it won't be added.
        word = "";
      }
    }
    //only add the word if it hasn't been blanked out
    if word != ""
    {
      //add the word to the current page's text
      CurrentPageString += word;
      //if letter was a return, increase the line count
      if letter="#" line += 1;
      //and reset word to blank
      word = "";
    }
  }
}
until (line >= numLines or DialogOffset > DialogLength)
//stop the loop when reach the last line or the end of the source text

With the comments, MakePage() should be pretty much self-explanatory, but there are two points I want to go into more detail on.

The first is the "escape character", ^. What is it for? Well, it's sort of like a page break. Sometimes you want the sentence of dialogue to end, and not start the next sentence until the player advances to the next page, even if there's enough space to fit the next few words. It all depends on the flow of the dialogue.

I used the caret because it's sufficiently obscure, but of course the escape character can be anything you want to define it as. If your RPG townsfolk are going to use emoticons like ^_^ then you might want to pick something else.

The second point is this: Why is the MakePage() script so complicated? Anyone familiar with GML will know that you can use a function called draw_text_ext(), which will automatically word wrap to any width that you specify. Why do I go through so much trouble to manually run through the string and add newline characters to cause it to wrap?

It becomes clear as we move on to the next aspect of text boxes. They have to type out.

Typing Out

In order to make them type out, we shouldn't draw CurrentPageString in the draw event. Instead, we should make a new string, ResultString, and draw it. ResultString will be built up from CurrentPageString in the step event of the TextBox object.

Draw Event (TextBox object):

draw_set_font(TextBoxFont);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_text_ext(x+xborder,y+yborder,ResultString,linespacing,-1);

Step Event (TextBox object):

//if the text box is typing out the text
if printing
{
  //increase CharIndex
  CharIndex += 1;
  //if CharIndex is the size of the page of text
  if CharIndex >= CurrentPageLength
  {
    //fill the ResultString with the entire current page and stop typing out
    CharIndex = CurrentPageLength;
    ResultString = CurrentPageString;
    printing = false;
  }
  else
  {
    //otherwise, make the ResultString as much of the current page as CharIndex is large
    ResultString = string_copy(CurrentPageString,1,CharIndex);
  }
}

We need the new variables, 'printing' so that we know when it's typing out and when it's done, 'CharIndex' to increase each step so we can keep taking more and more of CurrentPageString, and 'CurrentPageLength' so that we know when we've finished going through CurrentPageString. These three will need to be set up at the end of MakePage() now.

script: MakePage()

...

CurrentPageLength = string_length(CurrentPageString);
CharIndex = 0;
printing = true;

Now it'll print out. It's because of this that MakePage() needs to be so complex. If we relied on draw_text_ext() for word wrap, we'd get ugly results. Because we're actually drawing ResultString to the screen, and ResultString builds up letter by letter, the computer wouldn't know if a word was going to run off the side of the text box until after it had printed fully out. This would result in seeing words print out of bounds, and then skip on to the next line. MakePage() comes to the rescue here, determining where the lines should break before ever being printed, so that the words "know" to be on the next line before they even finish printing out.

Well, now that we've got our dialogue typing out, you'll notice a new problem. When the user presses the button, it'll skip to the next page. We don't want to do that, if the current page hasn't finished printing out. Instead, we want to instantly finish typing out the current page. Only if the user presses the button again should it advance one page.

This will require modifying the step event.

Step Event (TextBox object):

//if the text box is typing out the text
if printing
{
  //increase CharIndex
  CharIndex += 1;
  //if CharIndex is the size of the page of text OR the user presses the button
  if CharIndex >= CurrentPageLength or JoyButtonPressed(A)
  {
    //fill the ResultString with the entire current page and stop typing out
    CharIndex = CurrentPageLength;
    ResultString = CurrentPageString;
    printing = false;
  }
  else
  {
    //otherwise, make the ResultString as much of the current page as CharIndex is large
    ResultString = string_copy(CurrentPageString,1,CharIndex);
  }
}
else
{
  //if it's not typing out, pressing the button should advance one page
  if JoyButtonPressed(A)
  {
    //but if we're on the last page, we should close the text box
    if DialogOffset >= DialogLength
    {
      instance_destroy();
      exit;
    }
    //otherwise, determine the next page of text to type out
    MakePage();
  }
}

What we've done is check for a press of the button while 'printing' is true, and made it do the same thing as reaching the end of the page: ResultString becomes CurrentPageString in total, and 'printing' is set to false. Also, we've made the standard check for the button only happen when 'printing' is not true.

I've also added a check at that point if it's the last page or not. If the player presses the button on the last page, there's no new page to advance to, so the text box should close instead of calling MakePage() again.

Now that it's all working, we should add some visual cue so that the player knows that the button does something different at different times. While the text is typing out, the button skips to the end. While it's not, the button advances one page. On the last page, the button closes the text box.

Most games don't bother with a different icon for each possible state. They just show a triangle or something once the text is done typing out, so that you know there's more. If it's the last page, the triangle simply doesn't appear when the text finishes appearing.

It's easy enough to check for all three states, though, so this is how you can do it - add this to the draw event:

Draw Event (TextBox object):

...

if printing
{
  //draw "skip" icon/message
}
else
{
  if DialogOffset >= DialogLength
  //draw "close" icon/message
  else
  //draw "next" icon/message
}

Variable Text Speed

That's pretty much it for text boxes. But there's some nice finishing touches we can add - variable text speed, for one. In the code blocks above, the text types out at 1 character per step. This speed should be under the player's control, because everybody reads at a different rate.

All that needs to be done is replace the line that says

Code:

CharIndex += 1;

and replace it with

Code:

CharIndex += textspeed;

The text speed can be set to 1 at the game start, and then the player can change it from an option menu. Or - and this is pretty cool - since the left and right buttons usually do nothing while a text box is open, you could let the player alter the text speed any time a box is open.

Just add this to the step event:

Step Event (TextBox object):

if JoyButtonPressed(LEFT)
{
  //decrease the text speed
  textspeed /= 2;
  if textspeed < 0.25 textspeed = 0.25;
}
else
if JoyButtonPressed(RIGHT)
{
  //increase the text speed
  textspeed *= 2;
  if textspeed > 8 textspeed = 8;
}

You can make the upper and lower limits anything that seems reasonable to you. However, the difference between settings isn't enough when they're linear. They need to be logarithmic. So, instead of adding to and subtracting from textspeed, I suggest you multiply and divide it (or bit shift it).

There should be lights or pips or something worked into the design of the text box, so that the player has a visual clue to the setting that the text speed is at.

Canceling

Finally, you can add this to the step event.

Step Event (TextBox object):

if JoyButtonPressed(B)
{
  //close the text box
  instance_destroy();
  exit;
}

This lets the player hit a cancel button to close the text box whether the dialogue is finished or not. It's really annoying to accidentally re-talk to a character and be forced to page through their entire diatribe when you've already read it. You might want to add a check so that the player can only leave like this if they've talked to the person before, but I think the player ought to be able to cancel even if it's new dialogue. There might still be certain important story driven dialogue that they can't cancel, though.

Example GMK

For an example GMK, click here.

Until next time, happy coding, Code Ninjas!

2009-12-08

Sinusoidal Motion

Welcome, Code Ninjas!

This time we'll be looking at a simple script that makes certain motions look more natural.

Imagine you have a platform that you'd like to move back and forth, a common element in platformer games. You might use code something like this:

Create Event:

r = 64;//maximum distance in pixels the platform may travel from its origin before reversing direction

s = 1/32;//speed of the platforms. The divisor is how many steps you wish the platform to take to reach its maximum distance from its origin.

Step Event:

if a //moving forward...
  {
  p += s;
  if p >= 1 { p = 1; a = 0; }
  }
else //...and moving back
  {
  p -= s;
  if p <= 0 { p = 0; a = 1; }
  }

x = xstart + r*p;//update platform's position
xspeed = x-xprevious;//get platform's speed in pixels

(Note: If this code seems a little overcomplicated, it is because it had been purposefully written to be able to make platforms of any speed and range.)

This effect that this code achieves is a platform that moves away from its starting point at the specified speed, reaches its maximum distance, and then immediately reverses direction and trots back to repeat the process indefinitely. This is the sort of platform you'll often see in early 8- and 16-bit games.

The trouble with this kind of motion is the immediate reversal of motion. In one step, the platform can be moving with a speed of +5 pixels, and the in the next, with a speed of -5 pixels. This doesn't look very realistic, because in reality, most often when something reverses direction, it has to slow down to a halt, and then begin to accelerate again.

This jerky motion isn't so bad if the platform's "patrol area" is bounded by walls at its extremes - then it just looks like the platform is bouncing off of the walls, and the motion doesn't look too bad. But if the platform is floating in mid-air, as they often are, there is nothing that appears to plausibly reverse it, and the motion looks unnatural.

And it's worse than just looking unpleasant. It actually makes the game less fair, and less fun. If the player can't tell by some visual cue when the platform is going to decide to turn around, they have a much harder time getting the proper timing on their jump. They will have to use more trial and error, watching the platform make its rounds more than once before they can confidently make their move.

This is almost game-breaking if you want to keep good flow, as in a Sonic game. In the Sonic the Hedgehog games for the Mega Drive (Genesis), almost all platforms not bounded by walls move with a natural motion - decelerating as they reach their extremes, and accelerating toward their point of origin as they turn back around. This allows players to intuit exactly where the platform will be any time when they first come across it, without patient study of its entire cycle. This is one of the subtler points about the Sonic the Hedgehog game, seldom recognised, but it contributes not insignificantly to the sense of speed that made them popular.

Well then, how can we achieve the same effect so that our platforms move with a natural motion, rather than an outdated, unrealistic, and unfair jerky one? Why, with the trusty cosine function, of course!

Imagine, now, a platform that - instead of moving simply up and down, or left and right - moves in a complete circle, as many do in Sonic and Mario games. The platform's speed should be uniform, but if we were to look at just one component of its velocity - say, just the xspeed, or just the yspeed - we would notice acceleration and retardation of its speed. Simply imagine looking at the platform's circular path edge on, instead of face on. It would appear to be moving in a straight line, but slowing down at the edges and speeding up in the middle, just like those Sonic platforms we want to emulate.

So, in effect, what we want to do is make the platform move in a circle - just a really flat circle that might as well be a line. We'd use code something like this:

Create Event:

r = 64;//maximum distance in pixels the platform may travel from its origin before reversing direction (radius of the circle)

s = 180/32;//speed of the platforms. The divisor is how many steps you wish the platform to take to reach its maximum distance from its origin. (This time we use 180, not 1, because we'll be using degrees.)

Step Event:

a += s;
if a >= 360 a -= 360;
//alternatively the preceding two lines could be 'a = (a+s) mod 360;'.

//also, here we don't need to use two states, forward and back, because the circular motion takes care of that for us automatically.

x = xstart + r*cos(degtorad(a));//update platform's position
xspeed = x-xprevious;//get platform's speed in pixels

To make the platform move vertically, we can just replace all the references to x and make them y. Or, to make it actually move in a perceptible circle, we can have both sets of lines. By using a different range value for both x and y, you can squash the circle into any sort of ellipse you want - for example, a circle that is twice as wide as it is tall:

Create Event:

xr = 64;
yr = 32;

Step Event:

//...

x = xstart + xr*cos(degtorad(a)); xspeed = x-xprevious;
y = ystart - yr*sin(degtorad(a)); yspeed = y-yprevious;

Remember to subtract the sine for y, and add the cosine for x, otherwise instead of moving circularly, it'll just move in a smooth diagonal - which is actually another useful effect you might want to achieve.

Well, that about does it for platforms, but the power of sinusoidal motion goes far beyond. There are other applications, and for just such an example, I'll use the purpose for which I first I needed it myself.

In games like Phantasy Star, when you talk to townsfolk or shopkeepers, their dialogue appears on the screen inside of bordered window, or a 'text box'. In most games, the text boxes appear on the screen gradually, either opening up, dropping down, or fading in.

I wanted this animation to appear smoother, so I thought perhaps I could apply sinusoidal motion as the solution. But there was a slight hitch.

Imagine you want to fade in a window, from an alpha of 0 (invisible) to an alpha of 1 (fully opaque). You could simply add 0.1 for ten steps, but that wouldn't look very smooth. How about, instead, we use a sine function.

Code:

step = 18;

for {a=0;a<=180;a+=step}
  {
  alpha = sin(degtorad(a));
  }

Well, clearly this won't work. The value of alpha will go from 0 (the sine of angle 0), accelerate toward 1 (the sine of angle 90) and decelerate back to 0 again (the sine of angle 180). The text box wouldn't fade in, it would fade in and back out again just as quickly! This obviously isn't what we want.

But why not just use 90, instead of 180, so that alpha will stop at 1, thereby fading the text box in how we want it? Well, in that case, the fade would start smoothly, but stop abruptly. I wanted it to both start and stop smoothly.

I needed some way to have the alpha value "move" like a half-circle (slow start and stop), but only "traverse" a quarter-circle (start at 0 and end at 1).

So I made a script, called it 'sinusoidal()', and this is the function I used:

sinusoidal()

//argument0: any value between 0 and 1

return (cos(degtorad(180-(180*argument0)))+1)/2;

Now, sinusoidal motion can be employed anywhere by calling the script. The text box fade code ends up looking something like this:

Code:

step = 0.1;

for {a=0;a<=1;a+=step}
  {
  alpha = sinusoidal(a);
  }

This little script can be very versatile. You can use it to slide logos or menus onto the screen. You could use for flashing lights for smoother look. You could use it to animate a pendulum. You could even use it to make your character push a block (as Link does in the Zelda games) with a less abrupt and better looking motion. And with clever modification, who knows to what ends a Code Ninja might put it to.

For an example GMK illustrating the difference between normal and sinusoidal motion in several types of movement and animation (flashing, shrinking, swinging, sliding), click here.

Next time we'll be looking deeper into text boxes - how to make the text type out, change the text speed, and more. Until then, happy coding, Code Ninjas, and happy holidays, too!