Shared posts

08 Sep 16:20

How to Fund Your Games By Creating and Selling Game Assets

by Oussama Bouanani

I've been selling assets on the Unity Asset Store for two years, and I use a portion of the earnings to fund my current game's marketing budget. In this tutorial, I'll show you how you can do the same!

What Are Game Assets?

Game assets include everything that can go into a game, including 3D models, sprites, sound effects, music, code snippets and modules, and even complete projects that can be used by a game engine. Here's a list of examples:

2D/3D Design:

  • Characters
  • Objects
  • Environments
  • Vehicles

GUI:

  • HUD
  • Icons

Scripting:

  • AI
  • Special effects
  • Networking
  • Physics

Audio:

  • Background music
  • Sound effects

This means that, if you make games, you can sell some of your work as assets—whether you are a coder, artist, game designer, or music composer. Some people make a little extra money doing so, some are able to fund their games entirely, and others make a full living just selling assets.

Where Can You Sell Assets?

These are some of the most popular sites that allows you to sell your assets:

  • GameDev Market: A marketplace that includes 2D sprites, 3D models, GUI, music, and sound effects, along with a community forums that connects asset publishers with game developers allowing you to know what most of the buyers are looking 
  • TurboSquid: A marketplace specifically for 3D models. Includes high quality and professional models. 
  • Game Art 2D: A marketplace that includes 2D sprties, tilesets and GUI elements.
  • Unity Asset Store: The official Unity marketplace including all types of assets: 2D/3D models, GUI elements, sound effects, music, and everything related to Unity, such as scripting assets, shaders, animations, particle effects, and even complete Unity projects.
  • Unreal Engine Marketplace: The official Unreal Engine marketplace, including all types of assets along with ones related to Unreal Engine only, much like the Unity Asset Store.

How to Make an Asset Pack That Sells Well

First of all, if you are new to the game development world and you're still in the process of learning, don't jump to selling assets. Keep learning and take your time, because whatever you create while still learning is likely already to be available, and usually for free.

Second, you have to be original, and look for something that hasn't been created. Maybe you have created something new and unique while developing one of your games; why not sell it as an asset? Alternatively, look for something that has been done before but isn't that good, or is out-dated, or abandoned by the developer, and offer something better to attract buyers.

If You Are a Designer or Artist

You might have already created a lot of  2D or 3D designs, but make no mistake: that doesn't mean you should just package whatever you have created and try to sell it. First, you have to take a look at the market, which is already crowded with sprites and models of all types—characters, environments, vehicles—many of them available for free. Don't create some trees or zombie models and expect them to sell. 

For example, suppose you came up with an idea for a cool 2D character. Okay, let's see how you can sell it. The key here is variation. Customers are always looking for different types of the same thing; to attract buyers, you need to include few animations for this character: walking, running, jumping, giving damage, taking damage, crouching, and so on. You need at least to include the basic animations that are commonly used, but adding extra ones will increase your chances to sell this asset to more people. 

But that's not really enough. Why not create a pack of these characters, with same style and same animations for each one. Maybe three or four characters, or one character with variations: different clothes and accessories. If you were a customer looking to add characters to your game, you'd definitely want them to be the same style with all the animations you need, right? 

Another example would be an urban environment pack. If you just created a few building models and are ready to sell them, then why not include a few textures for each building which will allow the customer to create a rich environment? You can also include some extra models, like street signs and roads. Make a full, high-quality city construction pack; it will take some time, but in the end you'll be surprised by the number of customers you'll get. Customers are always looking for assets that include different types of the same thing.

Below is a screenshot from a popular Unity 3D art asset called Village Interior Kit, developed by 3DForge. The package contains 2288 meshes, 2626 prefabs, and 81 particle effect prefabs, and only costs $60!

If You Are a Composer

Game developers can get high quality sound effects and music loops for low prices and even for free. Therefore, you need to work on full audio assets that include a variation of sound effects for every action. 

For example, if creating a set of audio assets for a sci-fi game, make sure that you include all the sound effects that are likely to be needed, like explosions, weapons, collisions, and even opening and closing doors. 

As for music loops, an asset including 10 to 15 loops of the same genre is ideal for your customers, because it will allow them to use your audio assets for their entire game.

If You Are a Coder

As a coder, you are tied to the tools and platform you are working on, like Unity or Unreal, and these already have a lot of assets in their respective stores. So, unless you have some incredibly awesome ideas for a new scripting asset, don't create it and expect a lot of sales. 

Many scripts that are available on the Unity Asset Store aren't available on the Unreal Engine marketplace, and vice-versa, so it might be a good idea to look at what's popular on one of the stores and create something similar for the other platform, if it doesn't already exist. 

You should also look for something that solves a problem that many new users face in the game engine you're working with. Below is a screenshot of S-Quest, one of my assets on the Unity Asset Store. This asset allows you to create and customize quests for your games through code, which saves the time for many developers and allows them to add quests, quest logs, an objectives bar, and a player experience manager to their games easily.

You should also create assets that will work dynamically. For example, if you're working on an enemy AI asset, make sure that you include most possible actions that an enemy can perform, like wander, patrol, defend, hide. and attack. And don't tie your assets to one game type; in this case your enemy AI asset should work for both RTS and FPS games, for example.
Finally, make sure that your code is always clean and fully commented, so that your customers can figure out the job of each line you wrote in order to apply their modifications and customize your asset to their needs.

How to Set the Right Price for Your Packs

Now that you've finished making your asset—or even if you're still working on it but have a clear idea on what it will include exactly—it's time to give it a price.

Creating assets requires a lot of time and hard work, but they always cost less than their real value. For example, suppose you've just finished a city construction kit with high quality 3D models for roads, buildings, and street signs, and some additional textures. I'd say it shouldn't go above $80, maybe $100. (I came to this number by checking similar and popular assets in the market.) Yes, I know that's not the real value of such work and you're probably thinking that you can get its real value by doing freelance. But in freelance, you're doing all that work for one customer, who'll pay you once, and you can never sell that work again. If you're releasing it as an asset, however, it can generate more revenue in the long term because that asset will be available to unlimited potential buyers who will be attracted by its low price relative to its high quality content.

The price you pick also depends on other similar assets that are available in the market; I recommend always going a little bit lower with the price of your assets than the other assets that offer similar content. 

The most successful asset I released is S-Inventory, which includes an inventory system, an equipment system, a vendor system, a crafting system, a containers system, and a skill bar system for Unity, all in one. It only costs $15. If the price were higher, I don't think it would sell well and compete with similar assets in the store.

How to Provide Good Customer Support

As an asset developer, you must be active most of the time to provide support for your customers. (This is especially true if you're selling scripting assets or complete projects for a specific game engine, because then you'll be dealing with a lot of users who are new to these engines and will ask not only how to customize your assets to their needs, but also often questions that are not really related to your assets. If you can answer, they will really appreciate it.) 

I'd say you should always answer your customers in less than 24 hours. Maybe assign one hour a day to handling these support requests. Use e-mails and even Skype calls or Teamviewer sessions to help your customers, and inform your customers if you are going inactive for some time so that they don't assume you've abandoned your assets.

If your asset has a bug or a problem, a customer will quickly reach out to you and report that issue. Don't start by fixing the problem, as this might take some time (particularly if it's a serious problem), and don't leave the customer waiting for an answer. Instead, instantly answer them, thank them for finding the bug, and let them know that you're looking to find a solution, and how much time you expect it will it take you to fix it (hours, days or even weeks). Replying quickly to your customers will make you a responsible developer and will earn you a good reputation—assuming you follow up with the fixes!

Some customers will be loaded with suggestions requesting changes and improvements in your assets. You always have to consider their suggestions carefully. If you believe that ithe request would improve your asset for the majority of its users, then it's worth adding it. If you believe that it just serves that one customer, however, then you could simply deliver this new feature to them (if it's not too much work) without publishing it in your asset.

Pushing regular updates is also another sign that proves you're an active developer, which helps make customers comfortable with buying your assets, since they know that you'll be there to help them if they face problems using them.

What to Do About Reviews

Most people rely on reviews when they make a decision of whether or not to buy any asset. Even if your asset is great and satisfies all of its buyers, don't expect them all to write a good review, or even to rate it; only a few of them will likely take the time to do that. 

There is a way to ask your customers for reviews. When you finish answering their questions, fixing a problem, or handling a request for them regarding your asset, they will generally be very thankful and overwhelmed. Just ask them to consider rating and reviewing your asset at that time. Aside from complimenting your asset and all its features, they will also likely point out the great customer support that they you provided. That way, you make your customers' lives easier and, in return, they help you to get more sales.

Note that, if your asset doesn't offer what you promised in its description or screenshots, then every single customer will take a part in taking it down by writing long, bad reviews warning other users not to buy it. At that point, it might be too late to even update the asset and fix it. 

Conclusion

To sum up: creating successful game assets which end up being a decent source of revenue requires you to be original, offer variation, price for the market, and take responsibility when dealing with customers.

08 Sep 16:19

How to Incorporate Satisfying Death Mechanics Into Your Game

by Matthias Zarzecki

Games that do not allow you to die (or fail, for that matter) lack heft. When failure is impossible, what purpose is there in defying it? Success loses its meaning when there's no dread.

But player deaths don't have to end in frustration or the player having to replay long stretches of the game. Death mechanics can be integrated into the story and the gameplay, where they become part of the experience.

In this article we'll take a look at different ways to deal with player death and failure, both good and bad. Some games manage to do both at the same time!

The best ways to deal with player death are fully integrated into the narrative and gameplay; these are the ones we should use as inspiration in our titles.

(Note that we'll focus on non-competitive games. In multiplayer games, like Team Fortress 2, other rules apply. Also note that games can get away with no death if death or failure isn't expected. Point-and-click adventures like Monkey Island do not need to have death mechanics as there is no expectation of them.)

Narrative Deaths

Good: Keep Narrative Death and Gameplay Death Separate

Spoiler: In Final Fantasy VII a main character dies halfway through the game. Which is weird, because you and your teammates die all the time, but use resurrection spells to bring people back to life again.

Final Fantasy VII HD
Final Fantasy VII HD

The difference between the two is that one death exists in the narrative, while the other deaths exist in the gameplay. The game deals with that by keeping both things separate and never referencing each other. Nobody says, "Hey, couldn't you use Raise [the resurrection spell] on [Character] and save all that heartache?" as that would break the barrier between gameplay and narrative.

Bad: Mix Narrative Death and Gameplay Death

Borderlands 2 fails at this and mixes the two.

The game has a system of New U Stations. These work both as checkpoints from which to restart the game and as resurrection points. When you die, a New U is created: basically a clone of your previous self. You lose some money, and the voice of the machine quips that you should avoid jumping into lava and states how much money you have made for the company.

Borderlands 2 resurrection
A character from Borderlands 2, being reconstituted at a New U station.

This could work like in Final Fantasy VII, a system separate from the narrative, but it is not. The New U stations belong to Hyperion, one of the companies in the game. After a while you may start to wonder why you can't just use it to bring major dead story characters back to life, or why the enemies don't just use them as well.

Borderlands 2 reconsitution
The view in Borderlands 2 during reconstitution, re-entering the game, and travelling, which might imply you "die" and get cloned all the time, even for minor conveniences like travel.

This is especially egregious when Handsome Jack, the main antagonist who is trying to kill you, gives you a mission to kill yourself. You can do it, and you get insta-resurrected... Then why is he trying to kill you?!

Lead Writer, Anthony Burch, laments this as one of the major faults in the story. Keeping player death in the gameplay and out of the narrative should be done in a strict manner to prevent these weird overlaps.

Giving the Player Another Chance

Bad: Magically Save the Player at the Last Second

In the 2008 reboot of Prince of Persia, you can't actually die. Ever.

When you're about to fall to your death, you are saved at the last second and deposited back where you started. When your health is about to run out in battle, you are saved, you get your health back, and the enemies regain their health too, which is another break in the logic of the universe.

Prince of Persia being saved by Elika
Prince of Persia (2008). Before you die, your companion Elika saves you. The teamwork aspect itself is fun, but not when used to remove stakes from the game.

You end back where you started, with no progress made.

Good: Let the Player Rewind Time

One of the best features in Prince of Persia: The Sands of Time is the use of the titular sands to affect gameplay. The sand shows up in several story segments, but you can also use it during gameplay!

Prince of Persia The Forgotten Sands
Prince of Persia: The Forgotten Sands. All games in the Sands of Time series feature time reversal.

When you die, you just rewind time to a point where you are not dead. Instead of quick-loading, you stay in the game, and the gameplay fully supports this.

In a time-travel game, this feature practically comes with the gameplay. Braid allows you to rewind after death back to when you were alive, and even further to the point where you began the level.

Braid reversing time
Braid

The racing game GRID also allows this, and is maybe the only game in that genre to do so (apart from its sequels). Races can be long, and failing one due to a slip-up or a freak crash can be very frustrating, especially since racing games usually do not have in-race save systems. In GRID, however, you get a few rewinds per race, which you can use to save yourself from a major crash. The limited nature of this feature also keeps the player from abusing this system for minor slip-ups.

GRID replay controls
GRID. After a fatal crash you see these replay controls. Press the button on the right to flashback and get back into the race at that point.

Good: Use an Unreliable Narrator

Prince of Persia: The Sands of Time has another fun system for dealing with player death. When you actually do run out of both health and sand, the hero says, "Hang on a second. This isn't how it happened!" before you need to reload.

What's happening is that the entire story is actually told by the character after it happened. The game starts right at the end, and everything is told in flashbacks.

Prince of Persia The Sands of Time starting scene
Prince of Persia: The Sands of Time. This is the very start of the game, and everything after that is told in flashbacks.

This feels as if the prince might actually have made a honest mistake. Realizing you have an unreliable narrator is fun and softens the impact of being pulled out of the game to reload it.

Call of Juarez: Gunslinger centers its entire game around the conceit that the story is actually a tall tale told in a saloon. The narrator changes details and facts, and the game world reshapes itself in front of your eyes to fit the new story. Sadly it doesn't involve the player dying, which would fit perfectly.

Good: Allow the Player to Escape

In Batman: Arkham Asylum (and its sequels) the grapple-hook is a central element of the game. It lets you quickly move around the world and climb objects.

Batman Arkham Origins pit
Batman: Arkham Origins. When you fail a jumping segment, Batman pulls himself up where you began.

If you should fall into a pit in the game, you do not die. Instead, Batman pulls himself out where you began the jump. This nicely integrates the grapple mechanic to prevent some player deaths. As there are plenty of other ways to die in the game, this does not feel like a cop-out.

EVE Online also has a unique way of escaping before actually having to die.

When your spaceship in EVE is destroyed, the "pilot capsule" remains. This is a ship that can do nothing other than move. Usually you use it to get back to the nearest station, where you can get a proper ship.

The capsule can be destroyed, however, killing you and taking all your costly implants with it. You can get cloned at the nearest port, but without the implants that were part of your character.

This creates a unique mechanic in MMOs. Capsule killing is frowned upon, but is used in assassinations and other plots.

Good: Keep the Player in a "Downed" State With a Way to Get Back Into the Action

When you run out of health in the Borderlands games, you don't immediately die. Instead, you go into "last chance" mode.

The screen color fades until everything is grey, and you can only crawl. But if you manage to kill an enemy, you get a "second wind", stand up, get a portion of your health back, and can fight and walk again. You can also be helped up by another player, if you are playing co-op.

Borderlands 2 second wind
Borderlands 2. The Second Wind is also an empowerment moment. Instead of dying you just keep on going.

This is a great system. It keeps you in the game and engaged, and doesn't immediately throw you out.

Long before Borderlands, though, Prey already had a second wind mechanic.

In Prey you get the ability to enter the "spirit world" during gameplay. Inside, you are invisible, you can move through certain barriers, and you can use paths that only exist in there. It is necessary to use it within the main game.

Prey spirit world
The spirit world of Prey. The entire game has Native American culture woven into it.

When you die you go into this spirit world, a ghostly version of the level you are in. If you manage to kill a certain number of spirits with your bow, you get resurrected right on the spot where you died and can continue playing.

Left 4 Dead has a similar system, where upon losing all your health you are "downed". You fall to the ground and cannot move, but you can still fire your pistol. All the while the screen slowly turns grey, until you actually die. You can continue playing if another player helps you up, which keeps groups of players tight.

Left 4 Dead incapacitated
Being downed in Left 4 Dead lets you still fire a pistol, if you have one. There are other types of being downed or being incapacitated where you cannot fire.

But the most integrated version of a downed state is in Republic Commando.

This was the first game to include the resuscitate option, way before Battlefield 2, Left 4 Dead, or Mass Effect 3. When one of your teammates dies, you can bring them back to life with an injection of the magical healing substance Bacta and a defibrillator blast.

What it does (still!) uniquely is offer you the choice to command whether they should get you or keep fighting.

Republic Commando
Republic Commando's downed screen, which sadly has never been copied.

When you die, you see three options displayed on your (now blurry) in-game helmet (pictured above). Maintain Current Orders has your squad continue to engage the enemy, and Recall and Revive calls one of them over to bring you back up. In the heat of the battle it might be more useful to have them reduce the danger by eliminating a few enemies first, so you can't just call this automatically. Only the last option has you reload a game, and becomes necessary when your entire team dies.

This creates tense fights and moments where you might get downed and have to weigh the chances of your squadmates being able to revive you. The AI is also good enough to support this—with horrible AI it wouldn't work as well as it does.

Above All...

Best: Integrate Death Into the Story

Bioshock Infinite handles the theme of multiple universes. You jump between them in the story.

And when you do die, you suddenly wake up in your office, which looks like it did in flashback sequences. But opening the door puts you close to where you were when you died.

Bioshock Infinite office
Bioshock Infinite. You also visit your office in regular flashback sequences.

This implies that you did actually die. Then the universe-hopping characters that engaged you in the first place went to another universe to get another you, and fast-forwarded through the story to leave you at the point of your previous death.

This is alluded to at the start of the game, where you see a list of decisions you have made before, and it implies there were several dozen yous.

Bioshock Infinite heads and tails
Bioshock Infinite. That's the number of yous that have been through here before.

Once again player death is woven beautifully into the gameplay, and actually extends the mythology.

Bastion uses a similar approach in its New Game Plus playthrough. At the end of the game you get the choice of leaving the broken world or engaging a machine with unknown properties, which could possibly turn back time to before the game started. If you do the latter, characters refer to the repetition after the game has been started again, integrating the out-of-game action into the game itself.

Good: Keep Downtime to a Minimum

If there is no way to have a fun death mechanic, at least make sure it's as painless as possible. This means mainly two things:

Allow reloading or restarting as quickly as possible. When you fail in Trials you can press the restart button, which puts you immediately at the last checkpoint. There is no lengthy animation or load time. Players aren't frustrated by failure, yet it still retains its heft.

Trials 2 reset
Trials 2: Second Edition. Pressing backspace at any time resets you to the latest checkpoint. If you crash, a timer starts, which resets you there after a few seconds.

Also, keep automatic and fair restart/save points, so the player doesn't feel punished by having to replay segments.

Gunpoint uses a fun system of staggered reload points in the past. When you die, you get to chose from several reload points at different times in the past, which essentially turns it into a time-travel mechanic.

Gunpoint death screen
Gunpoint's death screen. The multiple points offer various options of how to approach the situation again.

Conclusion

Having the ability to fail in a game is important, as it gives the gameplay and story meaning and substance. Using an unreliable narrator is a relatively cost-effective way of having player death integrated into the story. Avoiding having in-game characters acknowledge resurrection systems as part of the narrative also helps maintain the suspension of disbelief.

08 Sep 16:18

How to Adapt A* Pathfinding to a 2D Grid-Based Platformer: Theory

by Daniel Branicki

A* grid-based pathfinding works well for games in which the characters can move freely along both the x- and y-axes. The problem with applying this to platformers is that movement on the y-axis is heavily restricted, due to simulated gravitational forces. 

In this tutorial, I'll give a broad overview of how to modify a standard A* algorithm to work for platformers by simulating these gravitational restrictions. (In the next part, I'll walk through actually coding the algorithm.) The adapted algorithm could be used to create an AI character that follows the player, or to show the player a route to their goal, for example.

I assume here that you're already familiar with A* pathfinding, but if you need an introduction, Patrick Lester's A* Pathfinding for Beginners is excellent.

Demo

You can play the Unity demo, or the WebGL version (64MB), to see the final result in action. Use WASD to move the character, left-click on a spot to find a path you can follow to get there, and right-click a cell to toggle the ground at that point.

By the end of this series, we'll also have added one-way platforms, extended the code to deal with different sizes of character, and coded a bot that can automatically follow the path! Check out that Unity demo here (or the 100MB+ WebGL version).

Defining the Rules

Before we can adapt the pathfinding algorithm, we need to clearly define what forms the paths themselves can take.

What Can the Character Do?

Let's say that our character takes up one cell, and can jump three cells high. 

We won't allow our character to move diagonally through cells, because we don't want it to go through solid terrain. (That is, we won't allow it to move through the corner of a cell, only through the top, bottom, left, or right side.) To move to a diagonally adjacent cell, the character must move one cell up or down, and one cell left or right. 

Since the character's jump height is three cells, then whenever it jumps, after it has moved up three times, it should not be able to move up any more cells, but it should still be able to move to the side.

Based on these rules, here is an example of the path the character would take during its maximum distance jump:

Of course the character can jump straight up, or with any combination of left and right movements, but this example shows the kind of approximation we'll need to embrace when calculating the path using the grid.

Representing the Jump Paths With Jump Values

Each of the cells in the path will need to keep data about the jump height, so that our algorithm can detect that the player can jump no higher and must start falling down. 

Let's start by assigning each cell's jump value by increasing it by one, each cell, for as long as the jump continues. Of course, when the character is on the ground, the jump value should be 0.

Here's that rule applied to the same maximum-distance jump path from before:

The cell that contains a 6 marks the highest point in the path. This makes sense, since that's twice the value of the character's maximum jump height, and the character alternates moving one cell up and one cell to the side, in this example.

Note that, if the character jumps straight up, and we continue increasing the jump value by one each cell, then this no longer works, because in that case the highest cell would have a jump value of 3.

Let's modify our rule to increase the jump value to the next even number whenever the character moves upwards. Then, if the jump value is even, the character can move either left, right, or down (or up, if they haven't reached a jump value of 6 yet), and if the jump value is odd, the character only move up or down (depending on whether they have reached the peak of the jump yet).

Here's what that looks like for a jump straight up:

And here's a more complicated case:

Here's how the jump values are calculated:

  1. Start on the ground: jump value is 0.
  2. Jump horizontally: increase the jump value by 1, so the jump value is 1.
  3. Jump vertically: increase the jump value to the next even number: 2.
  4. Jump vertically: increase the jump value to the next even number: 4.
  5. Jump horizontally: increase the jump value by 1; jump value is now 5.
  6. Jump vertically: increase the value to the next even number: 6. (The character can no longer move upwards, so only left, right and bottom nodes are available.)
  7. Fall horizontally: increase the jump value by 1; jump value is now 7.
  8. Fall vertically: increase the jump value to the next even number: 8.
  9. Fall vertically: increase the value to the next even number: 10.
  10. Fall vertically: since the character is now on the ground. set the jump value to 0.

As you can see, with this kind of numbering we know exactly when the character reaches its maximum jump height: it's the cell with the jump value equal to twice the maximum character jump height. If the jump value is less than this, the character can still move upwards; otherwise, we need to ignore the node directly above.

Adding Coordinates

Now that we're aware of the kind of movements the character can make on the grid, let's consider the following setup:

The green cell at position (3, 1) is the character; the blue cell at position (2, 5) is the goal. Let's draw a route that the A* algorithm may choose first to attempt to reach the goal@

The yellow number in the top right corner of a cell is the cell's jump value. As you can see, with a straight upwards jump, the character can jump three tiles up, but no further. This is no good. 

We may find more luck with another route, so let's rewind our search and start again from node (3, 2).

As you can see, jumping on the block to the right of the character allowed it to jump high enough to get to the goal! However, there is a big problem here... 

In all likelihood, the first route the algorithm will take is the first one we examined. After taking it, the algorithm will not get very far, and will end up back at node (3, 2). It can then search through nodes (4, 2), (4, 3), (3, 3) (again), (3, 4) (again), (3, 5), and finally the goal cell, (2, 5)

In a basic version of the A* algorithm, if a node has been visited once already, then we don't need to process it ever again. In this version, however, we do. This is because the nodes are not distinguished solely by x- and y-coordinates, but also by jump values. 

In our first attempt to find a path, the jump value at node (3, 3) was 4; in our second attempt, it was 3. Since in the second attempt the jump value at that cell was smaller, it meant that we could potentially get higher from there than we could during the first attempt. 

This basically means that node (3, 3) with a jump value of 4 is a different node than the node at (3, 3) with a jump value of 3. The grid essentially needs to become three-dimensional at some coordinates to accommodate for these differences, like so:

We can't simply change the jump value of the node at (3, 3) from 4 to 3, because some paths use the same node multiple times; if we did that, we would basically override the previous node and that would of course corrupt the end result. 

We'd have the same issue if the first path would have reached the goal despite the higher jump values; if we had overridden one of the nodes with a more promising one, then we would have no way to recover the original path.

The Strengths and Weaknesses of Using Grid-Based Pathfinding

Remember, it's the algorithm that uses a grid-based approach; in theory, your game and its levels don't have to.

Strengths

  • Works really well with tile-based games.
  • Extends the basic A* algorithm, so you don't have to have two completely different algorithms for flying and land characters.
  • Works really well with dynamic terrain; doesn't require a complicated node-rebuilding process.

Weaknesses

  • Inaccuracy: minimum distance is the size of a single cell.
  • Requires a grid representation of each map or level.

Engine and Physics Requirements

Character Freedom vs Algorithm Freedom

It is important for the character to have at least as much freedom of movement as the algorithm expects—and preferably a bit more than that. 

Having the character movement match exactly the algorithm's constraints is not a viable approach, due to the discrete nature of the grid and the grid's cell size. It is possible to code the physics in such a way that the algorithm will always find a way if there is one, but that requires you to build the physics for that purpose. 

The approach I take in this tutorial is to fit the algorithm to the game's physics, not the other way around.

The main problems occur in edge cases when the algorithm's expected freedom of movement freedom does not match the true, in-game character's freedom of movement.

When the Character Has More Freedom Than the Algorithm Expects

Let's say the AI allows for jumps six cells long, but the game's physics allow for a seven-cell jump. If there is a path that requires the longer jump to reach the goal in the quickest time, then the bot will ignore that path and choose the more conservative one, thinking that the longer jump is impossible. 

If there is a path that requires the longer jump and there is no other way to reach the goal, then the pathfinder will conclude that the goal is unreachable.

When the Algorithm Expects More Freedom Than the Character Has

If, conversely, the algorithm thinks that it is possible to jump seven cells away, but the game's physics actually allow only for a six-cell jump, then the bot may either follow the wrong path and fall into a place from which it cannot get out, or try to find a path again and receive the same incorrect result, causing a loop.

(Out of these two problems, it's preferable to let the game's physics allow for more freedom of movement than the algorithm expects.)

Solving These Problems

The first way to ensure that the algorithm is always correct is to have levels which players cannot modify. In this case, you just need to make sure that whatever terrain you design or generate works well with the pathfinding AI.

The second solution to these problems is to tweak the algorithm, the physics, or both, to make sure that they match. As I mentioned earlier, this doesn't mean they need to match exactly; for example, if the algorithm thinks the character can jump five cells upwards, it is fine to set the real maximum jump at 5.5 cells high. (Unlike the algorithm, the game's physics can use fractional values.)

Depending on the game, it could also be true that the AI bot not finding an existing path is not a huge deal; it will simply give up and go back to its post, or just sit and wait for the player.

Conclusion

At this point, you should have a decent conceptual understanding of how A* pathfinding can be adapted to a platformer. In my next tutorial, we'll make this concrete, by actually adapting an existing A* pathfinding algorithm in order to implement this in a game!

08 Sep 16:18

How to Adapt A* Pathfinding to a 2D Grid-Based Platformer: Implementation

by Daniel Branicki

Now that we have a good idea of how our A* platforming algorithm will work, it's time to actually code it. Rather than build it from scratch, we'll adapt an existing A* pathfinding system to add the new platfomer compatibility.

The implementation we'll adapt in this tutorial is Gustavo Franco's grid-based A* pathfinding system, which is written in C#; if you're not familiar with it, read his explanation of all the separate parts before continuing. If you haven't read the previous tutorial in this series, which gives a general overview of how this adaptation will work, read that first.

Note: the complete source code for this tutorial can be found in this GitHub repo, in the Implementation folder. 

Demo

You can play the Unity demo, or the WebGL version (64MB), to see the final result in action. Use WASD to move the character, left-click on a spot to find a path you can follow to get there, and right-click a cell to toggle the ground at that point.

Setting Up the Game Project

We need to set some rules for the example game project used in this tutorial. You can of course change these rules when implementing this algorithm in your own game!

Setting Up the Physics

The physics rules used in the example project are very simple. 

When it comes to horizontal speed, there is no momentum at all. The character can change directions immediately, and immediately moves in that direction at full speed. This makes it much easier for the algorithm to find a correct path, because we don't have to care about the character's current horizontal speed. It also makes it easier to create a path-following AI, because we don't have to make the character gain any momentum before performing a jump.

We use four constants to define and restrict character movement:

  • Gravity
  • Maximum falling speed
  • Walking speed
  • Jumping speed

Here's how they're defined for this example project:

public const float cGravity = -1030.0f;
public const float cMaxFallingSpeed = -900.0f;
public const float cWalkSpeed = 160.0f;
public const float cJumpSpeed = 410.0f;

The base unit used in the game is a pixel, so the character will move 160 pixels per second horizontally (when walking or jumping); when jumping, the character's vertical speed will be set to 410 pixels per second. In the test project the character's falling speed is limited to 900, so there is no possibility of it falling through the tiles. The gravitation is set to be -1030 pixels per second2.

The character's jump height is not fixed: the longer the jump key is pressed, the higher the character will jump. This is achieved by setting the character's speed to be no more than 200 pixels per second once the jump key is no longer pressed:

if (!mInputs[(int)KeyInput.Jump] && mSpeed.y > 0.0f)
{
    mSpeed.y = Mathf.Min(mSpeed.y, 200.0f);
    mFramesFromJumpStart = 100;
}

Setting Up the Grid

The grid is a simple array of bytes which represent the cost of movement to a specific cell (where 0 is reserved for blocks which the character cannot move through). 

We will not go deep into the weights in this tutorial; we'll actually be using just two values: 0 for solid blocks, and 1 for empty cells. 

The algorithm used in this tutorial requires the grid's width and height to be a power of two, so keep that in mind.

Here's an example of a grid array and an in-game representation of it.

private byte[,] mGrid = {{ 0, 1, 1, 1 }
                         { 0, 1, 1, 1 }
                         { 0, 1, 0, 0 }
                         { 0, 0, 0, 0 }};

A Note on Threading

Normally we would set up the pathfinder process in a separate thread, so there are no freezes in the game while it is working, but in this tutorial we'll use a single threaded version, due to the limitations of the WebGL platform which this demo runs on. 

The algorithm itself can be run in a separate thread, so you should have no problems with merging it into your code in that way if you need to.

Adding the Jump Values to the Nodes

Remember from the theory overview that nodes are distinguished not just by x- and y-coordinates, but also by jump values. In the standard implementation of A*, x- and y-coordinates are sufficient to define a node, so we need to modify it to use jump values as well.

From this point on, we'll be modifying the core PathFinderFast.cs source file from Gustavo Franco's implementation.

Re-Structuring the List of Nodes

First, we'll add a new list of nodes for each grid cell; this list will replace mCalcGrid from the original algorithm:

private List<PathFinderNodeFast>[] nodes;

Note that PathFinderFast uses one-dimensional arrays, rather than a 2D array as we might expect to use when representing a grid. 

This is not a problem, because we know the grid's width and height, so instead of accessing the data by X and Y indices, we'll access it by a single int which is calculated using  location = (y << gridWidthLog2) + x. (This is a slightly faster version of a classic location = (y * gridWidth) + x). 

Because we need a grid that is three-dimensional (to incorporate the jump values as a third "dimension"), we'll need to add another integer, which will be a node's index in a list at a particular X and Y position. 

Note that we cannot merge all three coordinates into one integer, because the third dimension of the grid is not a constant size. We could consider using simply a three-dimensional grid, which would restrict the number of nodes possible at a particular (x, y) position—but if the array size on the "z-axis" were too small, then the algorithm could return an incorrect result, so we'll play it safe.

Here, then, is the struct that we will use to index a particular node:

public struct Location
{
    public Location(int xy, int z)
    {
        this.xy = xy;
        this.z = z;
    }

    public int xy;
    public int z;
}

The next thing we need to modify is the PathFinderNodeFast structure. There are two things we need to add here:

  • The first is the index of a node's parent, which is basically the previous node from which we arrived to the current node. We need to have that index since we cannot identify the parent solely by its x- and y-coordinates. The x- and y-coordinates will point us to a list of nodes that are at that specific position, so we also need to know the index of our parent in that list. We'll name that index PZ.
  • The other thing we need to add to the structure is the jump value.

Here's the old struct:

internal struct PathFinderNodeFast
{
    public int     F;
    public int     G;
    public ushort  PX;      //parent x
    public ushort  PY;      //parent y
    public byte    Status;
}

And here's what we'll modify it to:

internal struct PathFinderNodeFast
{
    public int     F;
    public int     G;
    public ushort  PX;          //parent y
    public ushort  PY;          //parent x
    public byte    Status;
    public byte    PZ;          //parent z
    public short   JumpLength;  //jump value
}

There's still one problem, though. When we use the algorithm once, it will populate the cells' lists of nodes. We need to clear those lists after each time the pathfinder is run, because if we don't, then those lists will grow all the time with each use of the algorithm, and the memory use will rise uncontrollably. 

The thing is, we don't really want to clear every list every time the pathfinder is run, because the grid can be huge and the path will likely never touch most of the grid's nodes. It would cause a big overhead, so it's better to only clear the lists that the algorithm went through. 

For that, we need an additional container which we'll use to remember which cells were touched:

private Stack<int> touchedLocations;

A stack will work fine; we'll just need to clear all the lists contained in it one by one.

Updating the Priority Queue

Now let's get our priority queue mOpen to work with the new index. 

The first thing we need to do is change the declaration to use Locations rather than integers—so, from:

private PriorityQueueB<int> mOpen = null;

to:

private PriorityQueueB<Location> mOpen = null;

Next, we need to change the queue's comparer to make it use the new structure. Right now it uses just an array of nodes; we need to change it so it uses an array of lists of nodes instead. We also need to make sure it compares the nodes using a Location instead of just an integer.

Here's the old code:

internal class ComparePFNodeMatrix : IComparer<int>
{
    PathFinderNodeFast[] mMatrix;

    public ComparePFNodeMatrix(PathFinderNodeFast[] matrix)
    {
        mMatrix = matrix;
    }

    public int Compare(int a, int b)
    {
        if (mMatrix[a].F > mMatrix[b].F)
            return 1;
        else if (mMatrix[a].F < mMatrix[b].F)
            return -1;
        return 0;
    }
}

And here's the new:

internal class ComparePFNodeMatrix : IComparer<Location>
{
    List<PathFinderNodeFast>[] mMatrix;
    
    public ComparePFNodeMatrix(List<PathFinderNodeFast>[] matrix)
    {
        mMatrix = matrix;
    }

    public int Compare(Location a, Location b)
    {
        if (mMatrix[a.xy][a.z].F > mMatrix[b.xy][b.z].F)
            return 1;
        else if (mMatrix[a.xy][a.z].F < mMatrix[b.xy][b.z].F)
            return -1;
        return 0;
    }
}

Now, let's initialize the lists of nodes and the touched locations stack when the pathfinder is created. Again, here's the old code:

if (mCalcGrid == null || mCalcGrid.Length != (mGridX * mGridY))
{
    mCalcGrid = new PathFinderNodeFast[mGridX * mGridY];
    mClose = new List<Vector2i>(mGridX * mGridY);
}

And here's the new:

if (others == null || others.Length != (mGridX * mGridY))
{
	nodes = new List<PathFinderNodeFast>[mGridX * mGridY];
    touchedLocations = new Stack<int>(mGridX * mGridY);
    mClose = new List<Vector2i>(mGridX * mGridY);
}

for (var i = 0; i < others.Length; ++i)
{
	nodes[i] = new List<PathFinderNodeFast>(1);
}

Finally, let's create our priority queue using the new constructor:

mOpen = new PriorityQueueB<Location>(new ComparePFNodeMatrix(nodes));

Initializing the Algorithm

When we start the algorithm, we want to tell it how big our character is (in cells) and also how high the character can jump. 

(Note that, for this tutorial, we will not actually use characterWidth nor characterHeight; we will assume that the size of the character is a 1x1 block.)

Change this line:

public List<PathFinderNode> FindPath(Point start, Point end)

To this:

public List<Vector2i> FindPath(Vector2i start, Vector2i end, int characterWidth, int characterHeight, short maxCharacterJumpHeight)

First thing we need to do is clear the lists at the previously touched locations:

while (touchedLocations.Count > 0)
    others[touchedLocations.Pop()].Clear();

Next, we must make sure that the character can fit in the end location. (If it can't, there's no point in running the algorithm, because it will be impossible to find a valid path.)

if (mGrid[end.x, end.y] == 0)
	return null;

Now we can create a start node. Instead of setting the values in mCalcGrid, we need to add a node to the nodes list at a particular position. 

First, let's calculate the location of the node. Of course, to be able to do this, we also need to change the type of mLocation to Location.

Change this line:

mLocation = (start.y << mGridXLog2) + start.x;

To this:

mLocation.xy = (start.y << mGridXLog2) + start.x;
mLocation.z = 0;

The mEndLocation can be left as-is; we'll use this to check if we have already reached our goal, so we only need to check the X and Y positions in that case:

mEndLocation = (end.y << mGridXLog2) + end.x;

For the start node initialization, we need to reset the parent PZ to 0 and set the appropriate jump value. 

When the starting point is on the ground, the jump value should be equal to 0—but what if we're starting in the air? The simplest solution will be to set it to the falling value and not to worry about it too much; finding a path when starting in mid-air may be quite troublesome, so we'll take the easy way out.

Here's the old code:

mLocation                      = (start.y << mGridXLog2) + start.x;
mEndLocation                   = (end.y << mGridXLog2) + end.x;
mCalcGrid[mLocation].G         = 0;
mCalcGrid[mLocation].F         = mHEstimate;
mCalcGrid[mLocation].PX        = (ushort) start.x;
mCalcGrid[mLocation].PY        = (ushort) start.y;
mCalcGrid[mLocation].Status    = mOpenNodeValue;

And the new:

mLocation.xy = (start.y << mGridXLog2) + start.x;
mLocation.z = 0;
mEndLocation = (end.y << mGridXLog2) + end.x;
                
PathFinderNodeFast firstNode = new PathFinderNodeFast();
firstNode.G = 0;
firstNode.F = mHEstimate;
firstNode.PX = (ushort)start.x;
firstNode.PY = (ushort)start.y;
firstNode.PZ = 0;
firstNode.Status = mOpenNodeValue;

if (mMap.IsGround(start.x, start.y - 1))
    firstNode.JumpLength = 0;
else
	firstNode.JumpLength = (short)(maxCharacterJumpHeight * 2);

We must also add the node to the list at the start position:

nodes[mLocation.xy].Add(firstNode);

And we also need to remember that the start node list is to be cleared on the next run:

touchedLocations.Push(mLocation.xy);

Finally, the location is queued and we can start with the core algorithm. To sum up, this is what the initialization of the pathfinder run should look like:

while (touchedLocations.Count > 0)
    nodes[touchedLocations.Pop()].Clear();

if (mGrid[end.x, end.y] == 0)
	return null;

mFound              = false;
mStop               = false;
mStopped            = false;
mCloseNodeCounter   = 0;
mOpenNodeValue      += 2;
mCloseNodeValue     += 2;
mOpen.Clear();

mLocation.xy = (start.y << mGridXLog2) + start.x;
mLocation.z = 0;
mEndLocation                   = (end.y << mGridXLog2) + end.x;

PathFinderNodeFast firstNode = new PathFinderNodeFast();
firstNode.G = 0;
firstNode.F = mHEstimate;
firstNode.PX = (ushort)start.x;
firstNode.PY = (ushort)start.y;
firstNode.PZ = 0;
firstNode.Status = mOpenNodeValue;

if (mMap.IsGround(start.x, start.y - 1))
	firstNode.JumpLength = 0;
else
	firstNode.JumpLength = (short)(maxCharacterJumpHeight * 2);

nodes[mLocation.xy].Add(firstNode);
touchedLocations.Push(mLocation.xy);

mOpen.Push(mLocation);

Calculating a Successor

Checking the Position

We don't need to modify much in the node processing part; we just need to change mLocation to mLocation.xy so that mLocationX and mLocationY can be calculated.

Change this:

while(mOpen.Count > 0 && !mStop)
{
    mLocation    = mOpen.Pop();

    //Is it in closed list? means this node was already processed
    if (mCalcGrid[mLocation].Status == mCloseNodeValue)
        continue;

    mLocationX   = (ushort) (mLocation & mGridXMinus1);
    mLocationY   = (ushort) (mLocation >> mGridXLog2);

    if (mLocation == mEndLocation)
    {
        mCalcGrid[mLocation].Status = mCloseNodeValue;
        mFound = true;
        break;
    }

    if (mCloseNodeCounter > mSearchLimit)
    {
        mStopped = true;
        return null;
    }

To this:

while(mOpen.Count > 0 && !mStop)
{
    mLocation = mOpen.Pop();

    //Is it in closed list? means this node was already processed
    if (nodes[mLocation.xy][mLocation.z].Status == mCloseNodeValue)
        continue;

    mLocationX = (ushort) (mLocation.xy & mGridXMinus1);
    mLocationY = (ushort) (mLocation.xy >> mGridXLog2);

    if (mLocation.xy == mEndLocation)
    {
        nodes[mLocation.xy][mLocation.z] = nodes[mLocation.xy][mLocation.z].UpdateStatus(mCloseNodeValue);
        mFound = true;
        break;
    }

    if (mCloseNodeCounter > mSearchLimit)
    {
        mStopped = true;
        return null;
    }

Note that, when we change the status of a node inside the nodes list, we use the UpdateStatus(byte newStatus) function. We cannot really change any of the members of the struct inside the list, since the list returns a copy of the node; we need to replace the whole node. The function simply returns a copied node with the Status changed to newStatus:

public PathFinderNodeFast UpdateStatus(byte newStatus)
{
    PathFinderNodeFast newNode = this;
    newNode.Status = newStatus;
    return newNode;
}

We also need to alter the way the successors are calculated:

for (var i=0; i<(mDiagonals ? 8 : 4); i++)
{
    mNewLocationX = (ushort) (mLocationX + mDirection[i,0]);
    mNewLocationY = (ushort) (mLocationY + mDirection[i,1]);
    mNewLocation  = (mNewLocationY << mGridXLog2) + mNewLocationX;

Here we just calculate the location of one of the successors; we need this so that we can find the relative position of the successor node.

Determining the Type of a Position

The next thing we need to know is what type of position a successor represents. 

We are interested in four variants here:

  1. The character does not fit in the position. We assume that the cell position responds to the bottom-left "cell" of a character. If this is the case, then we can discard the successor, since there is no way to move the character through it.
  2. The character fits in the position, and is on the ground. This means that the successor's jump value needs to be changed to 0.
  3. The character fits in the position, and is just below the ceiling. This means that even if the character has enough speed to jump higher, it cannot, so we need to change the jump value appropriately.
  4. The character simply fits in the spot and is neither on the ground nor at the ceiling.

First, let's assume that the character is neither on the ground nor at the ceiling:

var onGround = false;
var atCeiling = false;

Let's check if the character fits the new spot. If not, we can safely skip the successor and check the next one.

if (mGrid[mNewLocationX, mNewLocationY] == 0)
    continue;

To check if the character would be on the ground when in the new location, we just need to see if there's a solid block below the successor:

if (mGrid[mNewLocationX, mNewLocationY - 1] == 0)
    onGround = true;

Similarly, we check whether the character would be at the ceiling:

if (mGrid[mNewLocationX, mNewLocationY + 1] == 0)
    atCeiling = true;    

Calculating the Jump Value

The next thing we need to do is see whether this successor is a valid one or not, and if it is then calculate an appropriate JumpLength for it.

First, let's get the JumpLength of the parent node:

var jumpLength = nodes[mLocation.xy][mLocation.z].JumpLength;

We'll also declare the newJumpLength for the currently processed successor:

short newJumpLength = jumpLength;

Now we can calculate the newJumpLength. (How we do this is explained at the very beginning of the theory overview.)

If the successor node is on the ground, then the newJumpValue is equal to 0:

if (onGround)
    newJumpLength = 0;

Nothing to worry about here. It's important to check whether the character is on the ground first, because if the character is both on the ground and at the ceiling then we want to set the jump value to 0.

If  the position is at the ceiling then we need  to consider two cases: 

  1. the character needs to drop straight down, or 
  2. the character can still move one cell to either side.

In the first case, we need to set the newJumpLength to be at least maxCharacterJumpHeight * 2 + 1, because this value means that we are falling and our next move needs to be done vertically. 

In the second case, the value needs to be at least maxCharacterJumpHeight * 2. Since the value is even, the successor node will still be able to move either left or right.

else if (atCeiling)
{
    if (mNewLocationX != mLocationX)
        newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2 + 1, jumpLength + 1);
    else
        newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2);
}

The "on ground" and "at ceiling" cases are solved; now we can get to calculating the jump value while in air.

Calculating the Jump Value in Mid-Air

First, let's handle the case in which the successor node is above the parent node.

If the jump length is even, then we increment it by two; otherwise, we increment it by one. This will result in an even value for newJumpLength:

else if (mNewLocationY > mLocationY)
{
	if (jumpLength % 2 == 0)
		newJumpLength = (short)(jumpLength + 2);
	else
		newJumpLength = (short)(jumpLength + 1);
}

Since, in an average jump, the character speed has its highest value at the jump start and end, we should represent this fact in the algorithm. 

We'll fix the jump start by forcing the algorithm to move two cells up if we just got off the ground. This can easily be achieved by swapping the jump value of 2 to 3 at the moment of the jump, because at the jump value of 3, the algorithm knows the character cannot go to the side (since 3 is an odd number). 

The jump curve will be changed to the following.

Let's also accommodate for this change in the code:

else if (mNewLocationY > mLocationY)
{
    if (jumpHeight < 2) //first jump is always two block up instead of one up and optionally one to either right or left
        newJumpHeight = 3;
	else if (jumpHeight % 2 == 0)
		newJumpHeight = (short)Mathf.Max(jumpHeight + 2, 2);
	else
		newJumpHeight = (short)Mathf.Max(jumpHeight + 1, 2);
}

(We'll fix the curve when the speed of the character is too high to go sideways when we validate the node later.)

Now let's handle the case in which the new node is below the previous one. 

If the new y-coordinate is lower than the parent's, that means we are falling. We calculate the jump value the same way we do when jumping up, but the minimum must be set to maxCharacterJumpHeight * 2. (That's because the character does not need to do a full jump to start falling—for example, it can simply walk off a ledge.) In that case the jump value should be changed from 1 to 6 immediately (in the case where the character's maximum jump height is 3):

else if (mNewLocationY < mLocationY)
{
	if (jumpLength % 2 == 0)
		newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2);
	else
		newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 1);
}

This way, the character can't step off a ledge and then jump three cells in the air!

Validating the Successor

Now we have all the data we need to validate a successor, so let's get to it.

First, let's dismiss a node if its jump value is odd and the parent is either to the left or to the right. That's because if the jump value is odd, then that means the character went to the side once already and now it needs to move either one block up or down:

if (jumpLength % 2 != 0 && mLocationX != mNewLocationX)
    continue;

If the character is falling, and the child node is above the parent, then we should skip it. This is how we prevent jumping ad infinitum; once the jump value hits the threshold we can only go down.

if (jumpLength >= maxCharacterJumpHeight * 2 && mNewLocationY > mLocationY)
    continue;

If the node's jump value is larger than (six plus the fall threshold value), then we should stop allowing the direction change on every even jump value. This will prevent the algorithm giving incorrect values when the character is falling really fast, because in that case instead of 1 block to the side and 1 block down it would need to move 1 block to the side and 2 or more blocks down. (Right now, the character can move 3 blocks to the side after it starts falling, and then we allow it to move sideways every 4 blocks traversed vertically.)

if (newJumpLength >= maxCharacterJumpHeight * 2 + 6 && mNewLocationX != mLocationX && (newJumpLength - (maxCharacterJumpHeight * 2 + 6)) % 8 != 3)
    continue;

If there's a need for a more accurate jump check, then instead of dismissing the node in the way shown above, we could create a lookup table with data determining at which jump lengths the character would be able to move to the side.

Calculating the Cost

When calculating the node cost, we need to take into account the jump value.

Making the Character Move Sensibly

It's good to make the character stick to the ground as much as possible, because it will make its movement less jumpy when moving through flat terrain, and will also encourage it to use "safer" paths, which do not require long falls. 

We can easily make the character do this by increasing the cost of the node according to its jump value. Here's the old code:

mNewG = mCalcGrid[mLocation].G + mGrid[mNewLocationX, mNewLocationY];

And here's the new:

mNewG = nodes[mLocation.xy][mLocation.z].G + mGrid[mNewLocationX, mNewLocationY] + newJumpLength / 4;

The newJumpLength / 4 works well for most cases; we don't want the character to stick to the ground too much, after all.

Revisiting Nodes With Different Jump Values

Normally, when we've processed the node once, we set its status to closed and never bother with it again; however, as we've already discussed, we may need to visit a particular position in the grid more than once.

First, before we decide to skip the currently checked node, we need to see if there is any node at the current (x, y) position. If there are no nodes in there yet, then we surely cannot skip the current one:

if (nodes[mNewLocation].Count > 0)
{
}

The only condition which allows us to skip the node is this: the node does not allow for any new movement compared to the other nodes on the same position

The new movement can happen if:

  • The currently processed node's jump value is lower than any of the other nodes at the same (x, y) position—in this case, the current node promises to let the character jump higher using this path than any other.
  • The currently processed node's jump value is even, and all other nodes' jump values at the position are not. This basically means that this particular node allows for sideways movement at this position, while others force us to move either up or down.

The first case is simple: we want to look through the nodes with lower jump values since these let us jump higher. The second case comes out in more peculiar situations, such as this one:

Here, we cannot move sideways when jumping up because we wanted to force the algorithm to go up twice after starting a jump. The problem is that, even when falling down, the algorithm would simply ignore the node with the jump value of 8, because we have already visited that position and the previous node had a lower jump value of 3. That's why in this case it's important to not skip the node with an even (and reasonably low) jump value.

First, let's declare our variables that will let us know what the lowest jump value at the current (x, y) position is, and whether any of the nodes there allow sideways movement:

int lowestJump = short.MaxValue;
bool couldMoveSideways = false;

Next, we need to iterate over all the nodes and set the declared variables to the appropriate values:

for (int j = 0; j < nodes[mNewLocation].Count; ++j)
{
    if (nodes[mNewLocation][j].JumpLength < lowestJump)
        lowestJump = nodes[mNewLocation][j].JumpLength;

    if (nodes[mNewLocation][j].JumpLength % 2 == 0 && nodes[mNewLocation][j].JumpLength < maxCharacterJumpHeight * 2 + 6)
        couldMoveSideways = true;
}

As you can see, we not only check whether the node's jump value is even, but also whether the jump value is not too high to move sideways.

Finally, let's get to the condition which will decide whether we can skip the node or not:

if (lowestJump <= newJumpLength && (newJumpLength % 2 != 0 || newJumpLength >= maxCharacterJumpHeight * 2 + 6 || couldMoveSideways))
    continue;

As you can see, the node is skipped if the lowestJump is less or equal to the processed node's jump value and any of the other nodes in the list allowed for sideways movement.

We can leave the heuristic formula as-is; we don't need to change anything here:

switch(mFormula)
{
    default:
    case HeuristicFormula.Manhattan:
        mH = mHEstimate * (Mathf.Abs(mNewLocationX - end.x) + Mathf.Abs(mNewLocationY - end.y));
        break;
    case HeuristicFormula.MaxDXDY:
        mH = mHEstimate * (Math.Max(Math.Abs(mNewLocationX - end.x), Math.Abs(mNewLocationY - end.y)));
        break;
    case HeuristicFormula.DiagonalShortCut:
        var h_diagonal  = Math.Min(Math.Abs(mNewLocationX - end.x), Math.Abs(mNewLocationY - end.y));
        var h_straight  = (Math.Abs(mNewLocationX - end.x) + Math.Abs(mNewLocationY - end.y));
        mH = (mHEstimate * 2) * h_diagonal + mHEstimate * (h_straight - 2 * h_diagonal);
        break;
    case HeuristicFormula.Euclidean:
        mH = (int) (mHEstimate * Math.Sqrt(Math.Pow((mNewLocationY - end.x) , 2) + Math.Pow((mNewLocationY - end.y), 2)));
        break;
    case HeuristicFormula.EuclideanNoSQR:
        mH = (int) (mHEstimate * (Math.Pow((mNewLocationX - end.x) , 2) + Math.Pow((mNewLocationY - end.y), 2)));
        break;
    case HeuristicFormula.Custom1:
        var dxy       = new Vector2i(Math.Abs(end.x - mNewLocationX), Math.Abs(end.y - mNewLocationY));
        var Orthogonal  = Math.Abs(dxy.x - dxy.y);
        var Diagonal    = Math.Abs(((dxy.x + dxy.y) - Orthogonal) / 2);
        mH = mHEstimate * (Diagonal + Orthogonal + dxy.x + dxy.y);
        break;
}

Tidying Up

Now, finally, since the node has passed all the checks, we can create an appropriate PathFinderNodeFast instance for it.

PathFinderNodeFast newNode = new PathFinderNodeFast();
newNode.JumpLength = newJumpLength;
newNode.PX = mLocationX;
newNode.PY = mLocationY;
newNode.PZ = (byte)mIdentifier.y;
newNode.G = mNewG;
newNode.F = mNewG + mH;
newNode.Status = mOpenNodeValue;

And we can also finally add the node to the node list at the mNewLocation

Before we do that, though, let's add the location to the touched locations stack if the list is empty. We'll know that we need to clear this location's list when we run the pathfinder again:

if (nodes[mNewLocation].Count == 0)
    touchedLocations.Push(mNewLocation);

nodes[mNewLocation].Add(newNode);
mOpen.Push(new Location(mNewLocation, nodes[mNewLocation].Count - 1));

After all the children have been processed, we can change the status of the parent to closed and increment the mCloseNodeCounter:

mCloseNodeCounter++;
nodes[mLocation.xy][mLocation.z] = nodes[mLocation.xy][mLocation.z].UpdateStatus(mCloseNodeValue);

In the end, the children's loop should look like this.

//Lets calculate each successors
for (var i=0; i<(mDiagonals ? 8 : 4); i++)
{
    mNewLocationX = (ushort) (mLocationX + mDirection[i,0]);
    mNewLocationY = (ushort) (mLocationY + mDirection[i,1]);
    mNewLocation  = (mNewLocationY << mGridXLog2) + mNewLocationX;
	
	var onGround = false;
	var atCeiling = false;

    if (mGrid[mNewLocationX, mNewLocationY] == 0)
        goto CHILDREN_LOOP_END;

    if (mMap.IsGround(mNewLocationX, mNewLocationY - 1))
        onGround = true;
    else if (mGrid[mNewLocationX, mNewLocationY + characterHeight] == 0)
        atCeiling = true;	
	
	//calculate a proper jumplength value for the successor

    var jumpLength = nodes[mLocation.xy][mLocation.z].JumpLength;
    short newJumpLength = jumpLength;

	if (atCeiling)
    {
        if (mNewLocationX != mLocationX)
            newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2 + 1, jumpLength + 1);
        else
            newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2);
    }
    else if (onGround)
		newJumpLength = 0;
	else if (mNewLocationY > mLocationY)
	{
        if (jumpLength < 2) //first jump is always two block up instead of one up and optionally one to either right or left
            newJumpLength = 3;
        else  if (jumpLength % 2 == 0)
            newJumpLength = (short)(jumpLength + 2);
        else
            newJumpLength = (short)(jumpLength + 1);
	}
	else if (mNewLocationY < mLocationY)
	{
		if (jumpLength % 2 == 0)
			newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2);
		else
			newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 1);
	}
	else if (!onGround && mNewLocationX != mLocationX)
		newJumpLength = (short)(jumpLength + 1);
	
	if (jumpLength >= 0 && jumpLength % 2 != 0 && mLocationX != mNewLocationX)
		continue;
	
	//if we're falling and succeor's height is bigger than ours, skip that successor
	if (jumpLength >= maxCharacterJumpHeight * 2 && mNewLocationY > mLocationY)
		continue;

    if (newJumpLength >= maxCharacterJumpHeight * 2 + 6 && mNewLocationX != mLocationX && (newJumpLength - (maxCharacterJumpHeight * 2 + 6)) % 8 != 3)
		continue;


    mNewG = nodes[mLocation.xy][mLocation.z].G + mGrid[mNewLocationX, mNewLocationY] + newJumpLength / 4;

    if (nodes[mNewLocation].Count > 0)
    {
        int lowestJump = short.MaxValue;
        bool couldMoveSideways = false;
        for (int j = 0; j < nodes[mNewLocation].Count; ++j)
        {
            if (nodes[mNewLocation][j].JumpLength < lowestJump)
                lowestJump = nodes[mNewLocation][j].JumpLength;

            if (nodes[mNewLocation][j].JumpLength % 2 == 0 && nodes[mNewLocation][j].JumpLength < maxCharacterJumpHeight * 2 + 6)
                couldMoveSideways = true;
        }

        if (lowestJump <= newJumpLength && (newJumpLength % 2 != 0 || newJumpLength >= maxCharacterJumpHeight * 2 + 6 || couldMoveSideways))
            continue;
    }
	
    switch(mFormula)
    {
        default:
        case HeuristicFormula.Manhattan:
            mH = mHEstimate * (Mathf.Abs(mNewLocationX - end.x) + Mathf.Abs(mNewLocationY - end.y));
            break;
        case HeuristicFormula.MaxDXDY:
            mH = mHEstimate * (Math.Max(Math.Abs(mNewLocationX - end.x), Math.Abs(mNewLocationY - end.y)));
            break;
        case HeuristicFormula.DiagonalShortCut:
            var h_diagonal  = Math.Min(Math.Abs(mNewLocationX - end.x), Math.Abs(mNewLocationY - end.y));
            var h_straight  = (Math.Abs(mNewLocationX - end.x) + Math.Abs(mNewLocationY - end.y));
            mH = (mHEstimate * 2) * h_diagonal + mHEstimate * (h_straight - 2 * h_diagonal);
            break;
        case HeuristicFormula.Euclidean:
            mH = (int) (mHEstimate * Math.Sqrt(Math.Pow((mNewLocationY - end.x) , 2) + Math.Pow((mNewLocationY - end.y), 2)));
            break;
        case HeuristicFormula.EuclideanNoSQR:
            mH = (int) (mHEstimate * (Math.Pow((mNewLocationX - end.x) , 2) + Math.Pow((mNewLocationY - end.y), 2)));
            break;
        case HeuristicFormula.Custom1:
            var dxy       = new Vector2i(Math.Abs(end.x - mNewLocationX), Math.Abs(end.y - mNewLocationY));
            var Orthogonal  = Math.Abs(dxy.x - dxy.y);
            var Diagonal    = Math.Abs(((dxy.x + dxy.y) - Orthogonal) / 2);
            mH = mHEstimate * (Diagonal + Orthogonal + dxy.x + dxy.y);
            break;
    }

    PathFinderNodeFast newNode = new PathFinderNodeFast();
    newNode.JumpLength = newJumpLength;
    newNode.PX = mLocationX;
    newNode.PY = mLocationY;
    newNode.PZ = (byte)mLocation.z;
    newNode.G = mNewG;
    newNode.F = mNewG + mH;
    newNode.Status = mOpenNodeValue;

    if (nodes[mNewLocation].Count == 0)
        touchedLocations.Push(mNewLocation);

    nodes[mNewLocation].Add(newNode);
    mOpen.Push(new Location(mNewLocation, nodes[mNewLocation].Count - 1));
	
CHILDREN_LOOP_END:
	continue;
}

Filtering the Nodes

We don't really need all the nodes that we'll get from the algorithm. Indeed, it will be much easier for us to write a path-following AI if we filter the nodes to a smaller set which we can work with more easily.

The node filtering process isn't actually part of the algorithm, but is rather an operation to prepare the output for further processing. It doesn't need to be executed in the PathFinderFast class itself, but that will be the most convenient place to do it for the purposes of this tutorial.

The node filtering can be done alongside the path following code; it is fairly unlikely that we'll filter the node set perfectly to suit our needs with our initial assumptions, so, often, a lot of tweaks will be needed. In this tutorial we'll go ahead and reduce the set to its final form right now, so later we can focus on the AI without having to modify the pathfinder class again.

We want our filter to let through any node that fulfills any of the following requirements:

  1. It is the start node.
  2. It is the end node.
  3. It is a jump node.
  4. It is a first in-air node in a side jump (a node with jump value equal to 3).
  5. It is the landing node (a node that had a non-zero jump value becomes 0).
  6. It is the high point of the jump (the node between moving upwards and and falling downwards).
  7. It is a node that goes around an obstacle.

Here are a couple of illustrations that show which nodes we want to keep. The red numbers show which of the above rules caused the filter to leave the node in the path:

Setting Up the Values

We filter the nodes as they get pushed to mClose list, so that means we'll go from the end node to the start node.

if (mFound)
{
    mClose.Clear();
    var posX = end.x;
    var posY = end.y;

Before we start the filtering process, we need to set up a few variables to keep track of the context of the filtered node:

var fPrevNodeTmp = new PathFinderNodeFast();
var fNodeTmp = nodes[mEndLocation][0];

var fNode = end;
var fPrevNode = end;

fNode and fPrevNode are simple Vector2s, while fNodeTmp and fPrevNodeTmp are the PathFinderNodeFast nodes. We need both; we'll be using Vector2s to get the position of the nodes and PathFinderNodeFast objects to get the parent location, jump value, and everything else we'll need.

 var loc = (fNodeTmp.PY << mGridXLog2) + fNodeTmp.PX;

loc points to the XY position in the grid of the node that will be processed next iteration.

Defining the Loop

Now we can start our loop. We'll keep looping as long we don't get to the start node (at which point, the parent's position is equal to the node's position):

while(fNode.x != fNodeTmp.PX || fNode.y != fNodeTmp.PY)
{

We will need access to the next node as well as the previous one, so let's get it:

var fNextNodeTmp = nodes[loc][fNodeTmp.PZ];

Adding the End Node

Now let's start the filtering process. The start node will get added to the list at the very end, after all other items have been dealt with. Since we're going from the end node, let's be sure to include that one in our final path:

if ((mClose.Count == 0))
    mClose.Add(fNode);

If mClose is empty, that means we haven't pushed any nodes into it yet, which means the currently processed node is the end node, and since we want to include it in the final list, we add it to mClose.

Adding Jump Nodes

For the jump nodes, we'll want to use two conditions. 

The first condition is that the currently processed node's jump value is 0, and the previous node's jump value is not 0:

if ((mClose.Count == 0)
    || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0))
    mClose.Add(fNode);

The second condition is that the jump value is equal to 3. This basically is the first jump-up or first in-air direction change point in a particular jump:

if ((mClose.Count == 0)
    || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0)
    || (fNodeTmp.JumpLength == 3))
    mClose.Add(fNode);

Adding Landing Nodes

Now for the landing nodes:

if ((mClose.Count == 0)
    || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0)
    || (fNodeTmp.JumpLength == 3)
    || (fNextNodeTmp.JumpLength != 0 && fNodeTmp.JumpLength == 0)   )
    mClose.Add(fNode);

We detect the landing node by seeing that the next node is on the ground and the current node isn't. Remember that we are processing the nodes in reversed order, so in fact the landing is detected when the previous node is on the ground and the current isn't.

Adding Highest Point Nodes

Now let's add the jump high points. We detect these by seeing if both the previous and the next nodes are lower than the current node:

if ((mClose.Count == 0)
    || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0)
    || (fNodeTmp.JumpLength == 3)
    || (fNextNodeTmp.JumpLength != 0 && fNodeTmp.JumpLength == 0)
    || (fNode.y > mClose[mClose.Count - 1].y && fNode.y > fNodeTmp.PY))
    mClose.Add(fNode);

Note that, in the last case, we don't compare the current node's y-coordinate to fPrevNode.y, but rather to the previous pushed node's y-coordinate. That's because it may be the case that the previous node is on the same height with the current one, if the character moved to the side to reach it.

Adding Nodes that Go Around Obstacles

Finally, let's take care of the nodes that let us maneuver around the obstacles. If we're next to an obstacle and the previous pushed node isn't aligned with the current one either horizontally or vertically, then we assume that this node will align us with the obstacle and let us move cleanly over it if need be:

if ((mClose.Count == 0)
    || (fNextNodeTmp.JumpLength != 0 && fNodeTmp.JumpLength == 0)
    || (fNodeTmp.JumpLength == 3 && fPrevNodeTmp.JumpLength != 0)
    || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0)
    || (fNode.y > mClose[mClose.Count - 1].y && fNode.y > fNodeTmp.PY)
    || ((mMap.IsGround(fNode.x - 1, fNode.y) || mMap.IsGround(fNode.x + 1, fNode.y)) 
        && fNode.y != mClose[mClose.Count - 1].y && fNode.x != mClose[mClose.Count - 1].x))
    mClose.Add(fNode);

Preparing for the Next Loop

After adding a node to the mClose list or disregarding it, we need to prepare the variables for the next iteration:

    fPrevNode = fNode;
    posX = fNodeTmp.PX;
    posY = fNodeTmp.PY;
    fPrevNodeTmp = fNodeTmp;
    fNodeTmp = fNextNodeTmp;
    loc = (fNodeTmp.PY << mGridXLog2) + fNodeTmp.PX;
    fNode = new Vector2i(posX, posY);
} 

As you can see, we calculate everything in the same way we prepare the loop for the first iteration.

Adding the Start Node

After all the nodes have been processed (and the loop is finished), we can add the start point to the list and finish the job:

    mClose.Add(fNode);

    mStopped = true;

    return mClose;
}

All Together

The whole path filtering procedure should look like this.

if (mFound)
{
    mClose.Clear();
    var posX = end.x;
    var posY = end.y;

    var fPrevNodeTmp = new PathFinderNodeFast();
    var fNodeTmp = nodes[mEndLocation][0];

    var fNode = end;
    var fPrevNode = end;

    var loc = (fNodeTmp.PY << mGridXLog2) + fNodeTmp.PX;

    while (fNode.x != fNodeTmp.PX || fNode.y != fNodeTmp.PY)
    {
        var fNextNodeTmp = nodes[loc][fNodeTmp.PZ];

        if ((mClose.Count == 0)
    		|| (fNextNodeTmp.JumpLength != 0 && fNodeTmp.JumpLength == 0)
    		|| (fNodeTmp.JumpLength == 3 && fPrevNodeTmp.JumpLength != 0)
    		|| (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength != 0)
    		|| (fNode.y > mClose[mClose.Count - 1].y && fNode.y > fNodeTmp.PY)
    		|| ((mMap.IsGround(fNode.x - 1, fNode.y) || mMap.IsGround(fNode.x + 1, fNode.y)) 
    			&& fNode.y != mClose[mClose.Count - 1].y && fNode.x != mClose[mClose.Count - 1].x))
	    	mClose.Add(fNode);

        fPrevNode = fNode;
        posX = fNodeTmp.PX;
        posY = fNodeTmp.PY;
        fPrevNodeTmp = fNodeTmp;
        fNodeTmp = fNextNodeTmp;
        loc = (fNodeTmp.PY << mGridXLog2) + fNodeTmp.PX;
        fNode = new Vector2i(posX, posY);
    }

    mClose.Add(fNode);

    mStopped = true;

    return mClose;
}

Conclusion

The final product of the algorithm is a path found for a character which is one block wide and one block high with a defined maximum jump height. 

We can improve on this: we could allow the character's size to be varied, we could add support for one-way platforms, and we could code an AI bot to follow the path. We will address all of these things in the next parts of the tutorial!

08 Sep 16:15

4 Ways to Teach Your Players How to Play Your Game

by Darran Jamieson

We all hate in-game tutorials. When we buy a game, we want to jump straight into the action, not spend ages reading through menus and flowcharts of moves. But we need to know how to play. We need to understand the new rules of each game—after all, if every game were the same, why would we need other games?

And because computer games are so complex, there's much more to a tutorial than just "use arrow keys and space to shoot". There's how we interact, our objectives, how the world reacts to us: all this needs to be imparted to the player, and preferably without sitting them down and specifically having to say "spikes are bad".

So we want to get our tutorial out of the way as quickly as possible, right? The thing is, the first few minutes of a game can make or break a player's experience. Triple-A games have a little bit more leeway, but if you're making a mobile or web game then you need to get the player into the meat of the game as soon as possible to ensure they're having fun; otherwise, they'll just find something else to play.

OK, so we need to make a tutorial, but we don't want the player to sit through a boring lesson on how the game works... this is a conundrum. The solution lies in how we construct our tutorial: can we make it fun? In fact, can we make it part of the game?

Tutorials can be largely split into three types: non-interactive, interactive, and passive. We'll look at each in turn.

Non-Interactive In-Game Tutorials

Non-interactive in-game tutorials are, in many ways, a leftover of old game design. The image below is from Infected, a game my team and I made several years back. 

Infected tutorial image
The "tutorial" from our game, Infected.

The entire game tutorial is essentially this one image; in retrospect, it was a massive design flaw. When watched people actually play the game, they would hit that screen, their eyes would briefly glaze over, and they would hit Start. For most of our players, they were none the wiser on how the game actually worked.

George Fan, the creator of the fantastic Plants vs Zombies, goes by the rule that "there should be no more than eight words on the screen at any time". Players generally have short attention spans, and are not looking to digest large quantities of information. 

While non-interactive tutorials are not necessarily bad, they nearly always break this eight-word limit. If we were wanted to explore this design flaw, we could sum it up neatly as:

Don't overwhelm the player.

This is really the first rule of in-game tutorial design. Players need to understand what's going on at all times: if you give the player a list of 200 combo moves and special attacks, then chances are they'll remember two or three and use those for the entire game. However, if you “trickle teach” the player—introduce one concept at a time—then they will have plenty of opportunity to get to grips with each ability.

Controller image
We've used this image before, but its still relevant.

We've actually talked briefly about this idea before, under the concept of using achievements as tutorial aids. Forcing the player to complete a level with only a basic weapon might damage the overall “fun level”, but giving players an achievement for doing it makes it optional, and encourages players (especially those who are already competent at the game) to try new strategies and tactics. 

Any sort of rank or reward system can be used in this way, such as a star rating or an A+ to F ranking. “Bad” players can complete the level easily, whereas players who adhere to more difficult challenges are rewarded with higher scores. This also allows more hardcore gamers to aim for 100% completion, while casual gamers can still enjoy the game without getting stuck on a difficult level.

Super Meat Boy spreads its "tutorial" over the first half dozen levels. The first level teaches you the fundamentals: moving left and right and jumping. Level 2 teaches wall jumping. Level 3, sprinting. Once the player has understood these basic concepts, the game starts introducing concepts like spinning blades, disintegrating platforms, and scrolling levels.

super meat boy gameplay image
The first level of Super Meat Boy. Can you handle walking and jumping? Then you're probably good.

The first level of Super Meat Boy, in fact, is incredibly difficult to fail at. The game uses a technique often referred to as a “noob cave”. Essentially, the player starts in a position from which it is impossible to fail—they need to make progress in order to get to a point where they can die. This gives the player a chance to get to grips with the game mechanics, without feeling under threat of enemies attacking or timers running out. 

The “noob cave” technique is something we implemented in Infected to some degree: although it is possible to lose the first level, it requires some effort. The player is given a significant starting advantage (twice as many pieces as the enemy), and the AI is almost non-existent. At high levels, the AI will make calculated moves, but on the first level, the AI will move 100% randomly (within the rules of making a legal move). This means that even if the player has zero idea of how to play, they are still highly likely to win. (Of course some players still managed to lose, but the overall experience was significantly better for our players than our first version, where we simply threw them against an advanced AI that would crush them.)

There are a few ways to implement a "noob cave" in a game, but one effective way is to create an interactive tutorial: a section of the game with locked down mechanics where the player can only perform the actions required to win. This allows the player to "play" the game, without running the risk of losing and getting bored.

Interactive In-Game Tutorials

Allowing player interaction within an in-game tutorial is a good way to teach mechanics. Rather than simply telling the player how the game works, making them perform required actions results in better retention. While you can tell a player the instructions a hundred times, actually getting them to perform game actions will guarantee that they remember and understand.

Highrise heroes gameplay image
Forcing the player through the motions in Highrise Heroes

The above image, from Highrise Heroes, is an excellent demonstration of how to involve a player in a tutorial. Although it would be easier to simply display an image of how making a word works, forcing them to go through the actions of actually completing a word ensures they understand this concept before they proceed. The player is locked out of “full” gameplay until they have demonstrated their ability to complete this basic gameplay element. Because you force the player to perform the action, you can be sure that the player is unable to progress until they have mastered this task.

The only drawback with this style is that, if the player is already familiar with how the game works, they can find playing through obligatory tutorial levels tedious. This can be avoided by letting players skip through the tutorial levels—but be aware that some players will then skip the tutorial regardless of whether they've played the game before. Andy Moore, creator of Steambirds, has talked about how he went from a 60% to a 95% player retention rate after making the tutorial unskippable.

Background In-Game Tutorials

Background in-game tutorials allow the player direct access to gameplay. While the player can still progress through a “noob cave” area, they are able to (hopefully) do it at a faster pace, and thus get into the "proper game" faster.

Here's an example:

VVVVVV gameplay image
In VVVVVV, the player starts here, in a safe area where they can work out the controls.

This is the first screen in VVVVVV, which uses a small pop-up to tell the player how to move left and right. As the player is completely boxed off, the only action they can perform is moving onto the second screen, where they are shown how to “jump” over obstacles. From there, they have essentially mastered gameplay—and although moving and jumping could fit into a single room, spacing them out ensures players have mastered each skill and aren't overwhelmed by masses of text.

The difference between a background and an interactive tutorial can be subtle, as they can both use a similar style. The primary difference is that a background tutorial can be skipped by the player with no effort: everything merely merges into the background. 

I saw her standing there gameplay image
I saw her standing there, a fantastic Flash game. Try it out, and notice that the game gets you playing even before the  main menu has come up.

An interactive or background tutorial is something we should have used in Infected. The entire tutorial could have been taught in three moves (how to move, how to jump, and how to capture), so there wouldn’t have been a significant loss to gameplay if we'd implemented it. Although we were aware of the issue, the tutorial was one of the last things we developed, so good design took second place to just getting the game finished.

No In-Game Tutorial

How do you get cold water from a faucet? Generally, you turn the tap handle to the right. How do you tighten a screw? You turn it clockwise. These things don't come with instruction manuals—we instinctively know how they work. Or rather, we learn how they work at an early age, and since they (generally) all work in the same way, that knowledge is reinforced throughout our lives.

Games operate on this principle as well. As veteran gamers, we automatically know that we want to collect coins and gain upgrades. Non-gamers might not understand these things automatically, so it's important to make these things as obvious as possible. (Even if you haven't played a platform game before, it's fairly obvious that jumping into fire is not a good idea.) 

A player should generally be able to identify the difference between “good” and “bad” objects at a glance, without having to use death as a trial and error discovery method. Shigeru Miyamoto once talked about how he decided why coins specifically were used in Mario:

"Thus, when we were thinking about something that anybody would look at and go 'I definitely want that!', we thought, 'Yep, it's gotta be money.'" 

Plants vs Zombies' use of "plant" and "zombie" themes helps teach the players without any explanation at all: players know that plants don't move around, and that zombies are slow moving. Rather than going for a boring "turrets vs soliders" theme, the Plants vs Zombies theme allows the game to be interesting and cutesy, and still impart vital knowledge.

Plants vs Zombies image
Honestly, it's really not that hard to work out the basics of whats going on here.

Since players will often “know” how a game plays, we don't always have to explain everything. When you throw a player into a game, consider telling them nothing—let them figure things out for themselves. If they stand around motionless, then give them a few hints ("try moving the control stick to walk!"), but  remember that players will generally try to perform basic commands themselves. Because the “basic rules” of gameplay tend to be universal, this means we can assume the player has some familiarity with them; however, it also means that it's incredibly dangerous to make changes to those basic rules.

Some fledgling games designers decide that doing things “the normal way” is the wrong way, and ignore established rules. The real-time strategy (RTS) genre has suffered from this in the past: while today's games tend to use a fairly standardised control system (left click to select, right click to move or attack), older games had very little consistency, and would often switch the left/right mouse button controls, or try to bind multiple commands to a single button. If you play one of these old games today, then the bizarre controls can be very jarring, as we've since learned a different control set.

Implementing "obvious" controls was the saving grace of Infected: while the player may not have read the tutorial, clicking on a piece immediately highlighted moves the player could make. By giving the player a visual response that clicking on a piece was the "right" move, it let them continue exploring the control system. Our initial versions of the game did not have this highlighting, and we found that adding this minor graphical addition made gameplay much more accessible and obvious, without taking anything anything away.

Infected gameplay image
In Infected, after you select a piece, highlighted squares show you where you can move.

If you want to change traditional gameplay elements around, have a good reason. Katamari Damacy uses both thumbsticks to move, rather than the more traditional setup of using one thumbstick to move and the other to control the camera. While this may cause some intial confusion, the simplicity of the game means that this control system works exceptionally well. 

In a similar vein, the web game Karoshi Suicide Salaryman actually demands that the player kill themselves, often by by jumping on spikes. While this is "non-standard" game behaviour due of the game's reversed theme (die to win), the player's objectives are always clear. 

Games will always change and evolve, but it's important to understand that when you change things—be it controls or game objectives—the player should not have to relearn everything.

Continual Learning and Experimenting

Its also interesting to note that not providing the player with explicit instructions can actually encourage gameplay through experimentation. In the Zelda series, the player is constantly finding new items and equipment, and must learn how they work. Rather than sitting through a lengthy explanation of “the hookshot: learning how to use your new weapon”, the game just dumps the player in a closed room and says “figure it out”—although the "puzzle" is generally so obvious that the player should be able to work things out instantly.

These rooms are basically noob caves: despite being found halfway through the game, they allow the player to explore how their new toy works within a safe environment. Once the player has worked out the intricacies of their new toy, they are thrown back into the game world, where they can continue puzzling and fighting monsters. By the end of the game, the player is so used to using their new weapons that switching between them to solve multi-tiered puzzles is second nature.

The way Zelda games handle in-game tutorials is also worth noting: for Zelda, the game is the tutorial. With perhaps the exception of the final dungeon, the player never really stops learning; this “trickle teaching” is one of the greatest strengths of the Zelda series. Rather than being given everything at the beginning, the player slowly unlocks the game, so they are never overwhelmed and are always unlocking cool new toys to use.

Plants vs Zombies, again, also uses trickle teaching effectively: in every level, the player unlocks a new plant (and sometimes new stages), and must learn how to use these to defeat the zombie army. The game never overwhelms the player, but always gives them something new to play with. Because the player gets to spend time with each weapon, it encourages the player to select the plants that are most effective, rather than finding a few plants they like at the start and sticking with them throughout the whole game.

Don't Scare the Player Away

All of this really just says: don't scare the player away, and don't bore them away either. It seems like such obvious advice, but it's remarkable how few games (including triple-A games) seem to be unable to understand this.

One common example of this, a mistake made time and time again, is requiring registration to play online. If you're trying to get a player hooked, don't make them wade through forms filling out their date of birth, their email address, and so on—just give them a guest account and let them play. If they enjoy the game, then they're more likely to sign up for a “full” account. (Tagpro is a online, multiplayer game which does this fairly well: select a server, hit Play as Guest and you're in.)

It's fine to make complex games, but realise that humans have poor memories and short attention spans, and do not learn well by being presented with masses of text. If you've not played them before, try playing a game like Crusader Kings, Europa Universalis, Dwarf Fortress, or even Civilisation. If you haven't been show how to play by someone else, it can be quite daunting to learn how the game actually works. And while these are all fantastic games, they will always have a certain “niche” quality—not because they have learning curves, but because they have learning cliffs, impassible to all but the most determined.

Conclusion

Remember: try make your tutorials fun, rather than a tedious slog. Every game is different, and it might be difficult to implement all of these ideas within a particular genre—but if you can make the first five minutes fun, you can probably hook the player to the end credits.

References

08 Sep 16:14

How to train players right, so they don't hate learning to play

Designer Mike Stout takes a look at examples of in-game training that skip the boring tutorials and teach players the rules of the game by letting them play the game. ...

01 Sep 00:29

Shovel Knight: Plague of Shadows mobility design

A detailed tour through the design of Shovel Knight's all-new playable character, Plague Knight, and how his completely different movement style posed both challenges and opportunities. ...

20 Aug 00:24

Everything I learned about dual-stick shooter controls

"Dual-stick shooter controls should be a completely solved problem by now, shouldn't they? I was very surprised to realize that I'd bang my head against walls to get everything up to an acceptable level of quality." ...

17 Aug 20:53

21 free learning resources for game developers

"To prepare this list, I dug through numerous websites. I drew from countless hours spent looking for resources in the past to bring you the best, free online resources I could find and be recommended." ...

07 Aug 00:14

Incorporating level design in melee combat systems

Combat developers on games including Heavenly Sword, God of War III, DmC: Devil May Cry, The Last of Us, Aztez, Dead Island 2, Killzone and Watchdogs offer analysis and tips. ...

05 Aug 12:44

Gameplay vs. story: How to strike the balance you want

Experienced narrative designer Evan Skolnick (Star Wars: Battlefront, Dying Light) explores how different games balance gameplay and story, and offers a design framework for making your own choice. ...

15 Jul 01:55

125 things I learned while developing games

A long list of short wisdom: "I'm not that good at making constructive stories, so I'll provide you with a handy bullet-list about stuff I've learned over the past few years." ...

06 Jul 22:28

Programmer, Interrupted

Strategies for avoiding interrupted coding sessions. ...

06 Jul 22:19

A Game Studio in the Clouds

How does an indie studio come together -- and ship games together -- if they're not located in the same city? Emeric Thoa, former Ubisoft developer and current creative director of The Game Bakers, explains what tech and techniques can make it work. The last time I had a second of free time was over the Christmas holidays and I used that free time to write a paper about our experience making Squids and the ...

06 Jul 22:10

Next-gen cel shading in Unity 5

"With the arrival of Unity 5, it's never been so easy to get high quality visuals in our game. In a small amount of time we 'hacked' the new Unity Deferred pipeline to completely change our visual style." ...

25 Jun 21:05

Unleashing the power of small teams

When set up appropriately, small game teams can move with flexibility and speed that's impossible for large teams. ...

18 Jun 02:07

11 tips to speed up your game design process

"At the start of a project, everything has yet to be done. There are loads of tasks at hand, and it is hard to tackle them in the right order. The large, almighty Game Design Document is a myth." ...

17 Jun 20:45

Video: Deconstructing the animation of Ori and the Blind Forest

James Benson takes the stage at GDC 2015 to break down how he went about animating Moon Studios' Ori & The Blind Forest in a Ghibli-esque style with limited time, people and personal experience. ...

17 Jun 20:37

Choice fields: Designing for emergent gameplay

"We need to think about how to create the overlapping sets of rules and abilities that create the environment from which emergence springs." ...

12 Jun 17:56

Thoughts on the design of Dark Souls

"I've wanted to write about Dark Souls for a while - in part because of the strong feelings I have about the game, but mostly because people aren't talking about the things that seem the most prominent to me." ...

12 Jun 17:55

Don't Miss: Naughty Dog's Uncharted 2 enemy AI design secrets

In this timeless 2010 feature Naughty Dog combat designer Benson Russell delves into the techniques the studio devised for its combat AI, and how those evolved from the original Uncharted game. ...

12 Jun 17:55

Four-step puzzle design

"Good puzzle design gives a player that moment of epiphany, where suddenly all is clear, and the following satisfaction when your put your solution in place, and it works!" How do you achieve it? Here's a guide. ...

12 Jun 17:55

Exploring games from the creator's perspective

Tools for taking a critical eye to games: "An analysis can be broad or focused. It can take a little or a lot of time. But in any case, it should answer your questions, your needs." ...

12 Jun 17:55

Don't Miss: A behind-the-scenes look at sound design for Journey

"There are too many stories in these sounds for the world to never know," wrote Journey sound designer Steve Johnson in this timeless 2012 feature. "So I thought I'd share them with you." ...

12 Jun 17:55

An approach to balancing game economies using spreadsheets

"At a high level, these are the techniques I use in Excel to accomplish this common task in a way that keeps my economy organized and ensures that it is easy to maintain and update." ...

16 Mar 19:22

Don't Miss: The fundamental pillars of a good combat system

"With the help of other designers, I have decided to gather and formalize what knowledge I can on the fundamental rules in designing a combat system. This article is the result of that exercise." ...

27 Feb 09:00

Water interaction model for boats in video games

Avalanche Studios senior software engineer Jacques Kerner (Just Cause 3) walks us through coding "a simplified model that captures the important features of a boat" in this meaty programming article. ...

08 Apr 04:00

The Designer's Notebook: Three Problems for Interactive Storytellers, Resolved

A deep look at the challenges facing game narrative, with potential solutions. ...