@mikeevmm tweeted me early this month and noted that there are some problems with FlxCamera.
Here are some known issues I could identify:
The problems above are apparent (as of writing) even in the FlxCamera demo.
There’s an article by Bullet Time Ninja that discusses and resolves the camera issue, but it’s from 2011 and uses Flixel (not HaxeFlixel), so the result may differ.
Today’s tutorial will attempt to resolve the first problem stated above. If there is a solution for the second problem, I’ll write a tutorial to address that next time. 🙂
Setup
As usual, let’s setup a new HaxeFlixel template project for testing:
1 |
flixel tpl -n "MyCameraTest3" |
Code
First, let’s set an arbitrary resolution of the game in the Main.hx file:
1 2 |
var gameWidth:Int = 160; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom). var gameHeight:Int = 120; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom). |
Now, let’s go into MenuState.hx (the default entry point for the game) and create a large-enough level with a movable character:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
package; import flixel.FlxState; import flixel.FlxG; import flixel.FlxSprite; import flixel.tile.FlxTilemap; class MenuState extends FlxState { var player:FlxSprite; override public function create():Void { super.create(); // Set the camera background as white for visibility sake FlxG.camera.bgColor = 0xFFFFFFFF; // Create level var levelData:Array<Int> = [ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, ]; var level = new FlxTilemap(); // There are 40 tiles for the map's width, hence the value "40" below level.loadMap(flixel.util.FlxStringUtil.arrayToCSV(levelData, 40), GraphicAuto, 8, 8, FlxTilemap.AUTO); add(level); // Create player player = new FlxSprite(20, 10); player.makeGraphic(5, 10, 0xFFFF0000); // create red square as player add(player); // Make camera follow the player FlxG.camera.follow(player, flixel.FlxCamera.STYLE_TOPDOWN); // Set camera bounds so the camera doesn't show off-screen area FlxG.camera.setBounds(0, 0, level.width, level.height); } override public function update():Void { super.update(); // Player movement var moveSpeed:Int = 3; if (FlxG.keys.pressed.UP) player.y -= moveSpeed; if (FlxG.keys.pressed.DOWN) player.y += moveSpeed; if (FlxG.keys.pressed.LEFT) player.x -= moveSpeed; if (FlxG.keys.pressed.RIGHT) player.x += moveSpeed; } override public function destroy():Void { super.destroy(); } } |
Now if you run the game with lime test neko , you’ll get a player that can move around a placeholder map, and the camera follows it, within the map’s bounds (btw, yes, there is no collision logic):
Now let’s add some debug controls to recreate the problems mentioned at the beginning of the article above:
1 2 3 4 5 6 7 8 9 10 11 |
override public function update():Void { super.update(); // Player movement ... // test zoom camera if (FlxG.keys.justPressed.ONE) FlxG.camera.zoom -= 0.25; // zoom in if (FlxG.keys.justPressed.TWO) FlxG.camera.zoom += 0.25; // zoom out } |
Now if you test the game — Zooming in and out will only zoom the camera’s viewport. The camera still “follows the player”, but not in a way you would expect:
When we use camera.zoom , it seems we’re actually resizing the camera’s viewport. So how can we fix this? With my limited knowledge, I’d assume the solution is to reposition and resize the camera every time its zoom is changed.
Before we do that, let’s add more debug input into the code so we can later understand what needs to be used to fix the problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
override public function update():Void { super.update(); // Player movement ... // test zoom camera ... // test camera position if (FlxG.keys.justPressed.W) FlxG.camera.y -= 5; if (FlxG.keys.justPressed.S) FlxG.camera.y += 5; if (FlxG.keys.justPressed.A) FlxG.camera.x -= 5; if (FlxG.keys.justPressed.D) FlxG.camera.x += 5; // test camera size if (FlxG.keys.justPressed.THREE) { FlxG.camera.width += 10; FlxG.camera.height += 10; } if (FlxG.keys.justPressed.FOUR) { FlxG.camera.width -= 10; FlxG.camera.height -= 10; } // When the camera is resized, update out-of-screen tile buffer if (FlxG.keys.justPressed.P) level.updateBuffers(); // Re-follow the player if (FlxG.keys.justPressed.O) FlxG.camera.follow(player, flixel.FlxCamera.STYLE_TOPDOWN); } |
Now if you play around with the debug keys above, you’ll notice the following points:
- Changing the camera’s x or y value will move the viewport’s origin (top-left corner).
- Changing the camera’s width or height value will increase/decrease the viewport’s size.
- Changing the viewport size doesn’t seem to update the tilemap’s size, until we use level.updateBuffers() (by pressing P).
- The camera’s deadzone (when following the player) isn’t updated, until we re-follow the player (by pressing O).
Resolving Camera Zoom issue
After some testing with the debug keys above, it seems we can combine the techniques and resolve the zoom issue, by writing the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
package; import flixel.FlxState; import flixel.FlxG; import flixel.FlxSprite; import flixel.tile.FlxTilemap; class MenuState extends FlxState { var level:FlxTilemap; var player:FlxSprite; var oriCameraZoom:Float; var oriCameraWidth:Int; var oriCameraHeight:Int; override public function create():Void { super.create(); // Set the camera background as white for visibility sake ... // Create level ... // There are 40 tiles for the map's width, hence the value "40" below ... // Create player ... // Make camera follow the player ... // Set camera bounds so the camera doesn't show off-screen area ... // Get starting camera zoom as reference oriCameraZoom = FlxG.camera.zoom; oriCameraWidth = FlxG.camera.width; oriCameraHeight = FlxG.camera.height; } override public function update():Void { super.update(); // Player movement ... // test zoom camera if (FlxG.keys.justPressed.ONE) ZoomCamera(-0.25); // zoom in if (FlxG.keys.justPressed.TWO) ZoomCamera(0.25); // zoom out // test camera position ... // test camera size ... // When the camera is resized, update out-of-screen tile buffer ... // Re-follow the player ... } function ZoomCamera(val:Float):Void { // set new camera zoom FlxG.camera.zoom += val; // Resize the camera based on the original value var newWidth:Float = oriCameraZoom / FlxG.camera.zoom * oriCameraWidth; var newHeight:Float = oriCameraZoom / FlxG.camera.zoom * oriCameraHeight; var newX:Float = 0; var newY:Float = 0; // NOTE: // If we use camera.setBounds() above, we need to cap the camera's // size to the level's size, otherwise the offset will be wrong. // // If we didn't use camera.setBounds() above, we don't have to // cap the camera's size below. if (newWidth > level.width) { newWidth = level.width; newX = (FlxG.stage.stageWidth/2) - (newWidth*FlxG.camera.zoom/2); } if (newHeight > level.height) { newHeight = level.height; newY = (FlxG.stage.stageHeight/2) - (FlxG.camera.height*FlxG.camera.zoom/2); } // Set final size FlxG.camera.setSize(Std.int(newWidth), Std.int(newHeight)); FlxG.camera.setPosition(Std.int(newX), Std.int(newY)); // Update tilemap out-of-screen buffer level.updateBuffers(); // Update player deadzones FlxG.camera.follow(player, flixel.FlxCamera.STYLE_TOPDOWN); } ... } |
Now when you zoom in or out, the camera is centered and the player’s deadzone is updated:
Notice that the player will get cropped when moving out of the map’s bounds. That’s because we used FlxG.camera.setBounds() . If you modify the following lines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
override public function create():Void { super.create(); // Set the camera background as white for visibility sake ... // Create level ... // There are 40 tiles for the map's width, hence the value "40" below ... // Create player ... // Make camera follow the player ... // Set camera bounds so the camera doesn't show off-screen area // FlxG.camera.setBounds(0, 0, level.width, level.height); // Get starting camera zoom as reference ... } ... function ZoomCamera(val:Float):Void { // set new camera zoom ... // Resize the camera based on the original value ... // NOTE: // If we use camera.setBounds() above, we need to cap the camera's // size to the level's size, otherwise the offset will be wrong. // // If we didn't use camera.setBounds() above, we don't have to // cap the camera's size below. // if (newWidth > level.width) // { // newWidth = level.width; // newX = (FlxG.stage.stageWidth/2) - (newWidth*FlxG.camera.zoom/2); // } // if (newHeight > level.height) // { // newHeight = level.height; // newY = (FlxG.stage.stageHeight/2) - (FlxG.camera.height*FlxG.camera.zoom/2); // } // Set final size ... // Update tilemap out-of-screen buffer ... // Update player deadzones ... } |
The player can now freely move around in the camera without being cropped, at the expense of not having any map boundaries:
To top things off, let’s use Bullet Time Ninja’s technique in lerping the camera’s zoom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
package; import flixel.FlxState; import flixel.FlxG; import flixel.FlxSprite; import flixel.tile.FlxTilemap; class MenuState extends FlxState { var level:FlxTilemap; var player:FlxSprite; var oriCameraZoom:Float; var oriCameraWidth:Int; var oriCameraHeight:Int; var targetZoom:Float; var zoomSpeed:Float; override public function create():Void { super.create(); // Set the camera background as white for visibility sake ... // Create level ... // Create player ... // Make camera follow the player ... // Set camera bounds so the camera doesn't show off-screen area // FlxG.camera.setBounds(0, 0, level.width, level.height); // Get starting camera zoom as reference ... // Set zoom lerp targetZoom = FlxG.camera.zoom; zoomSpeed = 1; } override public function update():Void { super.update(); // Player movement ... // test zoom camera ... // test camera position ... // test camera size ... // When the camera is resized, update out-of-screen tile buffer ... // Re-follow the player ... UpdateCamera(); } function UpdateCamera():Void { // Lerp the camera zoom! FlxG.camera.zoom += (targetZoom - FlxG.camera.zoom) / 2 * (FlxG.elapsed) * zoomSpeed; // NOTE: the code below is cut-pasted from ZoomCamera() // Resize the camera based on the original value ... // NOTE: // If we use camera.setBounds() above, we need to cap the camera's // size to the level's size, otherwise the offset will be wrong. // // If we didn't use camera.setBounds() above, we don't have to // cap the camera's size below. ... // Set final size ... // Update tilemap out-of-screen buffer ... // Update player deadzones ... } function ZoomCamera(val:Float):Void { // set new camera zoom targetZoom += val; // NOTE: the code here was cut-pasted to UpdateCamera() } ... } |
And the result:
It’s not perfect, but I think this should be enough for most camera-zooming issues. This concludes the tutorial on zooming the camera.
Great tutorial(s), but you should really take a long look at flixel-addons repo 🙂
https://github.com/HaxeFlixel/flixel-addons/blob/dev/flixel/addons/display/FlxZoomCamera.hx
Hi sruloart, thanks for the pointer. I can’t believe I missed that! I’ll make another quick post to address this next time. 😀 I will study the API a little more too.