This tutorial series has been updated for PixiJS v4.
Welcome to the fourth and final tutorial in the series detailing how to build a parallax scrolling map using JavaScript and pixi.js. In the previous tutorial we started writing the scroller’s foreground layer by implementing an object pool and learning how to work with sprite sheets. Today we’ll actually construct the foreground layer and write code to scroll its game map within the viewport.
What you will learn…
- How to represent a game map in memory
- How to display and scroll a large game map
- The support code required to construct a game map
What you should know…
We’ll continue where we left off. Keep working with the code you produced during the course of the first three tutorials. However, if you prefer you can download the third tutorial’s source code from GitHub and work from there.
By the end we’ll have a scrolling game map that’s almost identical to that found in Half Brick’s excellent Monster Dash game. Remember, our map will be built from a series of wall spans of varying height and width, and we’ll take advantage of our object pool to retrieve the individual slices that make up each wall span. We’ll also add a little icing on the cake by gradually increasing the map’s scrolling speed over time, just like in Monster Dash.
The final version of our scroller can be found above. Simply click the image to see it in action.
Getting Started
If you haven’t already worked your way through the three previous tutorials (part 1, part 2, part 3) then I recommend you do so before proceeding.
Also, remember that you’ll need to be running a local web server in order to test your work. If you haven’t already done so, refer to the first tutorial’s Getting Started section for details regarding how to set up your web server.
Just like the previous tutorial, you’ll need to set aside roughly two hours to make your way through this one.
Wall Slice Types
We’ve already covered the various wall slice types that will make up our foreground layer’s map. For the avoidance of doubt here they are again:
- Front edge
- Back edge
- Step
- Wall decoration
- Window
You can also see each of the wall slice types in the following diagram:
Let’s write a simple class named SliceType
that stores constants that represent each of our slice types. In additional we’ll add one more slice type: a wall gap. The wall gap will essentially represent an invisible slice that’s used to create space between our wall spans.
Open your text editor and create a new file. Add the following to it:
function SliceType() {} SliceType.FRONT = 0; SliceType.BACK = 1; SliceType.STEP = 2; SliceType.DECORATION = 3; SliceType.WINDOW = 4; SliceType.GAP = 5;
Save your file and name it SliceType.js
.
Each constant has an integer assigned to it, starting from zero. This is important as it will allow us to use these constants to create and access lookup tables later in our code.
Let’s not forget to include our class’ source file within the project. Move to your index.html
file and add the following line:
<script src="WallSpritesPool.js"></script> <script src="SliceType.js"></script> <script src="Main.js"></script>
Save your changes.
Starting the Foreground Layer
Let’s now actually create the class that represents our scroller’s foreground layer.
Just like our far and mid layers, our foreground layer will inherit functionality from one of Pixi’s display objects. Whereas the previous two layers were specialised versions of PIXI.extras.TilingSprite
, our foreground layer will inherit from PIXI.Container
. We don’t require Pixi’s tiling sprite functionality for our foreground layer, which is why we’ve opted for PIXI.Container
instead. The PIXI.Container
class provides us with just enough functionality to allow us to add our specialised foreground class to Pixi’s display list just like any other of its display objects.
Our foreground layer will essentially represent our game map. Since our game map consists of a series of wall spans, we’ll name our class Walls
Front
and Map
. However, considering the content represented by this tutorial’s foreground layer, Walls
feels like a suitable name.Okay, let’s start by creating a new file for your class and adding the following to it:
function Walls() { PIXI.Container.call(this); } Walls.prototype = Object.create(PIXI.Container.prototype);
Save the file as Walls.js
.
Our class doesn’t do much at the moment other than inherit functionality from PIXI.Container
. Before we start layering more code into our class let’s first include it within our project and also hook it up to our Scroller
class where both the far and mid layers reside.
Open index.html
and add the following line:
<script src="SliceType.js"></script> <script src="Walls.js"></script> <script src="Main.js"></script>
Save your changes.
Now open Scroller.js
and instantiate your Walls
class within its constructor. Additionally, add the instance to the display list:
function Scroller(stage) { this.far = new Far(); stage.addChild(this.far); this.mid = new Mid(); stage.addChild(this.mid); this.front = new Walls(); stage.addChild(this.front); this.viewportX = 0; }
Again, save your changes.
Integrating Your Object Pool
The Walls
class is going to make heavy use of your object pool. Remember your object pool allows you to borrow wall slice sprites and return them back to the pool when you’re done with them. For example, if you’re constructing a wall that contains a window, then you can obtain a window sprite from the object pool by calling its borrowWindow()
method. Once you no longer have a use for the window sprite you can return it to the pool by calling returnWindow()
. The object pool has similar methods for the other wall slice types.
Let’s create an instance of our object pool within our Walls
class. Open Walls.js
and add the following line to it:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); }
Save you change.
Lookup Tables
In the previous tutorial we utilised two lookup tables to help construct test wall spans. The first lookup table contained references to the ‘borrow’ methods from our object pool that were required to build a specific span. The other table held references to the corresponding ‘return’ methods required to return each slice sprite back to the pool.
We’ll write something similar here, but it will be written in a generic manner allowing us to create any wall spans we like rather than a single specific span. The first lookup table will contain references to our object pool’s five ‘borrow’ methods (one for each wall slice type). The second table will hold references to the pool’s five ‘return’ methods. This will provide a convenient way to manage the borrowing and returning of the wall slice types.
Let’s write a simple method that will setup both lookup tables. Add the following to Walls.js
:
Walls.prototype = Object.create(PIXI.Container.prototype); Walls.prototype.createLookupTables = function() { this.borrowWallSpriteLookup = []; this.borrowWallSpriteLookup[SliceType.FRONT] = this.pool.borrowFrontEdge; this.borrowWallSpriteLookup[SliceType.BACK] = this.pool.borrowBackEdge; this.borrowWallSpriteLookup[SliceType.STEP] = this.pool.borrowStep; this.borrowWallSpriteLookup[SliceType.DECORATION] = this.pool.borrowDecoration; this.borrowWallSpriteLookup[SliceType.WINDOW] = this.pool.borrowWindow; this.returnWallSpriteLookup = []; this.returnWallSpriteLookup[SliceType.FRONT] = this.pool.returnFrontEdge; this.returnWallSpriteLookup[SliceType.BACK] = this.pool.returnBackEdge; this.returnWallSpriteLookup[SliceType.STEP] = this.pool.returnStep; this.returnWallSpriteLookup[SliceType.DECORATION] = this.pool.returnDecoration; this.returnWallSpriteLookup[SliceType.WINDOW] = this.pool.returnWindow; };
In the method above we’ve created two member variables. The first, borrowWallSpriteLookup
is an array that holds references to each of our object pool’s ‘borrow’ methods. The second, returnWallSpriteLookup
is also an array and holds references to each of our object pool’s ‘return’ methods.
Notice the use of our SliceType
class’ constants to index each of our object pool’s methods. For example, we used SliceType.FRONT
to place a reference to the object pool’s borrowFrontEdge()
method at index position 0 within the lookup table’s array:
this.borrowWallSpriteLookup[SliceType.FRONT] = this.pool.borrowFrontEdge;
We’ll use the SliceType
class’ constants later on in this tutorial to access and call the correct ‘borrow’ and ‘return’ methods when rendering the foreground layer’s content. In fact, we’ll write two support methods in just a moment to help us do that. But first, let’s ensure that our lookup tables get created by calling createLookupTables()
from within our class’ constructor.
Add the following line:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); }
Save your changes.
Support Methods for Borrowing and Returning Wall Slices
Now that we have our two lookup tables we can write two very simple support methods: one that will allow us to borrow a particular wall slice sprite from the object pool, and another to return a wall slice sprite back to the pool.
Add the following two methods at the end of the Walls.js
class:
Walls.prototype.borrowWallSprite = function(sliceType) { return this.borrowWallSpriteLookup[sliceType].call(this.pool); }; Walls.prototype.returnWallSprite = function(sliceType, sliceSprite) { return this.returnWallSpriteLookup[sliceType].call(this.pool, sliceSprite); };
The first method, borrowWallSprite()
, takes a wall slice type as a parameter and returns a sprite of that type from the object pool. The second, returnWallSprite()
, expects two parameters: a wall slice type and a previously borrowed sprite of that type. It takes the sprite and returns it to the object pool. If you look at the implementation for both methods you’ll see that the slice type passed to each is used to lookup and call the appropriate object pool method.
Testing the Methods for Borrowing and Returning Wall Slices
To solidify your understanding of the borrowWallSprite()
and returnWallSprite()
methods, let’s write a couple of simple tests that use them.
We’ll start by borrowing a window sprite from the object pool and adding it to the display list. Add the following code to your class’ constructor:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); var sprite = this.borrowWallSprite(SliceType.WINDOW); this.addChild(sprite); }
As you can see from the code above, borrowing a wall slice sprite with our borrowWallSprite()
method is trivial. We passed the SliceType.WINDOW
constant to it in order to receive a window sprite. Passing any of the other constants from the SliceType
class will result in a wall slice sprite of that type being borrowed instead.
Save your code and test your changes within the browser. If everything is working you should see a window sprite sitting at the top-left corner of your stage.
Now let’s write some code to test that we can return our borrowed window sprite back to the object pool using the returnWallSprite()
method. We’ll need to output some stuff to the JavaScript console to satisfy ourselves that the sprite has indeed been returned to the object pool. Add the following:
console.log("Before borrowing window: " + this.pool.windows.length); var sprite = this.borrowWallSprite(SliceType.WINDOW); this.addChild(sprite); console.log("After borrowing window: " + this.pool.windows.length); this.removeChild(sprite); this.returnWallSprite(SliceType.WINDOW, sprite); console.log("After returning window: " + this.pool.windows.length);
Save your changes and test them within the browser.
This time you shouldn’t see the window sprite as it will have immediately been returned back to the object pool. We can further verify that this has happened by checking the object pool’s internal array. After borrowing a window sprite, the array’s length will decrement by one. When the window is returned to the pool, the array’s length will increment back to its original value. You should see the following output within the JavaScript console window:
>> Before borrowing window: 12 >> After borrowing window: 11 >> After returning window: 12
Okay, we’ve seen how to use the borrowWallSprite()
and returnWallSprite()
support methods. Now let’s remove our test code before continuing. Remove the following lines from your constructor:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables();
console.log("before borrowing window: " + this.pool.windows.length);var sprite = this.borrowWallSprite(SliceType.WINDOW);this.addChild(sprite);console.log("after borrowing window: " + this.pool.windows.length);this.removeChild(sprite);this.returnWallSprite(SliceType.WINDOW, sprite);console.log("after returning window: " + this.pool.windows.length);}
Save your changes.
Storing the Game Map in Memory
In the previous tutorial I talked at length about how we will show our foreground layer’s scrolling map. For performance reasons we’ll only display sprites for the wall slices that are actually currently visible within the viewport. However, while we certainly don’t need to display every sprite at once, we do still need to store some sort of representation of the entire map in memory. It’s this map that we’ll read from and determine what section currently falls within the viewport.
We’ll use an array to represent the map and each element of the array will represent a single wall slice.
Declare an empty array within the Walls
class’ constructor:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); this.slices = []; }
We’ve simply declared a new member variable named slices
that will represent the slices that make up the map. It’s important to realise that these slices aren’t directly represented by sprites. Instead each slice will be represented by a data structure that says something about that slice. This will become clearer when we build that data structure, so let’s go ahead and do just that.
Representing a Wall Slice
We’ll create a new class named WallSlice
. Create a new file within your text editor and add the following code to it:
function WallSlice(type, y) { this.type = type; this.y = y; this.sprite = null; } WallSlice.WIDTH = 64;
Save the file and name it WallSlice.js
.
Our class is extremely simple. Its constructor expects two parameters: a value that represents the type of wall slice (one of our six constants from the SliceType
class) that is being represented, and a y-position that the wall slice is to be displayed on screen at.
You can also see that the constructor contains a member variable named sprite
. We’ll use this member variable to associate a sprite borrowed from our sprite pool with a particular slice. This will happen for any slices that are currently within the viewport. When the slice leaves the viewport we’ll return its sprite back to the object pool and nullify its sprite
member variable.
Our class’ three member variables are also meant to be publicly accessible, so you’ll soon see plenty of instances where we use the dot operator within the Walls
class to directly access them.
Finally, our WallSlice
class also contains a constant that stores a wall slice’s width. This will come in handy later when we’re laying out the slices that are currently within the viewport. We’ve used a constant because all wall slices are the same width (64 pixels).
Before we continue, let’s include our WallSlice
class within the project. Open index.html
and add the following line:
<script src="SliceType.js"></script> <script src="WallSlice.js"></script> <script src="Walls.js"></script>
Save your changes.
Now let’s move back to our Walls
class.
Adding Wall Slices to the Game Map
Our Walls
class contains an array that will represent our map but it’s presently empty. Let’s write a simple support method that will create a WallSlice
instance and add it to the array. This will provide us with a mechanism for building our map.
Add the following method to your class:
Walls.prototype = Object.create(PIXI.Container.prototype); Walls.prototype.addSlice = function(sliceType, y) { var slice = new WallSlice(sliceType, y); this.slices.push(slice); };
Save your changes.
Building a Test Map
Each call to addSlice()
will add one more WallSlice
instance to our layer’s slices
array. Therefore, multiple calls to addSlice()
can be used to build a map. Let’s see this in action by writing a simple test method that creates a wall span that’s nine slices long. This should help clarify how the addSlice()
method and the WallSlice
class work.
Add the following test method at the end of your Walls
class:
Walls.prototype.returnWallSprite = function(sliceType, sliceSprite) { return this.returnWallSpriteLookup[sliceType].call(this.pool, sliceSprite); }; Walls.prototype.createTestWallSpan = function() { this.addSlice(SliceType.FRONT, 192); this.addSlice(SliceType.WINDOW, 192); this.addSlice(SliceType.DECORATION, 192); this.addSlice(SliceType.WINDOW, 192); this.addSlice(SliceType.DECORATION, 192); this.addSlice(SliceType.WINDOW, 192); this.addSlice(SliceType.DECORATION, 192); this.addSlice(SliceType.WINDOW, 192); this.addSlice(SliceType.BACK, 192); };
If you were to call createTestWallSpan()
it would create an in-memory representation of the following wall span:
Notice how we made use of the SliceType
class’ constants to dictate the type of each wall slice. I’ve marked each type in the diagram above. We also ensured that each slice in the wall span sat at the same vertical position. In this particular case, every slice was assigned a y-position of 192 pixels.
Let’s create another test method. This time we’ll write a method that creates an in-memory representation of a stepped wall span:
Add the following at the end of your class:
Walls.prototype.createTestSteppedWallSpan = function() { this.addSlice(SliceType.FRONT, 192); this.addSlice(SliceType.WINDOW, 192); this.addSlice(SliceType.DECORATION, 192); this.addSlice(SliceType.STEP, 256); this.addSlice(SliceType.WINDOW, 256); this.addSlice(SliceType.BACK, 256); };
Our stepped wall span is six slices long. Also notice that the first three slices are assigned a y-position of 192 pixels while the remaining three are assigned a y-position of 256 pixels. This helps create the stepped span’s upper and lower platforms. The diagram below shows a visual representation of how our stepped wall span will look:
Let’s create another test method. This one will add a gap into the map allowing us to place gaps between our wall spans. Add the following at the end of the class.
Walls.prototype.createTestGap = function() { this.addSlice(SliceType.GAP); };
Calling all three test methods (createTestWallSpan()
, createTestSteppedWallSpan()
and createTestGap()
) once won’t help us create a particularly long map. However we can write one final test method that can rectify that. Add the following to your class:
Walls.prototype.returnWallSprite = function(sliceType, sliceSprite) { return this.returnWallSpriteLookup[sliceType].call(this.pool, sliceSprite); }; Walls.prototype.createTestMap = function() { for (var i = 0; i < 10; i++) { this.createTestWallSpan(); this.createTestGap(); this.createTestSteppedWallSpan(); this.createTestGap(); } };
We’ve used a simple for-loop to ensure that our class’ slices
array gets populated with a healthy number of wall slices. There’s a predictable pattern to our map but for early testing purposes that’s fine. Our map will contain the following content repeated ten times:
9-slice wall span, gap, 6-slice stepped wall span, gap
Now all that’s left to do is to actually call our createTestMap()
method from our class’ constructor. Doing so will ensure that the slices
array actually gets populated:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); this.slices = []; this.createTestMap(); }
Save your changes and run the latest version of your code within the browser. You won’t see anything new as our test map is currently only an in-memory representation, however check Chrome’s JavaScript console and ensure you have no errors. If there are errors then fix them before proceeding.
Implementing a Viewport
If you think back to the second tutorial you should remember that we added the concept of a viewport to our Scroller
class. As you may remember, the viewport is essentially a window looking onto your game world. It’s possible to look at other regions of your game world by moving the viewport to a new position. To facilitate this, a method named setViewportX()
was added to our Scroller
class. In turn, both our Far
and Mid
classes implemented their own setViewportX()
methods, allowing both layers to handle the rendering of their content.
Similarly we need to provide a setViewportX()
method for our Walls
class and implement functionality within our class that will display the correct region of our game map based on the viewport’s position.
To start with we’ll need a couple of constants to help manage the rendering of our viewport: one that specifies the width of the viewport (measured in pixels) and another that specifies how many wall slices can be shown in the viewport at any one time. Add the following:
Walls.prototype = Object.create(PIXI.Container.prototype); Walls.VIEWPORT_WIDTH = 512; Walls.VIEWPORT_NUM_SLICES = Math.ceil(Walls.VIEWPORT_WIDTH/WallSlice.WIDTH) + 1;
Our viewport has a width of 512 pixels. Using this value and the width of a slice, we also calculate and store the number of wall slices that can be seen within the viewport at any one time. You might think that the number of slices is simply the viewport’s width divided by the width of a wall slice. However, since the first and last slices within the viewport may actually only be partially within view, it means that on most occasions there will actually be one more slice on screen.
You can see what I mean if you look at the two diagrams below. The first shows eight wall slices sitting snugly within the viewport (with a ninth just outside). In the second diagram however, the first and last wall slices are only partially in view (this will be a common occurrence during scrolling), which means that a total of nine wall slices can be shown within the viewport.
We’ll also need two member variables to track the viewport’s position and the index position of the first wall slice that’s currently within the viewport. Add the following to your constructor:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); this.slices = []; this.createTestMap(); this.viewportX = 0; this.viewportSliceX = 0; }
The viewportX
member will simply store the viewport’s current x-position.
The viewport’s x-position will also help dictate which wall slices from our slices
array currently fall within the viewport. For example, assume the viewport’s x-position is 0 and that we require 9 wall slices to cover our viewport. This means that the first 9 slices from the slices
array will fall within the viewport (index positions 0 to 8). This is where the viewportSliceX
member variable comes into play. We’ll use it to store the index position of the first of those slices. In the case of this example, it’ll hold a value of 0.
Here’s another example. If the viewport were set to an x-position of 128 pixels, then given the fact that a wall slice is 64 pixels wide, we’d actually skip the first two slices and instead show the 3rd to the 11th wall slice (index 2 to 10) within the viewport. Our viewportSliceX
member variable would therefore hold a value of 2.
Here’s one more example. I’ll also provide a diagram to help you visualise things and see how each member variable is used to represent the viewport’s state. This time our viewport’s x-position is set to 416 pixels. If you divide 416 by 64 (the width of a wall slice) you’ll get 6.5. If we round this value up then it tells us that the 7th wall slice (index 6) will be the first slice to sit within the viewport. Therefore our viewportSliceX
member variable will hold a value of 6.
The fact that 416 isn’t perfectly divisible by 64 also tells us that the 7th wall slice (index 6) will actually sit partially outside the viewport’s left-hand side. Since the remainder was 0.5 it means that the slice will sit outside the viewport by 32 pixels (0.5 * 64). This is also true of the slice on the far-right of the viewport, which will sit partially outside the viewport’s right-hand side by 32 pixels also.
While it may sound complicated, there really isn’t much to it, and we actually capture a surprising amount of information regarding the viewport’s state with only the following three member variables: slices
, viewportX
, and viewportSliceX
. With this information we can actually go on to render the region of the game map that’s currently within the viewport.
Rendering the Game Map
Just like the Far
and Mid
classes, our Walls
class will need a setViewportX()
method, which will be responsible for rendering the correct region of our game map that falls within the viewport.
Starting the setViewportX()
Method
Let’s start by writing a stub setViewportX()
method that can be called from our Scroller
class. This will allow our front layer (i.e. the Walls
class) to be scrolled along with the far and mid layers. Add the following code to your Walls
class:
Walls.VIEWPORT_WIDTH = 512; Walls.VIEWPORT_NUM_SLICES = Math.ceil(Walls.VIEWPORT_WIDTH/WallSlice.WIDTH) + 1; Walls.prototype.setViewportX = function(viewportX) { };
Save your changes and open Scroller.js
. Make a call to your setViewportX()
method from within the scroller’s own setViewportX()
method:
Scroller.prototype.setViewportX = function(viewportX) { this.viewportX = viewportX; this.far.setViewportX(viewportX); this.mid.setViewportX(viewportX); this.front.setViewportX(viewportX); };
Now save your changes and check within your browser that no errors are being reported. Once again, there will be nothing new to see when you run your code but it’s always good to check that you haven’t introduced any errors as you develop.
Walls
class’ setViewportX()
method is actually being called then simply add a log statement to its method. The following will do:
console.log("Walls::setViewportX( " + viewportX + ");");
This will write the name of the method to the JavaScript console window and also the viewport’s x-position.
Now move back to the Walls
class’ file.
Boundary Checking
Our far and mid layers simply shift a repeating texture essentially creating infinitely scrolling layers. The foreground layer however represents a game map that has a definite start and end point. This means that we’ll need to actually check the viewport’s x-position within our setViewportX()
method and ensure that the viewport does not fall outside the map’s boundaries. Let’s add some code to restrict the viewport:
Walls.prototype.setViewportX = function(viewportX) { var maxViewportX = (this.slices.length - Walls.VIEWPORT_NUM_SLICES) * WallSlice.WIDTH; if (viewportX < 0) { viewportX = 0; } else if (viewportX >= maxViewportX) { viewportX = maxViewportX; } this.viewportX = viewportX; };
The code above is straightforward. The only potential gotcha is the calculation for the viewport’s maximum permitted x-position. The temptation is to simply measure the width of the entire map and use that as the upper limit for our boundary check. However, since the viewport’s x-position is rendered from the screen’s left-hand side we need to subtract the viewport’s width from the final value. Our calculation initially works on wall slices then multiplies that value by the width of a slice (measured in pixels) to get the upper limit.
Before proceeding, let’s refactor our boundary check code into its own method, which we’ll name checkViewportXBounds()
. Make the following addition to your class:
Walls.prototype.addSlice = function(sliceType, y) { var slice = new WallSlice(sliceType, y); this.slices.push(slice); }; Walls.prototype.checkViewportXBounds = function(viewportX) { var maxViewportX = (this.slices.length - Walls.VIEWPORT_NUM_SLICES) * WallSlice.WIDTH; if (viewportX < 0) { viewportX = 0; } else if (viewportX > maxViewportX) { viewportX = maxViewportX; } return viewportX; };
Now move back to your setViewportX()
method and remove your original boundary check code:
Walls.prototype.setViewportX = function(viewportX) {
var maxViewportX = (this.slices.length - Walls.VIEWPORT_NUM_SLICES) *WallSlice.WIDTH;if (viewportX < 0){viewportX = 0;}else if (viewportX > maxViewportX){viewportX = maxViewportX + 1;}this.viewportX = viewportX;};
Replace it with a call to your checkViewportXBounds()
method:
Walls.prototype.setViewportX = function(viewportX) { this.viewportX = this.checkViewportXBounds(viewportX); };
Notice that the checkViewportXBounds()
method takes the specified x-position for the viewport, corrects it if it’s out of bounds, then returns it. We store that returned value within our viewportX
member variable.
Save your changes. You may also want to test your code in the browser and once again check that there are no runtime errors being reported in the JavaScript console.
Calculating the Index Position of the Viewport’s First Wall Slice
So we now have the viewport’s current position safely stored within our viewportX
member variable. Using that value we can calculate the index position of the first wall slice that will fall within the viewport. But first, we’ll need to store the previous index position as it’ll come into play when determining which wall slices can be removed from the display list. We’ll use a local variable for this. Add the following:
Walls.prototype.setViewportX = function(viewportX) { this.viewportX = this.checkViewportXBounds(viewportX); var prevViewportSliceX = this.viewportSliceX; };
Now let’s add a line immediately after that to actually calculate the index position of the first wall slice that will fall within the viewport:
Walls.prototype.setViewportX = function(viewportX) { this.viewportX = this.checkViewportXBounds(viewportX); var prevViewportSliceX = this.viewportSliceX; this.viewportSliceX = Math.floor(this.viewportX/WallSlice.WIDTH); };
It’s easy to calculate the index position of the first wall slice that currently falls within the viewport. Simply take the current viewport position and divide it by the width of a wall slice. This will return a floating point value, so we’ll need to call Math.floor()
to convert it into an integer that can be used to access our slices
array.
Save your changes.
Rendering Wall Slices
We’re now in a position to actually render the wall slices that fall within the viewport. We’ll also need to remove any wall slices that were previously within the viewport but have now moved offscreen. First we’ll concentrate on rendering the wall slices that are within view.
Adding Wall Slices
The general principle for rendering our wall slices isn’t difficult to grasp. Remember, we already know the index position of the first wall slice (viewportSliceX
) that falls within the viewport, and we also know how many slices fit within the viewport (Walls.VIEWPORT_NUM_SLICES
). Using that information we can pick out the WallSlice
objects from the slices
array that we need to work with. The diagram below should help clarify this.
As you can see, in our example, slices 7 to 15 currently fall within the viewport. Remember that each element within the slices
array is actually represented by a WallSlice
object. Each WallSlice
object has a type
property that tells us which type of wall slice we are dealing with. Knowing the slice type allows us to borrow a sprite that represents that type from the object pool. Once we’ve obtained the sprite we associate it with the slice’s WallSlice
object and also add it to the display list.
Associating the sprite with a WallSlice
object is easy: we simply set the object’s sprite
property to point to the sprite. This is important during subsequent calls to setViewportX()
as it lets us know whether or not we need to borrow a sprite from the object pool for this particular slice. If the object’s sprite
property is null
then we know we do; otherwise we know that we’ve already associated a sprite with that slice. Essentially we only need to borrow a sprite for a wall slice when it first falls within the viewport. On each subsequent call to setViewportX()
we only need to update the on-screen x-position of that sprite to give the illusion that the map is being scrolled.
A Simple Example
Let’s clarify all this with a series of diagrams. We’ll begin just after the construction of the in-memory representation of your map. In the diagram below we can see the slices
array and the state of each of the array’s WallSlice
objects. We haven’t yet made a call to setViewportX()
.
Now we’ll make our first call to setViewportX()
and pass an x-position of 0 pixels to it. This will result in slices 0 to 8 falling within the viewport. We’ll read each slice’s type
property and use it to borrow a sprite for each from the object pool. Additionally, we’ll associate each sprite with its WallSlice
object and also add it to the display list. The vertical position for each sprite is obtained by reading its WallSlice
object’s y
property, and the horizontal position is deduced from the viewport’s current x-position. All this is illustrated by the following diagram:
Now another request to setViewportX()
is made with the viewport being set to an x-position of 40 this time. As you can see from the diagram below, the same 8 slices still fall within our viewport. When reading each slice’s sprite
property, we’ll see that they already have a sprite associated with them, meaning that there’s no need to borrow any more sprites from the object pool. So all that’s needing done this time is to update the x-position of each wall slice’s sprite by moving them left by 40 pixels.
Let’s move the viewport on by another 40 pixels. Slices 1 to 9 now fall within the viewport. When we examine slices 1 to 8 we again see that each already has a sprite associated with it. So once again we simply update their horizontal positions by another 40 pixels. However, as you can see from the diagram below, slice 9 doesn’t yet have a sprite associated with it.
For slice 9 we’ll once again need to borrow a sprite from the object pool. So we’ll check the slice’s type
property and then borrow a sprite of that type. Once the sprite has been obtained we’ll position it on screen and associated it with the slice’s WallSlice
object. You can see this below:
In this simplified example, only one new slice moved into the viewport during our calls to setViewportX()
. However, when moving the viewport by larger distances it’s likely that several new slices will move into view. If the distance is large enough then the entire viewport may even be replaced with new slices.
It’s also worth noting that while our example only updated the viewport’s position three times, our actual code will call the setViewportX()
method sixty times every second. This will enable us to very smoothly scroll the map by shifting the viewport’s x-position by a small amount on each call.
Okay, with this clear in your head let’s write some code.
Back to Writing Code
We’ll write a new method named addNewSlices()
and call it from our setViewportX()
method. Let’s begin by making the call from setViewportX()
:
Walls.prototype.setViewportX = function(viewportX) { this.viewportX = this.checkViewportXBounds(viewportX); var prevViewportSliceX = this.viewportSliceX; this.viewportSliceX = Math.floor(this.viewportX/WallSlice.WIDTH); this.addNewSlices(); };
Now let’s start writing the addNewSlices()
method. Add the following immediately after your checkViewportXBounds()
method:
Walls.prototype.checkViewportXBounds = function(viewportX) { var maxViewportX = (this.slices.length - Walls.VIEWPORT_NUM_SLICES) * WallSlice.WIDTH; if (viewportX < 0) { viewportX = 0; } else if (viewportX > maxViewportX) { viewportX = maxViewportX; } return viewportX; }; Walls.prototype.addNewSlices = function() { };
Now add a for-loop that will walk through the slices that fall within the viewport:
Walls.prototype.addNewSlices = function() { for (var i = this.viewportSliceX; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++) { } };
Our loop’s iterator counts from the viewportSliceX
value upwards. However when positioning each of our wall slices we’ll also need an iterator that starts from zero. Let’s add an additional variable to our loop for this purpose and also increment it on each iteration of the loop:
Walls.prototype.addNewSlices = function() { for (var i = this.viewportSliceX, sliceIndex = 0; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++, sliceIndex++) { } };
It’s important that we know the x-position (measured in pixels) of the viewport’s first wall slice. We can calculate this by dividing the viewport’s x-position by the width of a wall slice, then taking the remainder. The modulo operator comes in handy for this. We’ll store the value within a local variable just before our loop:
Walls.prototype.addNewSlices = function() { var firstX = -(this.viewportX % WallSlice.WIDTH); for (var i = this.viewportSliceX, sliceIndex = 0; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++, sliceIndex++) { } };
Notice that we change our firstX
variable's sign to negative after performing our calculation. The reason for this is that our wall slices must be shifted to the left in order to simulate the viewport scrolling to the right.
Okay, now we're in a position to access each of the wall slices that fall within the viewport. Once we have a slice there are two groups of operations we can perform on it:
- If the wall slice has just moved into the viewport...
- Borrow a sprite for it from the object pool
- Set the sprite's on-screen position
- Add the sprite to the display list
- If the wall slice is already within the viewport...
- Update its sprite's on-screen position
Let's write some code to obtain a wall slice then add the logic required to determine what to do with it. Add the following:
Walls.prototype.addNewSlices = function() { var firstX = -(this.viewportX % WallSlice.WIDTH); for (var i = this.viewportSliceX, sliceIndex = 0; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++, sliceIndex++) { var slice = this.slices[i]; if (slice.sprite == null && slice.type != SliceType.GAP) { // Associate the slice with a sprite and update the sprite's position } else if (slice.sprite != null) { // The slice is already associated with a sprite. Just update its position } } };
Our if clause checks to see if the current slice has just moved into the viewport. If this is the case then the slice won't currently have a sprite associated with it. We also check within the clause that the slice does not represent a gap. Gaps are special cases that aren't represented by a sprite, so it's important we don't attempt to borrow a sprite for them from the object pool.
If these conditions aren't met then we drop down into our else-if clause where we check to see if the wall slice already has a sprite associated with it. If this is the case then we know we are working with a wall slice that was already within the viewport rather than one that has just come into view.
Now add some code to handle a slice that has just come into view:
Walls.prototype.addNewSlices = function() { var firstX = -(this.viewportX % WallSlice.WIDTH); for (var i = this.viewportSliceX, sliceIndex = 0; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++, sliceIndex++) { var slice = this.slices[i]; if (slice.sprite == null && slice.type != SliceType.GAP) { slice.sprite = this.borrowWallSprite(slice.type); slice.sprite.position.x = firstX + (sliceIndex * WallSlice.WIDTH); slice.sprite.position.y = slice.y; this.addChild(slice.sprite); } else if (slice.sprite != null) { } } };
The code you've just added borrows a sprite for our wall slice, sets its position, then adds it to the display list. Notice the use of our borrowWallSprite()
support method, which takes a slice type as an argument and returns a sprite from the object pool. Immediately after obtaining the sprite we associate it with the wall slice via the slice's sprite
property.
Once we have the sprite we can set its x and y positions. The x-position is calculated relative to the x-position of the first slice within the viewport. Remember that we calculated and stored the position of the first slice within a local variable named firstX
. The vertical position of each wall slice is stored within its object's y
property. We simply set our sprite's y-position to that value.
Finally, we add the sprite to the display list which makes it visible on screen.
Now all we need to do is handle the case where we are dealing with a wall slice that was already within the viewport. This is simply a matter of updating the sprite's x-position based on the viewport's current position. We've already done this for a brand new slice sprite, so we'll just re-use the appropriate line of code. Add the following:
Walls.prototype.addNewSlices = function() { var firstX = -(this.viewportX % WallSlice.WIDTH); for (var i = this.viewportSliceX, sliceIndex = 0; i < this.viewportSliceX + Walls.VIEWPORT_NUM_SLICES; i++, sliceIndex++) { var slice = this.slices[i]; if (slice.sprite == null && slice.type != SliceType.GAP) { slice.sprite = this.borrowWallSprite(slice.type); slice.sprite.position.x = firstX + (sliceIndex * WallSlice.WIDTH); slice.sprite.position.y = slice.y; this.addChild(slice.sprite); } else if (slice.sprite != null) { slice.sprite.position.x = firstX + (sliceIndex * WallSlice.WIDTH); } } };
That's our addNewSlices()
method complete. Save your changes.
Let's See Some Scrolling In Action
We've yet to deal with the removal of wall slice sprites that move out of the viewport, but we can test the current version of our code to a certain degree.
Run the latest version within Chrome and if everything goes according to plan you should actually see some walls go scrolling past the viewport. After a few moments, things will come to a sudden halt and the following error will be reported within the JavaScript console:
> Uncaught TypeError: Cannot read property 'position' of undefined
So why has this happened? Well, since we aren't yet returning any wall sprites back to our object pool it will very quickly run dry. That's exactly what has happened here. Our code has tried to borrow another wall slice sprite only for undefined
to be returned.
Removing Wall Slices
As discussed, we need to return any wall slice sprites that are no longer required back to the object pool. If we don't then our object pool will very quickly dry up. Additionally, if we don't manage wall slices that have scrolled out of view then their sprites will persist within the display list, which will eventually impact render performance.
Let's walk through an example that highlights the steps required to remove wall slices that have scrolled out of view.
Once again we’ll begin just after the construction of the in-memory representation of our map. As before, we can see the slices
array and the state of each of the array’s WallSlice
objects.
Our first call to setViewportX()
is made and an x-position of 0 is passed to it. Sprites are obtained from the object pool for slices 0 to 8 and rendered to the screen.
The next call to setViewportX()
moves the viewport on by 100 pixels, which pushes slice 0 out of the viewport's left-hand side:
Since slice 0 is no longer within view there's really no need for us to use a sprite to represent it. Instead we'll remove the sprite from the display list and return it back to the object pool. We'll also nullify the slice's sprite
property too. You can see this in the diagram below.
In this example, only one wall slice moved outside the viewport. However, when moving the viewport by larger distances it’s likely that several slices will move out of view. If the distance is large enough then all the slices that were previously within the viewport may actually find themselves outside it.
Time For More Code
Okay, let's write some code to handle wall slices that have moved outside the viewport. We'll write a method named removeOldSlices()
for this. Let's start by making a call to it from our setViewportX()
method:
Walls.prototype.setViewportX = function(viewportX) { this.viewportX = this.checkViewportXBounds(viewportX); var prevViewportSliceX = this.viewportSliceX; this.viewportSliceX = Math.floor(this.viewportX/WallSlice.WIDTH); this.removeOldSlices(prevViewportSliceX); this.addNewSlices(); };
Notice that we pass our prevViewportSliceX
variable to removeOldSlices()
. It'll use it to figure out how many slices have scrolled out of view. Also notice that we actually call removeOldSlices()
before addNewSlices()
. By handling the wall slices that have scrolled off-screen first we can guarantee that all unused sprites are returned to the object pool and ready to be borrowed by our addNewSlices()
method.
Now we can begin writing our removeOldSlices()
method. Add the following:
Walls.prototype.removeOldSlices = function(prevViewportSliceX) { }; Walls.prototype.addNewSlices = function() {
As you can see, our method takes one parameter: the index position of the first wall slice that fell within the viewport during the previous call to setViewportX()
. Since we have the index position of the first wall slice that currently sits within the viewport (the viewportSliceX
member variable) we can easily subtract the two to find out how many slices the viewport has scrolled by. Let's go ahead and make that calculation:
Walls.prototype.removeOldSlices = function(prevViewportSliceX) { var numOldSlices = this.viewportSliceX - prevViewportSliceX; };
In the code above we've stored our value within a local variable named numOldSlices
. As well as telling us how many slices the viewport has scrolled by, it also let's us know just how many slices have scrolled out of view and should be removed. For example, if the viewport has scrolled by 4 wall slices then it means that 4 wall slices have also scrolled out of view.
We're only interested in dealing with slices that have scrolled out of view and are actually associated with a sprite borrowed from the object pool. Given the width of the viewport, there will always be an upper limit (Walls.VIEWPORT_NUM_SLICES
) to the number of wall slices that are associated with sprites at any one time. However, the value stored within our numOldSlices
variable could actually be higher than this upper limit. This can happen when a call to setViewportX()
is made that moves the viewport by an extremely large distance.
As an example, let's say the viewport's x-position is moved on by 2048 pixels. In such a case the viewport would have moved by 32 wall slices, which is clearly far more slices than is shown on screen at any one time. We need to check for this and cap the value of our numOldSlices
variable if it exceeds our upper limit of Walls.VIEWPORT_NUM_SLICES
. Add the following few lines to accomplish this:
Walls.prototype.removeOldSlices = function(prevViewportSliceX) { var numOldSlices = this.viewportSliceX - prevViewportSliceX; if (numOldSlices > Walls.VIEWPORT_NUM_SLICES) { numOldSlices = Walls.VIEWPORT_NUM_SLICES; } };
Now we can write a for-loop that will, starting from the slice at index position prevViewportSliceX
, obtain each of the wall slices that have scrolled out of view. Add the following:
Walls.prototype.removeOldSlices = function(prevViewportSliceX) { var numOldSlices = this.viewportSliceX - prevViewportSliceX; if (numOldSlices > Walls.VIEWPORT_NUM_SLICES) { numOldSlices = Walls.VIEWPORT_NUM_SLICES; } for (var i = prevViewportSliceX; i < prevViewportSliceX + numOldSlices; i++) { var slice = this.slices[i]; } };
With a wall slice being obtained on each iteration of the loop, all that's left to do is: obtain its sprite; return the sprite to the object pool; remove the sprite from the display list; and finally disassociate the sprite from the wall slice. Add the following code to accomplish this:
Walls.prototype.removeOldSlices = function(prevViewportSliceX) { var numOldSlices = this.viewportSliceX - prevViewportSliceX; if (numOldSlices > Walls.VIEWPORT_NUM_SLICES) { numOldSlices = Walls.VIEWPORT_NUM_SLICES; } for (var i = prevViewportSliceX; i < prevViewportSliceX + numOldSlices; i++) { var slice = this.slices[i]; if (slice.sprite != null) { this.returnWallSprite(slice.type, slice.sprite); this.removeChild(slice.sprite); slice.sprite = null; } } };
That's our Walls
class complete!
Save your changes and test the current version of your project in the browser. This time your viewport should happily scroll to the end of your foreground layer's map. As new slices come into the viewport, our code will borrow suitable sprites for them from the object pool, and as slices leave the viewport their associated sprites will be returned to the object pool. When the end is reached, our foreground layer will stop scrolling.
Building a Larger Map
Of course, we're working with a short test map at the moment so you won't see much variety in its design and the viewport will reach the end of the map very quickly. Adding a larger and more interesting map however can be time consuming unless we're a little smarter about it. Rather than bundling a gigantic list of addSlice()
calls within our Walls
class, let's go ahead and build a support class that will allow us to build a map more easily.
We'll name our support class MapBuilder
and have it provide us with the ability to easily add the following things to a map:
- Gaps of a specified length
- Wall spans of a specified height and length
- Stepped wall spans of a specified height and length
To provide a little more flexibility, we also want to be able to control whether a wall span has a front and/or rear edge. This will come in handy in a few situations.
We'll also write a handful of helper methods that will let us add the fundamental building blocks that each wall span is built from.
Starting the Map Builder Class
Begin by creating a new file. Add the constructor for our class, an array that we'll make use of, and a stub method that we'll expand upon later:
function MapBuilder(walls) { this.walls = walls; this.createMap(); } MapBuilder.WALL_HEIGHTS = [ 256, // Lowest slice 224, 192, 160, 128 // Highest slice ]; MapBuilder.prototype.createMap = function() { };
The constructor has a single member variable named walls
, which will hold a reference to our Walls
object. It's with this reference that we'll be able to construct the map by making calls to its addSlice()
method.
Our map will have five possible heights for its wall spans, so we've placed them within an array named WALL_HEIGHTS
. The lowest span will have a y-position of 256 pixels, and we'll move in steps of 32 pixels for each increase in height. When constructing wall spans we'll use an index into that array to specify the span's height rather than explicitly stating the height in pixels. This will be a little tidier and certainly easier to manage if for any reason we decide to adjust the heights at a later date.
Finally, we have a stub method named createMap()
, which is called from the constructor. It'll eventually be responsible for building our map but for the time being we'll leave it empty.
Save your file and name it MapBuilder.js
.
Integrating The Map Builder
Before we add any more code to our map builder let's hook it up to the rest of our project.
Open index.html
and add the following line to it:
<script src="Walls.js"></script> <script src="MapBuilder.js"></script> <script src="Main.js"></script>
Save the file and move to Scroller.js
.
Now create an instance of the map builder and pass your front
member variable to its constructor:
function Scroller(stage) { this.far = new Far(); stage.addChild(this.far); this.mid = new Mid(); stage.addChild(this.mid); this.front = new Walls(); stage.addChild(this.front); this.mapBuilder = new MapBuilder(this.front); this.viewportX = 0; }
Save your change and move back to MapBuilder.js
.
The Helper Methods
Now let's add those helper methods I was just talking about. We'll start by writing methods to add the front and back edges of a wall span. Add the following two methods at the end of your class:
MapBuilder.prototype.createMap = function() { }; MapBuilder.prototype.addWallFront = function(heightIndex) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; this.walls.addSlice(SliceType.FRONT, y); }; MapBuilder.prototype.addWallBack = function(heightIndex) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; this.walls.addSlice(SliceType.BACK, y); };
Both methods are extremely simple. Each takes a height index as its parameter and uses that index to obtain the wall slice's y-position from the WALL_HEIGHTS
array. Once the y-position has been obtained, a wall slice of the correct type and height is added to the map by calling addSlice()
.
We've tackled the edges of a wall span, now let's add a helper method that will generate the slices required to build a wall span's mid section. Add the following code:
MapBuilder.prototype.addWallBack = function(heightIndex) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; this.walls.addSlice(SliceType.BACK, y); }; MapBuilder.prototype.addWallMid = function(heightIndex, spanLength) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; for (var i = 0; i < spanLength; i++) { if (i % 2 == 0) { this.walls.addSlice(SliceType.WINDOW, y); } else { this.walls.addSlice(SliceType.DECORATION, y); } } };
Our addWallMid()
methods takes two parameters: an index that represents the height of the span, and the number of slices to add to the map.
I won't go into too much detail regarding the method's implementation as we actually wrote test code that was very similar in the previous tutorial. Basically, a loop is used to add each wall slice, with a window slice being added on every odd iteration of the loop and a wall decoration slice being added on every even iteration.
One more helper method is required. Remember, we'll need to deal with stepped wall spans so let's add a method that adds a single wall step to our map:
MapBuilder.prototype.addWallMid = function(heightIndex, spanLength) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; for (var i = 0; i < spanLength; i++) { if (i % 2 == 0) { this.walls.addSlice(SliceType.WINDOW, y); } else { this.walls.addSlice(SliceType.DECORATION, y); } } }; MapBuilder.prototype.addWallStep = function(heightIndex) { var y = MapBuilder.WALL_HEIGHTS[heightIndex]; this.walls.addSlice(SliceType.STEP, y); };
You may be asking why we didn't add a helper to create a whole mid section for a stepped wall span. Well we already have enough helper methods to do just that. Think of a stepped wall span as being two wall spans connected by a step, with the first span being higher than the second. Knowing that, we can create the mid section of a stepped wall span with a call to addWallMid()
, then addWallStep()
, and finally another call to addWallMid()
.
Okay, save your work.
The Map Building Methods
With our helper methods complete we can now add the three main methods that are required to build a map. Those methods will be named:
createGap()
createWallSpan()
createSteppedWallSpan()
We can construct an entire map with these three methods, and we'll do just that by making calls to them from within our map builder's createMap()
method.
Generating Gaps
Let's start by writing the createGap()
method. Add the following:
MapBuilder.prototype.createMap = function() { }; MapBuilder.prototype.createGap = function(spanLength) { for (var i = 0; i < spanLength; i++) { this.walls.addSlice(SliceType.GAP); } };
The createGap()
method is the simplest of our three methods. It generates a gap of a specified length (measured in wall slices).
It's worth noting that we only passed one argument (a wall slice type) to addSlice()
whereas it also expects the slice's y-position. Since a gap doesn't have a visual representation there's no need to specify a y-position for it. By ignoring the second argument it will implicitly default to null
which, in this case, is fine.
Generating a Wall Span
We now need a method to generate a wall span. Start by adding the following and then I'll explain the method's signature:
MapBuilder.prototype.createWallSpan = function( heightIndex, spanLength, noFront, noBack ) { };
The createWallSpan()
method takes four parameters. The first is an index representing the height of the span. The second is the number of slices that the span will stretch for. The third and fourth parameters are boolean values stating whether the span's front and back wall slices are to be omitted. For the avoidance of doubt, passing true
for either will ensure that the slice is not included in the span.
The third and fourth parameters are optional and if omitted will default to false
, meaning the slice will be included. Let's add some code to handle this:
MapBuilder.prototype.createWallSpan = function( heightIndex, spanLength, noFront, noBack ) { noFront = noFront || false; noBack = noBack || false; };
The two lines we added simply check that the noFront
and noBack
parameters have a value assigned to them. If either is undefined
then it will be defaulted to false
.
We can break the remainder of this method into three distinct parts:
- Add the wall's front slice (if required)
- Add the wall's mid section
- Add the wall's back slice (if required)
Let's begin by writing the code to add the front slice. Add the following:
MapBuilder.prototype.createWallSpan = function( heightIndex, spanLength, noFront, noBack ) { noFront = noFront || false; noBack = noBack || false; if (noFront == false && spanLength > 0) { this.addWallFront(heightIndex); spanLength--; } };
Our code is once again straightforward. If the front slice is to be included and if the wall's specified length is greater than 0 then we add it to the map. Notice the use of our class' addWallFront()
helper method to actually add the front slice to the map. Also notice that we decrement our method's spanLength
parameter. This lets us track how many slices are required to build the remainder of the span.
Now let's add the code that handles the wall's mid section:
MapBuilder.prototype.createWallSpan = function( heightIndex, spanLength, noFront, noBack ) { noFront = noFront || false; noBack = noBack || false; if (noFront == false && spanLength > 0) { this.addWallFront(heightIndex); spanLength--; } var midSpanLength = spanLength - (noBack ? 0 : 1); if (midSpanLength > 0) { this.addWallMid(heightIndex, midSpanLength) spanLength -= midSpanLength; } };
We calculated the number of slices that are required for the mid section and stored it in a local variable named midSpanLength
. If the value of midSpanLength
is greater than 0 then we go ahead and generate the mid section using our addWallMid()
helper method. Finally, we decrement the number of slices consumed by the mid section from our method's spanLength
parameter.
Finally, we deal with the wall's back slice. Add the following code to our method:
MapBuilder.prototype.createWallSpan = function( heightIndex, spanLength, noFront, noBack ) { noFront = noFront || false; noBack = noBack || false; if (noFront == false && spanLength > 0) { this.addWallFront(heightIndex); spanLength--; } var midSpanLength = spanLength - (noBack ? 0 : 1); if (midSpanLength > 0) { this.addWallMid(heightIndex, midSpanLength) spanLength -= midSpanLength; } if (noBack == false && spanLength > 0) { this.addWallBack(heightIndex); } };
Save your code.
Generating a Stepped Wall Span
Only one more method is required before we can actually build our map. Generating a stepped wall span won't be difficult because we'll take advantage of methods already provided by our class. As we did previously, we'll begin with the method's signature. Write the following immediately after your createWallSpan()
method:
MapBuilder.prototype.createSteppedWallSpan = function( heightIndex, spanALength, spanBLength ) { };
The createSteppedWallSpan()
method takes three parameters: a height index and two span lengths. The first span is everything to the left of the wall's step. The second span is everything to the right of the wall's step. It's also worth noting that the second span will be lower than the first. The height index you pass to the createSteppedWallSpan()
method explicitly specifies the first span's height. The second span's height is deduced from this value (its height index will be two lower than the first span's) so there's no need to pass a second height index to the method.
Since the second span's height index is two less than the first span's, we need to guard against the first span's height index being less than 2. Otherwise this would result in the second span's height index being negative, which would lead to an error at runtime. Add the following code to take care of that:
MapBuilder.prototype.createSteppedWallSpan = function( heightIndex, spanALength, spanBLength ) { if (heightIndex < 2) { heightIndex = 2; } };
Three method calls are all that remains in order to generate our stepped wall span. Add the following:
MapBuilder.prototype.createSteppedWallSpan = function( heightIndex, spanALength, spanBLength ) { if (heightIndex < 2) { heightIndex = 2; } this.createWallSpan(heightIndex, spanALength, false, true); this.addWallStep(heightIndex - 2); this.createWallSpan(heightIndex - 2, spanBLength - 1, true, false); };
The span to the left of our wall's step is constructed by calling createWallSpan()
. Note the use of the boolean values for the 3rd and 4th parameters. This ensures that the span has a front edge but no back edge. We don't want a back edge because this span will connect directly to our step.
The step itself is added to the map by calling our addWallStep()
support method. As we discussed in the previous tutorial, the step shares a y-position with the slices from the span on its right. Therefore, we subtract a value of two from our heightIndex
parameter and pass it to addWallStep()
.
Finally, another call to createWallSpan()
is made. This time it's used to generate the span connected to the right of the step. Once again the method's 3rd and 4th parameters come into play to ensure that the span has no front edge attached but does have a back edge. The front edge is not required since we want the right-hand span to join firmly with our step slice.
Save the current version of your code.
Building the Map
Good news. We're now in a position to go ahead and define our map. Remember, our entire map can be generated from the following three methods:
createGap()
createWallSpan()
createSteppedWallSpan()
I'll walk you through some of the spans we create. However, for most of the spans I'll just provide you with the code, which you should be able to read and understand by yourself.
Let's begin by adding the first wall span. Here's how we want it to look:
Our wall will have a vertical position of 160 pixels, which is an index position of 3. And it should have a length of 9 slices. Being the map's first wall span it'll sit flush against the left of the screen. That being the case, it won't require a front edge (as you can see in the screenshot above). We can omit the front edge by passing true
to our call's 3rd parameter. All this can be done with the following line:
MapBuilder.prototype.createMap = function() { this.createWallSpan(3, 9, true); };
We didn't pass a 4th argument to our createWallSpan()
method. It will therefore default to false
at runtime, which will result in a back edge being included with the wall span.
Now let's add a gap that is 1 slice long immediately after our first wall span. Add the following line:
MapBuilder.prototype.createMap = function() { this.createWallSpan(3, 9, true); this.createGap(1); };
Okay, now add two more spans, both separated by a one-slice gap. The first of these spans will have a height index of 1 and will be 30 slices long. The second will have a height index of 2 and will span 18 slices. Both walls will include a front and back edge. Since this is the default there will be no need to explicitly pass a 3rd and 4th parameter to our createWallSpan()
calls. Here's the code:
MapBuilder.prototype.createMap = function() { this.createWallSpan(3, 9, true); this.createGap(1); this.createWallSpan(1, 30); this.createGap(1); this.createWallSpan(2, 18); this.createGap(1); };
Now we'll add a stepped wall span. Its upper platform will start at a height index of 2 and will span 5 slices. The lower span will be 28 slices in length. Add the following line of code for this:
MapBuilder.prototype.createMap = function() { this.createWallSpan(3, 9, true); this.createGap(1); this.createWallSpan(1, 30); this.createGap(1); this.createWallSpan(2, 18); this.createGap(1); this.createSteppedWallSpan(2, 5, 28); };
The remainder of the map is just as easy to read and understand so I'll simply provide you with the code. Make the following additions to your createMap()
method:
MapBuilder.prototype.createMap = function() { this.createWallSpan(3, 9, true); this.createGap(1); this.createWallSpan(1, 30); this.createGap(1); this.createWallSpan(2, 18); this.createGap(1); this.createSteppedWallSpan(2, 5, 28); this.createGap(1); this.createWallSpan(1, 10); this.createGap(1); this.createWallSpan(2, 6); this.createGap(1); this.createWallSpan(1, 8); this.createGap(1); this.createWallSpan(2, 6); this.createGap(1); this.createWallSpan(1, 8); this.createGap(1) this.createWallSpan(2, 7); this.createGap(1); this.createWallSpan(1, 16); this.createGap(1); this.createWallSpan(2, 6); this.createGap(1); this.createWallSpan(1, 22); this.createGap(2); this.createWallSpan(2, 14); this.createGap(2); this.createWallSpan(3, 8); this.createGap(2); this.createSteppedWallSpan(3, 5, 12); this.createGap(3); this.createWallSpan(0, 8); this.createGap(3); this.createWallSpan(1, 50); this.createGap(20); };
Save your changes.
Removing The Old Test Map
Before we see the new map in action we'll need to remove the code for our old test map.
Open Walls.js
and remove the following line from its constructor:
function Walls() { PIXI.Container.call(this); this.pool = new WallSpritesPool(); this.createLookupTables(); this.slices = [];
this.createTestMap();this.viewportX = 0; this.viewportSliceX = 0; }
You'll also find a collection of test methods near the bottom of the class. Remove the following methods as they are no longer required:
createTestMap()
createTestWallSpan()
createTestSteppedWallSpan()
createTestGap()
Now save your changes.
Testing the Map
That's the map ready. Test your changes in Chrome and if there are any errors reported in the JavaScript console then look back over your code and fix them.
If all goes according to plan then you should see a foreground parallax layer that contains a fairly lengthy and varied game map.
Congratulations on getting this far. You should now have a firm understanding of how parallax scrolling and map generation is performed in a video game. There is however one more thing I'd like to do. If you've played games like Monster Dash and Canabalt then you'll know that the viewport's scroll speed increases as it makes its way through the level. Let's do that with our scroller too.
Gradually Increasing the Scroll Speed
We'll make changes to our Main
class to allow the viewport's speed to increase over time.
Open Main.js
and add the following constants to it:
Main.SCROLL_SPEED = 5; Main.MIN_SCROLL_SPEED = 5; Main.MAX_SCROLL_SPEED = 15; Main.SCROLL_ACCELERATION = 0.005;
The first constant, MIN_SCROLL_SPEED
, specifies the speed that the map will initially start scrolling at. The second constant, MAX_SCROLL_SPEED
, specifies the maximum speed that the viewport can reach. The final constant contains the viewport's rate of acceleration.
There will no longer be a need for our original SCROLL_SPEED
constant so remove it:
Main.SCROLL_SPEED = 5;Main.MIN_SCROLL_SPEED = 5; Main.MAX_SCROLL_SPEED = 15; Main.SCROLL_ACCELERATION = 0.005;
Now move to the constructor and add a member variable to track the viewport's current scroll speed. We'll initialise the scroller's speed by setting our member variable to the MIN_SCROLL_SPEED
constant:
function Main() { this.stage = new PIXI.Container(); this.renderer = PIXI.autoDetectRenderer( 512, 384, {view:document.getElementById("game-canvas")} ); this.scrollSpeed = Main.MIN_SCROLL_SPEED; this.loadSpriteSheet(); }
Now move down to the class' update()
method and replace the following line:
Main.prototype.update = function() {
this.scroller.moveViewportXBy(Main.SCROLL_SPEED);this.renderer.render(this.stage); requestAnimationFrame(this.update.bind(this)); };
with:
Main.prototype.update = function() { this.scroller.moveViewportXBy(this.scrollSpeed); this.renderer.render(this.stage); requestAnimationFrame(this.update.bind(this)); };
With this change our scroller's viewport is now moved on by the amount specified by our scrollSpeed
member variable rather than the SCROLL_SPEED
constant that we just removed.
Now all that's required is a few lines of code to increase the viewport's scroll speed and to ensure that it doesn't exceed our specified maximum speed. Add the following lines to accomplish this:
Main.prototype.update = function() { this.scroller.moveViewportXBy(this.scrollSpeed); this.scrollSpeed += Main.SCROLL_ACCELERATION; if (this.scrollSpeed > Main.MAX_SCROLL_SPEED) { this.scrollSpeed = Main.MAX_SCROLL_SPEED; } this.renderer.render(this.stage); requestAnimationFrame(this.update.bind(this)); };
Save and test your changes. You should now see your scroller's speed gradually increase over time. This is the exact behaviour you see in most endless runners.
Some Final Adjustments
Typically when everything is up and running you may want to make some final adjustments to get things just the way you want them. I personally wasn't happy with the scroll speed of my far and mid layers. Let's make a slight change to both layers to slow them down a little.
Open Mid.js
and change the following line form:
Mid.DELTA_X = 0.64;
to this:
Mid.DELTA_X = 0.32;
Save your change then open Far.js
. Make a similar adjustment by changing:
Far.DELTA_X = 0.128;
to:
Far.DELTA_X = 0.064;
Save the file and test everything within Chrome again.
Where To Go From Here
An incredible amount of ground has been covered over the course of all four tutorials, however there's definitely still room for improvement. Many of the things I'm going to quickly talk about here are outside the scope of this tutorial but you certainly know enough now to consider tackling them.
The map we generate is completely predetermined making it identical every time our project is run. Randomising the map wouldn't be that difficult. Simply alter your MapBuilder
class' createMap()
method so that the length and height of spans is randomly chosen. You could also throw a random number when deciding whether to place a wall span or stepped wall span onto the map. Perhaps there could be a 1 in 30 chance of a stepped wall span being added to the map next as opposed to a standard wall span.
Many of you may have noticed that the scroller we've built isn't actually endless. The levels in Monster Dash, which this tutorial is based upon, are all actually of a finite length too. With a little effort you could dynamically add more spans to the map at runtime. For example, you could generate a few more spans every second, or after the viewport has scrolled a certain number of slices. This would create scrolling behaviour similar to Canabalt, which is actually a true endless runner.
You could consider adding more parallax layers. Monster Dash for example has an additional layer in front of the scrolling map. More background layers might also be appropriate for other game projects.
Finally, some games aren't restricted to a single axis. There's nothing stopping you adding vertical scrolling to your game map as well as horizontal scrolling.
Conclusion
Whether you're a beginner or experienced programmer I hope you got something from this series of tutorials. In combination, JavaScript and pixi.js are excellent options for writing exciting games and interactive content for the web.
Pixi is an incredibly flexible and powerful 2D renderer that has gathered a huge amount of momentum since its initial release. While we've only scratched the surface you should be in an ideal position to take your existing knowledge of the Pixi API and go on to master it. It's actively updated with new and exciting features being pushed out by its author Mat Groves on a regular basis.
In addition to Pixi we've covered some important games programming concepts including parallax scrolling, how to work with sprite sheets, object pooling, and map generation. I've also ensured that we've performed significant refactoring of our code as we've worked through the tutorials. This has lead to code that's hopefully cleaner, maintainable and more readable. If you aren't in the habit of refactoring as you develop then I strongly suggest you give it a try.
Thanks for hanging in there and congratulations on making it to the end. If you have any questions then feel free to get in touch.
I’m a junior developer, decided to give this tutorial a go after hearing about Pixi from a friend.
Awesome introduction to the framework – well paced, thoroughly explained, and gives a great sense of accomplishment (especially to someone who is new to domain).
Hope to see more of these tutorials in the future.
Thanks!
Superb tutorial. You really cover a lot of ground and present all of it very clearly. Thank you so much for this!
Thanks Tom and Chris for the positive feedback. Hope you guys go on to make some awesome stuff with Pixi.
Nice tutorial, really enjoyed it. Although in the first part of the tutorials you mentions the setup function which was not made yet which i found confusing but i figured it out.
Any tips on what tutorial to follow to get familiar with collisions and a simple character controller, that I can hook up with this game?
Thanks for spotting the problem with the setup function. I’ll try and find the time update the tutorial. Regarding collision detection, I’d suggest you take a look at Box2D. You can find some tutorials here: http://creativejs.com/2011/09/box2d-javascript-tutorial-series-by-seth-ladd/
Great tutorial !
I’ve learned some nice tips from you and I was personnaly intersted in the basics of PIXI and I was fully satisfied with that.
Thanks again and good job.
It’s a good tutorial,thank you.
I have a question,Walls’ class has a function named addNewSlices, this.addChild(slice.sprite); within it;I don’t know why this.addChild can do something?the keyword this represent wall’s instance,but why not the stage’s addChid?
Hi,
If you take a look at the
Walls
class’ constructor you’ll see that it inherits from PIXI’sDisplayObjectContainer
class:function Walls() {
PIXI.DisplayObjectContainer.call(this);
By doing so, the
Walls
class exhibits the behaviour of PIXI’s other display object containers such asSprite
andMovieClip
. TheWalls
class can therefore be added to the display list, and also have other display objects added to itself as children.In other words, we are free to add our slice sprites directly within our
Walls
instance using its inheritedaddChild()
method.Hope that clarifies things. Thanks.
Thank you for your answer
Very, very nice tutorial! Thank you very much!
If I do understand correctly, method Walls.prototype.removeOldSlices supports only one direction of scrolling from left to right. All code before that works fine for any scrolling direction.
Yeah that’s correct. The removeOldSlices() method only supports scrolling from right to left. It’s probably not too much work though to get it working for left to right scrolling. Thanks.
Excellent! Thanks for sharing Leo!
Hey, I first want to say thank you for the tutorial, it has really helped me!
I wanted to ask about the “Endless” option… What would be the best way to go about this? I thought I would just have to add a new set of slices everytime it hit the update function, but then I quickly realized this would probably not be a good idea since the slices array will quickly end up with thousands of slices! If you could point me in the direction that you would go about it, I would love to know. Thank you for your great work.
Hi Alex. Thanks for getting in touch.
Yeah, adding slices to the array over time will only get you so far but if you want a true endless scrolling experience then you’re best option is to dynamically remove slices from the array once they are no longer needed. You can do this within the Walls class’ removeOldSlices() method. You’ll need to track the number of removed slices using a member variable (this.removedSlicesCount or something like that) then use that member variable as an offset when indexing into your this.slices member variable. Here’s an example:
var slice = this.slices[i – this.removedSlicesCount];
As you remove slices, the this.slices array will obviously shrink in size. So you’ll need to keep an eye on it, and once it drops below a certain size (for example, below 100 slices), start to top the array up again with new slices. An ideal place to do that would be within the Scroller class’ setViewportX() method. Here’s an example:
Scroller.prototype.setViewportX = function(viewportX) {
:
:
if (this.front.isLow()) // If less than 100 slices left
{
// Generate new slices in here via this.mapBuilder
}
}
Hope that was enough to get you going. Just ask if you need more clarification.
Thanks.
Thanks for the quick reply!
Ahhh I understand now. I was trying to delete the slices once they got to a certain point but was never updating the accessing of the slices array.
I Ill go ahead and give this a try. Thanks for all your help!
Thought I would give an update to how I solved this just in case anyone else wants to know
In Walls.js, I added a member this.removedSlices
In the Walls.removeOldSlices function, you need to actually remove the slice from the array (this.slices = this.slices.slice(1);) as well as increment the member variable (this.removedSlicesCount++;)
Like Christopher mentioned above, you need to then change the way you are accessing your array… var slice = this.slices[i – this.removedSlicesCount]; Make sure to change it in the addNewSlices function as well.
I then went into the Scroller.js class and added a check inside my setViewportX function (if (this.front.slicesAreLow()) {
this.mapBuilder.addAndBuildRandomSequence();
})
I added a function in the Walls class called slicesAreLow which checks if the slices are low (Walls.prototype.slicesAreLow = function() {
return this.slices.length < 100;
};)
I also added a few other functions inside my MapBuilder for generating random slices and whatnot
MapBuilder.prototype.addAndBuildRandomSequence = function() {
var rand = Math.floor((Math.random() * 4));
switch (rand) {
case 0:
this.sequenceOne();
break;
case 1:
this.sequenceTwo();
break;
case 2:
this.sequenceThree();
break;
case 3:
this.sequenceFour();
break;
default:
break;
}
};
ill post an example sequenceOne function
MapBuilder.prototype.sequenceOne = function() {
this.createWallSpan(1, 20);
this.createGap(1);
};
Hi Alex. That’s great that you got it working and thanks for posting your solution.
I’ve got one quick suggestion for you. Try replacing the line:
this.slices = this.slices.slice(1);
with simply:
this.slices.shift();
The shift() method works directly upon the array, unlike slice() which returns a shallow copy of your array. Therefore shift() should perform significantly faster than slice() and also result in less garbage collection over the course of your scroller’s lifetime.
Thanks.
thanks for the suggestion! Learn something new everyday.
-Alex
The backgrounds are tearing, seems to be a problem with the latest release of pixi. As the live demo works but the github project does not.