Friday, September 16, 2011

Zoom Camera

In Bullet Time Ninja, when the player enters Bullet Time, the camera dramatically zooms in.



Today, I will walk through how I implemented this camera in Flixel 2.5.


The Problem with Zooming
Changing a camera's zoom in Flixel is actually very easy. Simply set FlxG.camera.zoom to whatever you wish.

FlxG.camera.zoom = 2;



However, the problem is that FlxG.camera.zoom simply zooms on the center of the screen.

I wanted the camera to center on its target when zoomed. This is a careful matter of offsetting the (x,y) values of the camera based on where its target is in the game world. One might think this is a simple matter of following the target, like so:

// this is in a class that extends FlxCamera
public override function update():void
{
     super.update();
     if(target)
          alignCamera();
}

private function alignCamera():void
{
     x = -(target.x - scroll.x) + width/2;
     y = -(target.y - scroll.y) + height/2;
}


Excellent right? Well, no. This causes a bunch of problems.


First, the camera always centers on the target, completely ignoring level bounds.




Second, the zoomed camera still does not center on the character. This is because the center point of zoom for the FlxCamera is actually the center of its screen width and screen height (the middle of the level in this case). The ninja looks pretty good if we put her in the center of the level:






What we need here is a fundamentally different approach that respects camera bounds and always follows the character. We know that the camera will always zoom in towards its screen center, so let's nudge the camera's (x,y) based on how far away the character is from the center of the screen.


private function alignCamera():void
{
     // target position in screen space
     var targetScreenX:Number = target.x - scroll.x;
     var targetScreenY:Number = target.y - scroll.y;

     // center on the target, until the camera bumps up to its bounds
     // then gradually favor the edge of the screen based on zoomMargin
     var ratioMinX:Number = (targetScreenX / (width/2) ) - 1 - zoomMargin;
     var ratioMinY:Number = (targetScreenY / (height/2)) - 1 - zoomMargin;
     var ratioMaxX:Number = ((-width + targetScreenX) / (width/2) ) + 1 + zoomMargin;
     var ratioMaxY:Number = ((-height + targetScreenY) / (height/2)) + 1 + zoomMargin;

     // offsets are numbers between [-1, 1]
     var offsetX:Number = clamp(ratioMinX, -1, 0) + clamp(ratioMaxX, 0, 1);
     var offsetY:Number = clamp(ratioMinY, -1, 0) + clamp(ratioMaxY, 0, 1);

     // offset the screen in any direction, based on zoom level
     // Example: a zoom of 2 offsets it half the screen at most
     x = -(width  / 2) * (offsetX) * (_zoom - 1);
     y = -(height / 2) * (offsetY) * (_zoom - 1);
}


Whoa! That's an intense amount of code! What does it mean???

Effectively, I have 4 variables that "watch" when the target approaches the edge of each side of the screen, which I call ratio variables. If the target is in the dead center on the screen, which is common on big levels, then the ratio variables amount to 0 after the clamp() function is applied, and so the camera (x,y) is set to zero. As the character moves towards the edge of the screen, these ratio variables change linearly (i.e smoothly), and shift the camera into the right place.


In Bullet Time Ninja, zoomMargin is 0.25, which looks like this:



When zoomMargin is 0, it looks like this:


So having that margin is very useful to "look ahead" when a target is close to the edge of a level.

Animation!
The last little thing we need to do is "animate" the camera zoom. This is actually pretty easy.

public override function update():void
{
     super.update()
     // update camera zoom
     zoom += (targetZoom - zoom) / 2 * (FlxG.elapsed) * zoomSpeed;
     
     //....
}

Set the variable targetZoom to any number you want, and the camera will happily zoom there as quickly as zoomSpeed will take it.

Bringing It All Together
To integrate the camera into your game, you will roughly want to take the following steps:
  1. Have a flixel 2.5+ game project
  2. Download the file: ZoomCamera.as
  3. Throw the file into your game project
  4. Change the package name in ZoomCamera.as to fit your needs
  5. In your FlxState's create() function, add something like:
var zoomCam:ZoomCamera = new ZoomCamera();
FlxG.resetCameras( zoomCam );
zoomCam.targetZoom = 2;

And you're good to go. Create something awesome! 

-Greg



5 comments:

  1. Good work! You handled these issues in a very similar way to what I did in Kitty, Catch Mouse! (still in development) You beat me to the punch. I was planning on blogging about the camera system too. Well, I still can really. They do work a little differently and mine has some other extras that I think people will be interested in. At any rate, thanks for posting this! It was a very interesting read and I'm absolutely sure that a lot of people will find it useful.

    ReplyDelete
  2. This has saved me from extreme annoyance on two different occasions so far! Thanks so much! I did notice when I used it that it zoomed in on the corner of the sprite rather than the center, so I made a small edit. https://gist.github.com/emmett9001/5941327#file-zoomcamera-as-L76 . Again - thanks for this.

    ReplyDelete
  3. I must set zoomMargin to -10 (class comment say "0 = no effect, 1 = huge effect") and it's started work perfectly :)

    Thanks so much :)

    ReplyDelete