In this level data, we use 8 to denote a pickup (1 and 0 represent walls and walkable tiles respectively, as before).

It's important to understand that 8 actually denotes two tiles, not just one: it means we need to first place a walkable grass tile and then place a pickup on top. This means that every pickup will always be on a grass tile. If we want it to be on a walkable brick tile, then we'll need another tile denoted by another number, say 9, that represents "pickup on brick tile".

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 * 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 do this, we can use another array with the pickup data alone, and use this to place pickup tiles atop the level layout data:

In the function isPickup(tile coordinate), we check whether the pickup data array value at the given coordinate is a pickup tile or not. The number in the pickup array at that tile coordinate denotes the type of pickup.

We check for collisions before moving the character but 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 pickup 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 pickup array has been changed as the player picked up items. The solution is to use a duplicate array for pickups while in play and to keep the original pickup array intact - for instance, we use pickupsArray[] and pickupsLive[], clone the latter from the former at the start of the level, and only change pickupsLive[] during play.

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 pickup array data to 0 the first time we detect a pickup, all subsequent isPickup(tile) checks will returns false for that tile.

2. Trigger Tiles

As the name suggests, trigger tiles cause something to happen when the player steps on them or presses a key while stepping 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 Space bar, they'll proceed to the next level.

To change levels, all we need to do is swap the current level data array with that of the new level, and set the new tile 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 an array to store trigger values. This is inefficient and you should consider other data structures for this purpose, but let's keep this simple for the sake of the tutorial. Let the new level arrays be as below (7 denotes a door):

The function isTrigger() checks whether the trigger data array value at the given coordinate is greater than zero. If so, our code passes that value to doRelevantAction(), which decides which function to call next. For our purposes, we'll use the simple rule that if the value lies between 1 and 10, it's a door, and so this function will be called:

I have made the trigger be activated when Space 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.

3. Path Finding

Path finding and path following is a fairly complicated process. There are various approaches using different algorithms for finding the path between two points, but our level data 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.

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 neighboring 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 neighboring nodes, nodes that have already been visited nodes 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 path finding fails. We usually end up finding multiple paths between the two nodes, in which case we take the one with the least number of nodes.

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 is as the destination. Once we get to the destination node, we check where there are any more nodes in the node array and, if so, set the next node as 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 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.

You may have noticed that I removed the collision check logic; it's no longer needed as we cannot manually move our character using the keyboard. However, 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.

Also, if you explore the above demo, you may notice that our draw logic gets disrupted when the hero is moving diagonally close to a wall tile. This is an extreme case where, for one frame, our hero seems to be inside the wall tile. This happens because we have disabled the collision check. One workaround is to use a pathfinding algorithm that ignores the diagonal solutions. (Almost all path finding algorithms have options to enable or disable diagonal walk solutions.)

4. Projectiles

A projectile is something that moves in a particular direction with a particular speed, like a bullet, a magic spell, a ball, and so on.

Everything about the projectile is 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. There are no complicated conversions involved. If a ball is 10 pixels above ground in Cartesian coordinates, it is 10 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 around in our walled grassland. We'll ignore damping effects (and so make the bouncing continue endlessly), and for a touch of realism we'll add a shadow to the ball. We move the shadow just like we move the hero character (i.e. without using a height value), but for the ball we must add the height value to the isometric Y value. The 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 us represent the height of the ball by a variable zValue. Imagine that, to begin with, the ball is ten pixels high, so zValue = 10. We'll use two more variables: incrementValue, which starts at 0, and gravity, which has a value of 1.

Each frame, we add incrementValue to zValue, and subtract gravity from incrementValue. 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.

Do understand that the role played by the shadow is a very important one which adds to the realism of this illusion. In the above example, I have added half the ball's height to the ball's y-position, so that it bounces at the right position with respect to the shadow.

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!

5. Isometric Scrolling

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

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 player remains the same with respect to the screen rectangle, commonly at the screen center. All we need, to implement scrolling, is to track the corner point of the inner rectangle:

This corner point, which is in Cartesian coordinates (in the image we can only show the isometric values), 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 only draw it if the tile's new position falls within the screen. 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 the tile only if the new isometric draw position falls within the screen.

The draw logic only changes in two lines, where we determine the Cartesian coordinates of each tile. We just pass the corner point to the original point which actually combines points 1, 2 and 3 above:

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 in 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 purposefully chosen the easiest ones. They may not fulfill all scenarios which you may encounter, but the knowledge gained can be used to build upon these concepts to create much complicated solutions.