Make A 2D Game With Unity3D Using Only Free Tools: Beginning Enemy AI With A* Pathfinding

Make a 2d game in unity3d part 5In This installment of our 2D tutorial series, we will be adding enemy AI to our Lode Runner clone using A* Pathfinding. This post has been guest written by Adrian Seeto of Fun Mob Games who was also kind enough to write the AI scripts that are a huge part of bringing the game to life.

Adding the enemy AI required changes to many of the scripts that we added in the previous tutorials. You can download the complete project here which has been rebuilt to support the new prefab system in Unity 3.5x. If you’re using an older version of Unity (pre 3.5), then you should download this version. You can also follow the steps below to update your existing project. I recommend following along with the tutorial as there’s tons of great information on implementing the AI with A*. You can also play the game as it will be at the end of this tutorial here.

Installing the A* Package

  1. Import the free version of Aron Granberg’s A* Pathfinding Project into Unity3d
  2. Create an empty GameObject and rename it A*. Make sure it’s positioned at 0,0,0
  3. Drag the /AstarPathfindingProject/Core/AstarPath.cs file onto the A* GameObject
  4. Click on the A* GameObject and disable the “Allow Javascript” option

Importing game C# scripts

Many of the scripts have been modified since Part 4 of this series, so you’ll need to update them. If you’ve made changes to your own scripts then you’ll need to merge your changes into these new scripts.

  1. Copy these scripts into your Assets/Scripts/ folder:
    1. Player.cs (updated)
    2. PlayerAnims.cs (updated)
    3. xa.cs (updated)
    4. ChangeBehaviorGUI.cs (new)
    5. Character.cs (new)
    6. Enemy.cs (new)
  2. Copy this script into your AstarPathfindingProject/Editor/GraphEditors folder:
    1. MyGridGeneratorEditor.cs
  3. And finally copy this script into your AstarPathfindingProject/Generators folder:
    1. MyGridGraph.cs

Configure The xa.cs Script

Create a new Layer by going to Edit –> Project Settings –> Tags and then enter “Enemy” in one of the empty User Layer slots.

  1. Select the Global/Scripts/xa GameObject
  2. Set Ground Mask field to “Ground
  3. Set Ladder and Rope Mask field to “NoDraw
  4. Set Enemy Mask field to “Enemy

Miscellaneous Project Changes

Note that some of these may be unnecessary or redundant depending on how your project is currently setup.

  1. Expand the Global game object in the Hierarchy and then select the “border bottom” and “border top” GameObjects and set their layer to Ground.  This is so enemies can stand on it if/when we dig a trap above the bottom border and our enemies fall into it they won’t fall continue through the border into the great abyss.
  2. Select the player object in the Hierarchy and then change its Tag to “Player
  3. Select the OT/View object in the Hierarchy and uncheck “Always Pixel Perfect“. Note I’m not sure why Adrian turns off pixel perfect, feel free experiment on your own.

Configure A* Pathfinding For Our Level

A* is a graph traversal algorithm widely used in computer science, and commonly used in games for path finding.  A full discussion of A* is beyond the scope of this tutorial, but there is some terminology you should be aware of.  A graph consists of nodes and the network of connections between them.  Once A* has been provided with a graph, you can query it to find the shortest path from one node to another.  A* will return a path way list of all the nodes that you must travel along to get from your start node to your end node, in the shortest path possible.  

More accurately, the path returned is the “least cost” path, because nodes have a specified penalty cost for traveling along it.  A* will attempt to create a path which incurs the least cost.  The unit of a penalty is up to the application developer.  For example, in an isometric RTS game the terrain tiles would be nodes, with 8 connections per node (except for the boundary tiles).  We could have the penalty cost units be related to the time it takes to travel on it.   A swampy area might therefore have very high penalties on the nodes in it, while the grass land nodes surrounding the swamp have smaller penalties.  A computer-controlled tank which wants to get from one side of the swamp to the other may then plot a course around the swamp if it costs less, even though it is physically further (i.e. more tiles must be travelled, but the total travel duration is less) . Of course, it is up to us as the game developer to implement all the code to actually slow the tank down if/when it travels over swamps, A* is only the pathfinder.

We don’t make use of penalties in this game because traveling along ropes, ladders, the ground, and falling is all done at the same speed.  If gravity in our game was a useful non-lethal force (i.e. falling is faster than climbing and just as safe) then we can set node penalties so that A* would return a path where jumping off a platform would be a preferable shortcut to climbing down the ladder. If your game enforces lethal falls from great heights, then you would need additional logic.

A game could have multiple graphs, for use by the various units (ships have different travel behavior to an infantry man for example).  For this tutorial, we only need to create one graph.  In the graph one node will be created for every tile in a 2d grid-like layout.  Each node may have up to 4 connections (up, down, left, right).  Aron’s A* Project includes several different path graph generators, such as GridGraph, ListGraph, and NavMeshGraph.  The GridGraph is a suitable starting point for our game.

The node connections generated by the base GridGraph are always bidirectional (if it creates a connection from from node1 to node2 then it will also create a connection from node2 to node1) which is not necessarily what we want.  Bidirectional paths are fine for ladders, ropes, and the ground because you can go up and down a ladder, and both left-to-right and right-to-left on rope and ground.  The problem arises when you want to include fall lanes. A fall lane is a term I’ve made up for a vertical one-way path that you can only travel on by falling.  Fall lanes exist under the ropes (you can let go of a rope and fall) and off of cliff edges (you can run off a cliff and fall down to a lower platform).  Once on a fall-lane you can’t travel in any direction but down until you’ve hit bottom.  This is a uni-directional connection so the nodes on a fall lane only have one connection each: a connection to the node directly below it.  Other nodes in the level will have between 2 and 4 connections each.  

We could use the base GridGraph and not bother about having any fall lanes, it would just mean that our AI would not plan any paths that involve letting go of ropes or running off platforms.  In this scenario you could still have your AI let go of ropes in a “reactionary” manner i.e. if it’s already on the rope and see a player below it, it could let go.  You would do this will “special case” code in your enemy AI loop.  However, its important to understand this is different from forward planning a route which takes into account the ability to jump off ropes.  In order to that kind of path planning with A*, you need to provide that information in the graph, via the fall lane concept.  

I’ve created a new generator class called “MyGridGraph” which inherits from GridGraph and overrides the Scan() and IsValidConnection() functions.  Our new MyGridGraph class implements the fall lanes idea so let’s add it the scene.

  1. Select the A* GameObject
  2. Click the Graphs -> Add Graph button
    1. Add a new “My Grid Graph
    2. Width: 26 (these dimensions match our level)
    3. Depth: 20
    4. Node Size: 1
    5. Aspect Ratio: 1
    6. Center: 0, 0.3, 0.01 (y is 0.3 so that our nodes are centered in the middle of each tile (which we already previously aligned at 0.3), the z is 0.01 because the integer coordinates used for Aron’s A* node positions have a fixed precision of 0.01 and anything smaller than that would cause troubles.)
    7. Rotation: -90, 0, 0 (rotate x because our grid is for a side-on view rather than the expected top-down view)
    8. Disable Cut Corners checkbox
    9. Connections: Six (we only need 4 connections per node up/down/left/right, but we don’t have that option)
    10. Disable Collision testing checkbox
    11. Disable Height testing checkbox
    12. Level Layers: Ground and NoDraw
    13. Scroll to the bottom and enable the Show Graphs checkbox then hit Scan

If it worked right you should now see the A* graph layout in your game/scene window (enable Gizmos) similar to the following image (click to see a larger version).

2dGamePt5 pathfinding

The red squares represent nodes that the AI can not travel on.  All the bricks should have a red node centered on them.  Much of the black “air” tiles should also have a red square gizmo.  The tiles without the red gizmos are the nodes the AI can travel on.  The blue line represents the connections between nodes. You should therefore see that the AI can travel along the surface of ground tiles, along ropes, and up and down ladders.  You should also see the vertical fall lanes under ropes and over the edge of a platform.  If you are very perceptive you will see that the blue lines come in 2 thicknesses.  A thick line means that the connection is bidirectional (the line gizmo was rendered twice, once each way, making it thicker).  A thin line represents a unidirectional connection.

Making Enemies

Our enemies and the player classes both derive from the Character class which implements the movement code.  In this way we can ensure that the enemies and player move with the same constraints to help prevent giving either an unfair advantage, and just general time-saving and efficiency.  The character class does have a movement speed variable, so we can give the enemies different movement speeds than the player, if desired.

Below we create 2 enemy game objects.  I’ve just reused the player sprite for them, and I’ve given each one a different tint so I can tell them apart (helpful during the development stage). If you create your own different enemy sprite go ahead and use it.

  1. Create an empty GameObject called Enemies to use as an organizational holder.
  2. Clone the player GameObject in the hierarchy and rename it to enemy-1.
  3. Drag the enemy-1 to child it under your Enemies GameObject.
  4. Drag the /AstarPathfindingProject/Core/Seeker.cs script onto your enemy-1.  The script’s defaults are fine.  Go ahead and break enemy-1’s connection to the player prefab when prompted.  The Seeker provides the enemy with the ability to use A* path finding.  It has gizmos which paint green lines representing the found path.
  5. Expand the enemy-1 GameObject to see it’s children.
  6. Delete the “shoot parent” child.
  7. Add an empty GameObject child under enemy-1 named “path target”.  Local Position 0,0,0 Local Scale 1,1,1. This will be a transform that the enemy script will move around to place on the desired target for our path finding AI.  Useful during debugging to see what it is targeting.
  8. Duplicate “path target” and rename to “tile bounds”.  Add a box collider to it, set to Trigger and centered at 0,0,1 and scale 1,1,1.  This is so that when we trap an enemy we can walk smoothly on top of it’s head as if it was ground.
  9. Select the enemy-1 game object and set its Layer to Enemy and apply to children.  Set its Tag to Enemy (leave children untagged).
  10. Expand the enemy-1 Player script component.  Drag the /Scripts/Enemy.cs script onto the Player Script field to replace it with the Enemy script.
  11. The Enemy script has 3 slots that need to be filled: Player and Player Tr (drag the player gameobject to those slots), and Target (drag the path target gameobject to this slot).
  12. Expand the enemy-1 OTAnimatingSprite script.  Set the Material Reference slot to “tint”.  Set the Tint Color to red (or your preference) to visually differentiate it from the player.
  13. Clone enemy-1 as enemy-2 and give it a green tint. (You may have to redo the Red tint on the other enemy due to an OT quirk).
  14. Drag the enemies to suitable spawn points in the scene.
  15. Optional: Select the Enemies root GameObject.  Drag the ChangeBehaviors.cs script onto it.  Set the Enemies array size to 2, and drag enemy-1 and enemy-2 into the slots.  This script will allow you to modify an enemy’s behavior on-the-fly via GUI between a “plain A* to Player” and a “Lode Runner-esque” behavior.  The A* to Player behavior simply follows the shortest path toward the player’s exact position.  The Lode Runner-esque behavior still uses A* but also tries to reimplement some of the reactionary quirks of the original game, especially with regards to ladder use.  See the Enemy.cs file for details.  You can disable/remove the ChangeBehaviors.cs script when you are done testing it out.

Conclusion

While the enemy AI is not a completely faithful recreation of the original, it is hoped that it will provide you with enough of an idea on how you can use and implement A* path finding in your own creations.

If you have any questions about the tutorial, please let us know in the comments below. Your support helps to keep these tutorial coming so be sure to follow us on Twitter and Facebook. This blog post is part of iDevBlogADay, a collection of indie developers writing about their development experiences.

More Tutorials In This Series

Make A 2D Game in Unity3D Using Only Free Tools Part 1
Make A 2D Game in Unity3D Using Only Free Tools Part 2
Make A 2D Game With Unity3D Using Only Free Tools Part 3
Make A 2D Game With Unity3D Using Only Free Tools Part 4
Make A 2D Game With Unity3D Using Only Free Tools: Beginning Enemy AI With A* Pathfinding

And here’s another tutorial series that uses Sprite Manager 2 for the sprite display and animation duties:
Creating 2D Games with Unity3D Part 1
Creating 2D Games with Unity3D Part 2
Creating 2D Games with Unity3D Part 3
Creating 2D Games with Unity3D Part 5

Show Your Support

If you find these tutorials useful, please considering buying one or more of our games and apps for iPhone and iPad. Your support helps to keep the tutorials flowing!

  • Giant Moto: High flying arcade style motocross action!
  • Small Space: Fly a spaceship, eat space creatures, score point!
  • Holeshot Drag Racing: High speed drag racing action!
  • Alien Booth: Transform pictures of your friends into awesome aliens!
  • iSpoof Walken: A hilarious look into the world of Christopher Walken!
  • Gaga Eyes: Transform pictures of your friends to give them huge anime eyes!

About the author

The AI scripts and this tutorial were contributed by Adrian Seeto of Fun Mob Games, creators of Pocket BMX game for iOS, a free-roaming 2D bike tricking game with physics.  You can check them out at FunMobGames.com or on twitter @FunMobGames.

62 thoughts on “Make A 2D Game With Unity3D Using Only Free Tools: Beginning Enemy AI With A* Pathfinding

  1. Pingback: Tutorial: 2D Game – Enemy AI mit A Pathfinding - Unity News

  2. WyrmTale Games

    Regarding the remark under ‘Miscellaneous Project Changes’ nr 3.

    >> Note I’m not sure why Adrian turns off pixel perfect

    I will explain what the OT.view.alwaysPixelPerfect does.

    When OT.view.alwaysPixelPerfect is set to true, Orthello will keep your sprites always pixel perfect independend of your current device resolution. This means that a sprite of 30×30 pixels will be 30×30 pixels on a 320x200px device and 30×30 pixels on a 1024×768 device.

    Your ‘playing’ world will be smaller on a 320×200 device and a lot bigger on a 1024×768 device.

    If you would like to have your game look the same ( auto zoom/sizing ) on each device, you would turn off the OT.view.alwaysPixelPerfect setting and orthello will adjust the camera’s zoom factor so auto sizing will take place automaticly. the OT.view.pixelPerfectResolution will determine on which device resolution, your sprites will be really pixel perfect. On other resolutions Orthello will zoom in or out.

    Hope this makes any sense …

  3. Tachyon

    Hi, just a quick correction for configuring the xa.cs script:

    “Select the Global/Scripts/xa GameObject”This should be Global/Scripts GameObject or:Global/Scripts/xa Component.A minor error but makes a big difference for noobs like me who are trying to learn 😛

  4. JJ

    NullReferenceException
    UnityEngine.Transform.set_localScale (Vector3 value) (at C:/BuildAgent/work/b0bcff80449a48aa/Runtime/ExportGenerated/Editor/UnityEngineTransform.cs:79)
    Player+c__IteratorC.MoveNext () (at Assets/Scripts/Player.cs:81)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    Player:Update() (at Assets/Scripts/Player.cs:59)

  5. JJ

    It happens when I press space. What’s weird is it only throwing that error on the first two fires. After that, it’s normal, it behaves exactly the same on your web demo.

  6. Tim Miller

    Sounds like the transform isn’t being cached before it’s accessed (like in Awake) and then it is being cached when you press Space so then it works after that.

  7. Mfs780

    Hi, anytime I download one of your projects from the links at the end of the tutorial and run it some of the sprites don’t show the correct texture. All the settings in the inspector are correct but the sprite shows the entire sprite sheet instead of the correct texture. Also once this starts to occur any time I copy a brick that shows the correct texture the new copy shows the full sprite sheet texture again.

    Also as a added note I downloaded your final project 4 and despite the texture errors when I play the game the player character drops through the level.
    I am using Unity 3.5.2f2

  8. Tim Miller

    Hi, yes we found a solution.  The problem was that the prefab system changed in unity 3.5 and Orthello had to be updated to support the new prefabs and then the project had to be recreated with all new prefabs.

    You can download the updated project here: http://rocket5studios.com/files/Rocket5_2DGame_part5_unity35x.zip

    Please let me know if this fixes the problem for you, if so then I’ll update the blog post to include the new link.

  9. Pingback: Creating 2D Games With Unity3D Part 4 | Rocket 5 Studios

  10. Pingback: Creating 2D Games with Unity3D Part 5 Complex Collision | Rocket 5 Studios

  11. Pingback: Creating 2D Games With Unity3D Part 2 | Rocket 5 Studios

  12. bluecollarartist

    This seems to just not work at all with the current version of A*. “Assets/AstarPathfindingProject/Generators/MyGridGraph.cs(261,27): error CS0246: The type or namespace name `GridNode’ could not be found. Are you missing a using directive or an assembly reference?”
    One difference is in my “AstarPathfindingProject/Generators” folder, I actually have a subfolder called “NodeClasses” that has a “GridNode.cs” file in it. That folder isn’t present in your project. Any help would be great. If you want to use e-mail instead, you can reach me at l3reakmanx@gmail.com

    I hope you do! It would be great to keep this updated since the first 4 steps were great! But then, this one just doesn’t work at all. :(

  13. mich

    i had the same problem, it was fixed by downloading the older version of arongranberg A* unitypackage

  14. wyrmtale games

    If anyone has problems with the player character snapping back into place and not moving, check my comment on part 4 .. as soon as Tim updates the source it will be a problem no longer

  15. plaw

    I can’t find:
    – Component -> Pathfinding
    – Graphs -> Add Graph

    Are these only available in Unity Pro? I don’t have Pro :(

  16. plaw

    I can import the A* Pathfinding into a new project, and then these appear, but not in my LoadRunner project. Really don’t want to start from the beginning again :(

  17. Michael Scott

    Hey man, really excellent tutorials. Really helped get me familiar with the 2D assets available with Unity.

    Quick question: any idea when you’re going to add a tutorial for breaking the brick blocks? Would love to know how to work out that interaction. Thanks again; take care.

  18. Michael Scott

    Hey Tim, just wanted to say again how much I loved your tutorials.

    I actually wrote scripts to break the bricks and have them reform and whatnot, and I’m working on doing the death scripts and the exit level scripts. I was wondering if you’d like to collaborate on the next tutorial?

  19. Michael Scott

    Just some comments on your scripts:

    The “isLeft, isRight, isUp, etc.” booleans are really dangerous and not a very good way to handle the state of the player. The states are mutually exclusive, but the booleans are not–you could in theory accidentally set more than one to true.

    A better idea would be to use an enum, like..

    enum FacingDir { LEFT, RIGHT, UP, DOWN }

    This is also true for the int facingDir.

    Rather than using a number, you can use a word that everyone recognizes, and the states are automatically mutually exclusive.

  20. plaw

    Tried to follow your comment on Part 4, however, the scripts have changed in Part 5 and I can’t find where/ what to change.

    So now I’m stuck with the player character snapping back into place. Can anyone help?

  21. wyrmtale games

    Use sprite.position instead of transform.Translate or set OT.dirtyChecks to true when the game starts

  22. plaw

    Hello again!

    I have two new problems, currently, hoping you can help/ push me in the right direction to find a solution.

    1- When pressing space to shoot, the player becomes stuck. I can see in the inspector that “shooting” remains ticked after pressing space.

    2- The enemy AI is a bit poor, I’m assuming because I’ve done something wrong. It quite often stands on the sport spinning round or stuck against a wall. My nodes don’t look exactly the same as in the tutorial (see here http://screencast.com/t/gEah4dF4), is this anything to do with it ?

    Here’s my version so far (I added a main menu):
    http://enigma23.co.uk/Unity/WebPlayer.html

  23. makeupsomething

    I had the same problem. Make sure that your shoot parent object is called “shoot parent” (without quotes) as it is called in the tutorial. Otherwise change the code in Player.cs to match whatever you have called your shoot parent. The line you need to change is

    shootParent = transform.Find(“shoot parent”);

  24. ChiefPotato

    This tutorial is great but the project doesn’t work at all anymore once one adds in the newest version of Aron’s A* or the newest version of Orthello. Will you update it for one of these or both? That’d be super nice =)

  25. ChiefPotato

    This tutorial is great but the project doesn’t work at all anymore once one adds in the newest version of Aron’s A* or the newest version of Orthello. Will you update it for one of these or both? That’d be super nice =)

  26. Grigory Kireyev

    The same thing when i try to shoot.

    NullReferenceException

    UnityEngine.Transform.set_localScale (Vector3 value) (at C:/BuildAgent/work/812c4f5049264fad/Runtime/ExportGenerated/Editor/
    UnityEngineTransform.cs:79)

    It continues infinitely, non interruptable, and works when a hero is on the ladder and moving up and down.

    I’m not a programmer (i’m game designer) and i can’t imagine what it is…

    Unity 4 / Latest Orthello etc…

  27. Austin Felipe

    Hi! First, thanks for share this tutorial with us and make my life much more simple =D… I have one question and I hope you can share this information… We are working with 30×30 sprites, so I set 800×600 in Pixel Perfect Resolution and change the custom size to 10, right? And when I add a new sprite, I can change the scale to 1 x 1 x 1 and the camera view will work fine… Ok, but, if I have a 32×32 sprite? What is the math for this?

    Thanks for support!

  28. Evandro Abu Kamel

    I am having the following error messages and my enemies are falling through the objects.

    IndexOutOfRangeException: Array index is out of range.
    Enemy.Repath () (at Assets/Scripts/Enemy.cs:166)
    Enemy.Start () (at Assets/Scripts/Enemy.cs:88)

  29. Tim Garthwaite

    Any luck with this? My enemies aren’t falling through objects – they simply aren’t moving at all. But, the same error on the same line, and the same behavior if the Level Layers field – it reverts to Nothing every time I start the game.

  30. Tim Garthwaite

    Are you using the new versions of Unity, A*, and Orthello? I can’t seem to get the Level Layers field to work…

  31. Tim Garthwaite

    isUp, etc. are not used to determine which direction the character is facing – that is determined in logic later on when updating the movement. They simply determine which buttons the player has pressed – intent, as it were. Better names for these might be pressedUp or upIntent or something like that.

  32. Tim Garthwaite

    I much prefer Jan-Hendrik’s solution. Fight through the differences in the versions, and learn, instead of copping out and simply copying things that you don’t understand!

  33. Tim Garthwaite

    Hi Tim, it seems as though your logic for applying the Level Layers field value to the graph, or perhaps the serialization code in the graph itself, no longer works with newer versions of A*. As I’m still learning here, it would be great if you could offer some advice on what’s changed in A*. Thanks!

Comments are closed.