Phaser Platformer Series: 16 Camera Follow
posted on 5 July 2021
All the tutorials so far have been on one screen. We’re now going to expand it out a bit and make larger level. There are several things we’ll need to do to make this work:
- Make our world bigger, and position more platforms so our hero can never get lost in space
- Make the camera follow our hero as they move around the larger level
- Make sure our score and hearts are fixed to the camera so we can always see them
- Add more than one baddie
Here is what we’ll be making:
See the Pen
Phaser Platformer Series: 16 Camera Follow by Digitherium (@digitherium)
on CodePen.
If you are viewing on a mobile, you can open a version of it here without all the codepen chrome taking up screen space so you play it in landscape mode.
To begin with we’ll add more platforms and rejig the positioning of the ones we already have. We could build some walls and box everything in to stop our hero being able to walk outside of the game area, but there is an easier way of doing it by setting the world bounds:
this.physics.world.setBounds(0, 0, 1630, 400);
This defines the world as starting at 0,0 being 1630 across and 400 high. If our hero tries to go across more than 1630px they’ll hit an invisible brick wall.
We then change the layout of our platforms slightly. This is a messy way of doing things, and involves a lot of trial and error with the numbers. At a later date, we’ll look at tilemaps as a much cleaner way of building levels, but doing it this way first helps us to see what goes on under the hood:
platforms.create(1300, 400, 'ground').setScale(2).refreshBody();
platforms.create(400, 400, 'ground').setScale(2).refreshBody();
platforms.create(150, 240, 'ground');
platforms.create(860, 190, 'ground');
The invincibility powerup, spikes and shell were also moved just to fit the level better. What we’ve done is made a much bigger level for our hero to walk around, but our hero can just walk right out of frame currently. We need to make the camera follow them:
camera = this.cameras.main;
camera.setBounds(0, 0, 1630, 400);
camera.startFollow(player, true, 0.05, 0, -200, 120);
The camera already exists with some default settings. Here were are grabbing the camera and then changing its bounds and telling it to follow the player. By setting the bounds, the camera knows when to stop as it approaches the edge of the level. The startFollow function takes in the game object to follow, a boolean on whether to round pixel values (to avoid subpixel rendering and jaggedness), the lerp x and y of the camera (the speed at which the camera tracks, 1 being the fastest, 0 being not at all) and an x and y offset. We are telling the camera to round pixels to make it smoother, and using low numbers for the camera lerp along the x-axis so it almost has a tween on the camera movement instead of it being instant. We only want the camera to follow our hero horizontally, so we set the lerp along the y axis to be 0 which turns off vertical tracking. If this number is anything other than 1, then when we jumped the camera would move up to follow our hero. The offset positions the camera so our player is always in the lower left third of the screen when there is space to do so. Without the offset, the player would appear to be dead center which is not what we’re going for.
So now we have a bigger world, and the camera follows the player. If we left it like this then as the player moves, the score and hearts would get left behind! All our heads up display sprites (the score, the hearts, etc) are just positioned absolutely in the world, but now the world is bigger than one screen we need them fixed to the camera. This is simple enough to do, by adding all our heads up display sprites into a group and then fixing that group to the camera.
//create a contain for our heads up display and add some sprites
hud = this.add.container(0, 0, [scoreCoin, scoreText, heart1, heart2, heart3, heartOutline1, heartOutline2, heartOutline3]);
//lock it to the camera
hud.setScrollFactor(0);
Whilst we are at it, we should fix the logo in place in the background, or even better make it move slightly so we get a parallax effect. We can do that by setting a scroll factor that is less than 1 but more than 0. The closer the number to 0, the less the logo will move. We need to add this extra line to our logo code in create:
logo.setScrollFactor(0.2);
We also need to edit the code for our buildTouchSlider function. This adds the touch movement indicator whenever the user touches the screen. Just like the heads up display, this needs to be fixed to the camera using setScrollFactor. We simply add one line to the bottom of the function so it looks like this:
function buildTouchSlider(game) {
sliderBar = game.add.sprite(0, 0, 'touch-slider');
sliderKnob = game.add.sprite(0, 0, 'touch-knob');
touchSlider = game.add.container(100, 450);
touchSlider.add(sliderBar);
touchSlider.add(sliderKnob);
touchSlider.alpha = 0;
touchSlider.setScrollFactor(0);
}
The last thing that needs changing to accommodate our new fixed HUD is the coin collection. Previously we’ve just tweened the collected coin directly to the position of the scoreCoin on the top left, but now that the scoreCoin is in a fixed container (the hud group) and doesn’t scroll, its position is always 0,0. This means all our coin animations are going to tween to the top left of the whole game and look odd. What we need to do is work out the actual position of the score coin at the moment the coin is collected. We can work this out using how far the camera has scrolled.
On top of that, this score coin is now a moving target! The player and therefore camera can be constantly moving, so if we collect a coin and kick off a tween to go to the score coin’s position at the top left of the screen, a second later the position of score coin has changed! So we also need a way of constantly updating the tween to this new end location (where ever scoreCoin has moved to now).
First we’ll add a global var to store all our coin collection tweens in an array:
coinTweens = [];
Then in out collectCoin function we amend the code slightly:
//tween coin to score coin in corner shrink
var collectCoinTween = this.tweens.add({
targets: coin,
alpha: 0.3,
angle: 720,
x: scoreCoin.x,
y: scoreCoin.y,
scaleX: 0.5,
scaleY: 0.5,
ease: 'Linear',
duration: 500,
onComplete: function() {
destroyGameObject(coin);
coinTweens.shift();
},
});
//add the tween to the tweens array
coinTweens.push(collectCoinTween);
We renamed the tween var to collectCoinTween to make it more obvious what it is. Then every tween is added to our new coinTweens array. When a tween completes, it’s removed from the array using coinTweens.shift();. That just removes the oldest item from the array, and as we are collecting coins in order, it’s safe to assume that’s the oldest one is the one to be removed. This now gives us an array of tweens we can check at the start of our update function like this:
//loop through any coin tweens
for (var i = 0; i < coinTweens.length; i++) {
var tween = coinTweens[i];
//if we find a tween update it to the new position of the scoreCoin
if (tween) tween.updateTo("x", camera.scrollX + scoreCoin.x);
}
So we are constantly checked for the presence of coin tweens, and if we find one, we can update it's end point to be the new position of the score coin. Neat. That should cover all the pinned / fixed items and the changes needed to the display of the game. Now let's add more baddies.
Handling multiple enemies is thankfully really easy. We can just use a Phaser group like we do for platforms and that will handle most of it for us. We need a global var for our new baddies group. As well as that we need one for our camera and also our heads up display (HUD i.e. the score and number of lives you have):
baddies,
camera,
hud;
Because our camera will follow the player, we're going to move the score and hearts into our new 'hud' group and lock it to the camera so it always stays in place.
//create a contain for our heads up display and add some sprites
hud = this.add.container(0, 0, [scoreCoin, scoreText, heart1, heart2, heart3, heartOutline1, heartOutline2, heartOutline3]);
//lock it to the camera
hud.setScrollFactor(0);
Because our game objects are already positioned, all we have to go is create the group, place it at 0,0 and then add all the items. There are other ways of adding items, for instance you can say hud.add(item) after the group is created. As all of our hud objects are already created, it's easiest for us to just pass them all in at once as an array of objects. setScrollFactor(0) tells the group not to scroll at all - i.e. stay fixed to the camera.
Now that our world has opened up a little there is some tidying to do. Our shell can now hit the world's bounds, but at the moment nothing happens. The collision detection makes the shell sit on and react to platforms and walls, but we need to give it some rules so that if it hits a wall (i.e. the side of the shell hits a platform) then the shell should be destroyed. We update our shell collider to use a callback function:
this.physics.add.collider(shell, platforms, shellWallHit, null, this);
That function looks like this:
function shellWallHit(shell, wall) {
//if shell has hit a wall with either the right of left side of itself it should be destroyed
if (shell.body.onWall()) {
destroyShell.call(this);
}
}
shell.body.onWall() checks if the shell is colliding with the platform or world boundary with either it's left or right side. If so we destroy the shell.
Finally, we'll add an extra baddie. This will scale up nicely, so whilst we're just adding 2 baddies, this will work for any number. The idea is that we add all of our baddies to a group, and physics-wise, anything within that group will be treated the same. First, we create the group:
//create our baddies group
baddies = this.physics.add.group();
Then each baddie create using the sam baddie variable. We can keep reusing it temporarily hold values until the baddie is added to the group:
baddie = baddies.create(1220, 350, 'baddie');
baddie.setOrigin(.5, .5);
baddie.setCollideWorldBounds(true);
baddie.body.velocity.x = -100;
baddie.maxDistance = 300;
baddie.previousX = baddie.x;
Then main difference here is that baddies.create is called to create each baddie game object which places it inside the baddies group. The rest of the parameters, particularly the maxDistance and velocity we can tweak for each baddie to set the area in which they patrol.
We place the second baddie on the platform and reduce their maxDistance to 250 like this:
baddie = baddies.create(940, 100, 'baddie');
baddie.setOrigin(.5, .5);
baddie.setCollideWorldBounds(true);
baddie.body.velocity.x = -100;
baddie.maxDistance = 250;
baddie.previousX = baddie.x;
This was just done by eye to stop the second baddie from walking too far and falling off the platform.
We then update all the collisions so instead of checking for a collison with the baddie game object, they are checking against our group baddies:
this.physics.add.collider(baddies, platforms);
...
this.physics.add.overlap(player, baddies, hitBaddie, null, this);
...
this.physics.add.overlap(shell, baddies, shellHitBaddie, null, this);
None of the callback code needs to change, it will already be passing through the individual baddie within the group that collided, so all the hard work is already done for us. The one thing we do need to update is the baddie turning around.
Previously in our game loop, we just checked the baddie game object and checked their maxDistance parameter so see if it was time to turn around. We just need to change our code in the update loop to do the same for all baddies. We can do this by looping through the baddies groups children:
//loop through baddies group and for each baddie...
baddies.getChildren().forEach(function(theBaddie) {
//check if it's time for them to turn around
if (Math.abs(theBaddie.x - theBaddie.previousX) >= theBaddie.maxDistance) {
switchDirection(theBaddie);
}
}, this);
The code that previously worked for just one baddie, will now work for any number, as long as they are placed into this baddies group.
And that'll do us. This is by no means the best way of building a larger level, but it does start to demonstrate how we scale things. The main drawback with this method is placing all the objects using x and y coordinates - it's just a bit fiddly. We will get to tilemaps eventually, which are a far more efficient method of level building. There are a few more platformer tropes to tick off the list before we get there yet though. Next up, another power-up - mushrooms / changing the character sprite.
chromeless mobile version
view all the code on codepen
download the source on github.