Advertisement
  1. Game Development
  2. Game Development
Gamedevelopment

Updated Primer for Creating Isometric Worlds, Part 2

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Primer for Creating Isometric Worlds.
An Updated Primer for Creating Isometric Worlds, Part 1
Final product image
What You'll Be Creating

In this final part of the tutorial series, we'll build on the first tutorial and learn about implementing pickups, triggers, level swapping, pathfinding, path following, level scrolling, isometric height, and isometric projectiles.

1. Pickups

Pickups are items that can be collected within the level, normally by simply walking over them—for example, coins, gems, cash, ammo, etc.

Pickup data can be accommodated right into our level data as below:

In this level data, we use 8 to denote a pickup on a grass tile (1 and 0 represent walls and walkable tiles respectively, as before). This could be a single tile image with a grass tile overlaid with the pickup image. Going by this logic, we will need two different tile states for every tile which has a pickup, i.e. one with pickup and one without to be shown after the pickup gets collected.

Typical isometric art will have multiple walkable tiles—suppose we have 30. The above approach means that if we have N pickups, we will need N x 30 tiles in addition to the 30 original tiles, as each tile will need to have one version with pickups and one without. This is not very efficient; instead, we should try to dynamically create these combinations. 

To solve this, we could use the same method we used to place the hero in the first tutorial. Whenever we come across a pickup tile, we will place a grass tile first and then place the pickup on top of the grass tile. This way, we just need N pickup tiles in addition to 30 walkable tiles, but we would need number values to represent each combination in the level data. To solve the need for N x 30 representation values, we can keep a separate pickupArray to exclusively store the pickup data apart from the levelData. The completed level with the pickup is shown below:

Isometric level with coin pickup

For our example, I am keeping things simple and not using an additional array for pickups.

Picking Up Pickups

Detecting pickups is done in the same way as detecting collision tiles, but after moving the character.

In the function onPickupTile(), we check whether the levelData array value at the heroMapTile coordinate is a pickup tile or not. The number in the levelData array at that tile coordinate denotes the type of pickup. We check for collisions before moving the character but need to check for pickups afterwards, because in the case of collisions the character should not occupy the spot if it is already occupied by the collision tile, but in case of pickups the character is free to move over it.

Another thing to note is that the collision data usually never changes, but the pickup data changes whenever we pick up an item. (This usually just involves changing the value in the levelData array from, say, 8 to 0.)

This leads to a problem: what happens when we need to restart the level, and thus reset all pickups back to their original positions? We do not have the information to do this, as the levelData array has been changed as the player picked up items. The solution is to use a duplicate array for the level while in play and to keep the original levelData array intact. For instance, we use levelData and levelDataLive[], clone the latter from the former at the start of the level, and only change levelDataLive[] during play.

For the example, I am spawning a random pickup on a vacant grass tile after each pickup and incrementing the pickupCount. The pickupItem function looks like this.

You should notice that we check for pickups whenever the character is on that tile. This can happen multiple times within a second (we check only when the user moves, but we may go round and round within a tile), but the above logic won't fail; since we set the levelData array data to 0 the first time we detect a pickup, all subsequent onPickupTile() checks will return false for that tile. Check out the interactive example below:

2. Trigger Tiles

As the name suggests, trigger tiles cause something to happen when the player steps on them or presses a key when on them. They might teleport the player to a different location, open a gate, or spawn an enemy, to give a few examples. In a sense, pickups are just a special form of trigger tiles: when the player steps on a tile containing a coin, the coin disappears and their coin counter increases.

Let's look at how we could implement a door that takes the player to a different level. The tile next to the door will be a trigger tile; when the player presses the x key, they'll proceed to the next level.

Isometric level with doors  trigger tiles

To change levels, all we need to do is swap the current levelData array with that of the new level, and set the new heroMapTile position and direction for the hero character. Suppose there are two levels with doors to allow passing between them. Since the ground tile next to the door will be the trigger tile in both levels, we can use this as the new position for the character when they appear in the level.

The implementation logic here is the same as for pickups, and again we use the levelData array to store trigger values. For our example, 2 denotes a door tile, and the value beside it is the trigger. I have used 101 and 102 with the basic convention that any tile with a value greater than 100 is a trigger tile and the value minus 100 can be the level which it leads to:

The code for checking for a trigger event is shown below:

The function triggerListener() checks whether the trigger data array value at the given coordinate is greater than 100. If so, we find which level we need to switch to by subtracting 100 from the tile value. The function finds the trigger tile in the new levelData, which will be the spawn position for our hero. I have made the trigger to be activated when x is released; if we just listen for the key being pressed then we end up in a loop where we swap between levels as long as the key is held down, since the character always spawns in the new level on top of a trigger tile.

Here is a working demo. Try picking up items by walking over them and swapping levels by standing next to doors and hitting x.

3. Projectiles

projectile is something that moves in a particular direction with a particular speed, like a bullet, a magic spell, a ball, etc. Everything about the projectile is the same as the hero character, apart from the height: rather than rolling along the ground, projectiles often float above it at a certain height. A bullet will travel above the waist level of the character, and even a ball may need to bounce around.

One interesting thing to note is that isometric height is the same as height in a 2D side view, although smaller in value. There are no complicated conversions involved. If a ball is 10 pixels above the ground in Cartesian coordinates, it could be 10 or 6 pixels above the ground in isometric coordinates. (In our case, the relevant axis is the y-axis.)

Let's try to implement a ball bouncing in our walled grassland. As a touch of realism, we'll add a shadow for the ball. All we need to do is to add the bounce height value to the isometric Y value of our ball. The jump height value will change from frame to frame depending on the gravity, and once the ball hits the ground we'll flip the current velocity along the y-axis.

Before we tackle bouncing in an isometric system, we'll see how we can implement it in a 2D Cartesian system. Let's represent the jump power of the ball with a variable zValue. Imagine that, to begin with, the ball has a jump power of 100, so zValue = 100

We'll use two more variables: incrementValue, which starts at 0, and gravity, which has a value of -1. Each frame, we subtract incrementValue from zValue, and subtract gravity from incrementValue in order to create a dampening effect. When zValue reaches 0, it means the ball has reached the ground; at this point, we flip the sign of incrementValue by multiplying it by -1, turning it into a positive number. This means that the ball will move upwards from the next frame, thus bouncing.

Here's how that looks in code:

The code remains the same for the isometric view as well, with the slight difference that you can use a lower value for zValue to start with. See below how the zValue is added to the isometric y value of the ball while rendering.

Check out the interactive example below:

Do understand that the role played by the shadow is a very important one which adds to the realism of this illusion. Also, note that we're now using the two screen coordinates (x and y) to represent three dimensions in isometric coordinates—the y-axis in screen coordinates is also the z-axis in isometric coordinates. This can be confusing!

4. Finding and Following a Path

Pathfinding and path following are fairly complicated processes. There are various approaches using different algorithms for finding the path between two points, but as our levelData is a 2D array, things are easier than they might otherwise be. We have well-defined and unique nodes which the player can occupy, and we can easily check whether they are walkable.

Related Posts

A detailed overview of pathfinding algorithms is outside of the scope of this article, but I will try to explain the most common way it works: the shortest path algorithm, of which A* and Dijkstra's algorithms are famous implementations.

We aim to find nodes connecting a starting node and an ending node. From the starting node, we visit all eight neighbouring nodes and mark them all as visited; this core process is repeated for each newly visited node, recursively. 

Each thread tracks the nodes visited. When jumping to neighbouring nodes, nodes that have already been visited are skipped (the recursion stops); otherwise, the process continues until we reach the ending node, where the recursion ends and the full path followed is returned as a node array. Sometimes the end node is never reached, in which case the pathfinding fails. We usually end up finding multiple paths between the two nodes, in which case we take the one with the smallest number of nodes.

Pathfinding

It is unwise to reinvent the wheel when it comes to well-defined algorithms, so we would use existing solutions for our path-finding purposes. To use Phaser, we need a JavaScript solution, and the one I have chosen is EasyStarJS. We initialise the path-finding engine as below.

As our levelData has only 0 and 1, we can directly pass it in as the node array. We set the value of 0 as the walkable node. We enable diagonal walking capability but disable this when walking close to the corners of non-walkable tiles. 

This is because, if enabled, the hero may cut into the non-walkable tile while doing a diagonal walk. In such a case, our collision detection will not allow the hero to pass through. Also, please be advised that in the example I have completely removed the collision detection as that is no longer necessary for an AI-based walk example. 

We will detect the tap on any free tile inside the level and calculate the path using the findPath function. The callback method plotAndMove receives the node array of the resulting path. We mark the minimap with the newly found path.

Isometric level with the newly found path highlighted in minimap

Path Following

Once we have the path as a node array, we need to make the character follow it.

Say we want to make the character walk to a tile that we click on. We first need to look for a path between the node that the character currently occupies and the node where we clicked. If a successful path is found, then we need to move the character to the first node in the node array by setting it as the destination. Once we get to the destination node, we check whether there are any more nodes in the node array and, if so, set the next node as the destination—and so on until we reach the final node.

We will also change the direction of the player based on the current node and the new destination node each time we reach a node. Between nodes, we just walk in the required direction until we reach the destination node. This is a very simple AI, and in the example this is done in the method aiWalk shown partially below.

We do need to filter out valid click points by determining whether we've clicked within the walkable area, rather than a wall tile or other non-walkable tile.

Another interesting point for coding the AI: we do not want the character to turn to face the next tile in the node array as soon as he has arrived in the current one, as such an immediate turn results in our character walking on the borders of tiles. Instead, we should wait until the character is a few steps inside the tile before we look for the next destination. It is also better to manually place the hero in the middle of the current tile just before we turn, to make it all feel perfect.

Check out the working demo below:

5. Isometric Scrolling

When the level area is much larger than the available screen area, we will need to make it scroll.

Isometric level with 12x12 visible area

The visible screen area can be considered as a smaller rectangle within the larger rectangle of the complete level area. Scrolling is, essentially, just moving the inner rectangle inside the larger one. Usually, when such scrolling happens, the position of the hero remains the same with respect to the screen rectangle, commonly at the screen center. Interestingly, all we need to implement scrolling is to track the corner point of the inner rectangle.

This corner point, which we represent in Cartesian coordinates, will fall within a tile in the level data. For scrolling, we increment the x- and y-position of the corner point in Cartesian coordinates. Now we can convert this point to isometric coordinates and use it to draw the screen. 

The newly converted values, in isometric space, need to be the corner of our screen too, which means they are the new (0, 0). So, while parsing and drawing the level data, we subtract this value from the isometric position of each tile, and can determine if the tile's new position falls within the screen. 

Alternatively, we can decide we are going to draw only an X x Y isometric tile grid on screen to make the drawing loop efficient for larger levels. 

We can express this in steps as so:

  • Update Cartesian corner point's x- and y-coordinates.
  • Convert this to isometric space.
  • Subtract this value from the isometric draw position of each tile.
  • Draw only a limited predefined number of tiles on screen starting from this new corner.
  • Optional: Draw the tile only if the new isometric draw position falls within the screen.

Please note that the corner point is incremented in the opposite direction to the hero's position update as he moves. This makes sure that the hero stays where he is with respect to the screen. Check out this example (use arrows to scroll, tap to increase the visible grid).

A couple of notes:

  • While scrolling, we may need to draw additional tiles at the screen borders, or else we may see tiles disappearing and appearing at the screen extremes.
  • If you have tiles that take up more than one space, then you will need to draw more tiles at the borders. For example, if the largest tile in the whole set measures X by Y, then you will need to draw X more tiles to the left and right and Y more tiles to the top and bottom. This makes sure that the corners of the bigger tile will still be visible when scrolling in or out of the screen.
  • We still need to make sure that we don't have blank areas on the screen while we are drawing near the borders of the level.
  • The level should only scroll until the most extreme tile gets drawn at the corresponding screen extreme—after this, the character should continue moving in screen space without the level scrolling. For this, we will need to track all four corners of the inner screen rectangle, and throttle the scrolling and player movement logic accordingly. Are you up for the challenge to try implementing that for yourself?

Conclusion

This series is particularly aimed at beginners trying to explore isometric game worlds. Many of the concepts explained have alternate approaches which are a bit more complicated, and I have deliberately chosen the easiest ones. 

They may not fulfil most of the scenarios you may encounter, but the knowledge gained can be used to build upon these concepts to create more complicated solutions. For example, the simple depth sorting implemented will break when we have multi-storied levels and platform tiles moving from one story to the other. 

But that is a tutorial for another time. 

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.