Written by: Melanie Leemans
Implementing AI into your game using Unity can be a daunting thought, but should it be? Lets take a look at what tools unity comes with to get this job done like a pro!
The amount of code and knowledge that a developer needs to get AI working is for experts only, and there is no way a novice can even think about implementing any intelligent AI system. MYTH BUSTED!!!!
The Navmesh tool in unity makes it as easy as stealing code from an expert ;). With the right information, blogs, tutorials and Unity’s documentation, a newbie can get AI implemented in a few hours. Let’s take a look at how!
- Create new Unity 3D Project
- Create an environment of your choice. Can be either just spheres, planes, cubes etc or you can set up a more interesting game scene.
- Move the environmental objects into an empty parent container, IE your planes(ground), cubes(walls), trees, rocks, etc. Anything that is environmental and doesn’t move as in static.
- Select the parent container, and tick static in the inspector
- Click yes to apply to children
- Click on Window, AI, navigation
- Choose the navigation panel (depends on how your unity window layout looks on where the navigation panel will be situated)
- Click Bake
I had to change my agent Radius here to 0.1 due to the static Game Objects I was using. The navmesh colliders were not covering the rocks entirely.
- The baking of all static objects will now take place, and depending on how many items are set to static will depend on how long it will take.
- Select your 2 characters (Mine is an alien type character)
- Add a component called Nav Mesh Agent
- I left all settings as default for now
- Create a new script and name it to something according to your naming standards
- This script will be used to enable the AI properties and behaviours that each character will be controlled by.
- Add the script to each character, so in my case, the Hero and the Enemy
Firstly I created a few private variables and made them Serialized Fields
Difference between making variables Serialized private compared to public is : The SerializeField makes the variable display in the inspector so that it can be edited within Unity editor but does not make it accessible by other scripts, I.E. keeping its private variable function but displaying in inspector like a public variable. A public variable is accessible from the editor, and other scripts can access it publicly.*
These variables are as follows:
So that each variable group is neatly listed in the editor I have put a heading above each
private NavMeshAgent m_NavMeshAgent; //This is populated with the navmesh component of the game object.
private GameObject m_Target; //The target you want to follow/find/attack. This variable will be populated with my enemy Game Object in the editor
private Animation m_TargetAnimation; //My character has various animations, so to access these animations I created this variable
private float m_DetectionRange; //This is the range that my hero and enemy need to be in before they actually start “looking” for each other. So if I set this value to 100 and they are 110 distance apart, then they won’t try and find each other, staying at their basecamps
private float m_DistanctToTarget; //I am using this variable to see in realtime what the distance is between the characters when the scene is played. This is so that I can tweak my stopping distance and weapon throw distance in the future functions
private GameObject m_HomeBase; //This will be populated with the Hero’s Home base Game Objects and the Enemies Home base object. The purpose of the variable is so that each character knows where to retreat to when health is below the health threshold.
[Space(8)] //Space between the next group in the editor
private float m_Health = 20f; //This is each characters health, and can be adjusted in the editor per character. I just set it to 20 as default for both my Hero and Enemy
private float m_RetreatHealth = 5f; //This is each characters health threshold, and can be adjusted in the editor per character. I set both my characters to 5. If the characters health gets to 5 or below it retreats to its Homebase.
private GameObject m_BulletPrefab; //This is the Game Object that gets instantiated on attack. In this case the bulletPrefab
private float m_WeaponThrowRange; //The character needs to know when to start the weapon attack, so I use this as the range within the BulletAttack() function
private float m_BulletAttackRate = 2; //How often should the weapons fire. This can be adjusted per character, and I have set it to every 2 “seconds” for now.
private float m_BulletAttackTracker = 0; //This variable has a timer functionality, and deltaTime gets added to this variable in the BulletAttack() function. As soon as m_BulletAttackTracker is equal or greater than m_BulletAttackRate(currently 2), it will reset the value for m_BulletAttackTracker timer back to 0.
Now that I have the variables that I think I need for now, I decided to tackle the navigation first. So getting the characters to “track” each other using the animations I have on these characters.
- I start by setting the characters animation to walk in the Start() Function, so that as soon as the scene starts the characters change their animation from Idle to Walk.
Let’s start tracking the characters.
- In the update function I need to know how far the characters are from each other to start off with.
- I create a new Function called DistanceDetection()
- With the method in Vector3 called Distance(Vector3.Distance), it calculates the distance between point a and point b for you, no manual maths, or equations needed!!
- The value is going to be saved in the m_DistanceToTarget variable, and displayed in the editor. You will see this value update in real time as the characters move closer to each other.
- this.m_DistanceToTarget = Vector3.Distance(this.transform.position, this.m_Target.transform.position);
- I want the mechanics to be that the characters only go searching for each other if their health is above health threshold.
- You don’t want to start tracking and get attacked if your health is low! That would be suicide.
This is where we will use an if statement:
- If my character’s health is greater than the RetreatHealth
- The distance that is being calculated between the 2 characters is greater than the weapon Throw Range Variable
- Set the characters navmesh stopping distance to the weapon throw range value
- call the MoveToTarget() function
Let me put this in an actual example:
- My Hero or Enemy checks his health
- He asks himself, “Is my health greater than 5?” (m_RetreatHealth).
- “Why Yes!! I have 20!”(m_Health,number set in the variable in the editor).
- “Is my opponent further away than my range to shoot him?”
- “Why Yes!!!!, I am further away than my current weapon threshold of 15”(current value in editor).
- “I am then going to set my stopping distance to the weapon threshold range, so that I know when to stop.” “Navmesh stop needs to stop me when I am in range to shoot a bullet.”
- So now my Hero or Enemy knows when to stop,
- I know that my Hero’s health or Enemy health is top notch and,
- I know when to start shooting, so let’s go find each other!!
- The MoveToTarget function takes the Game Object that the script is attached to and tells it to set it destination to the target’s position. IE, My Hero’s navmesh agent sets its destination to the variable M_Targets transform position.
- What happens though if my Hero and enemy are in Weapon Throw Range, if the distance to the target is equal to or smaller than the weapon threshold?
- Well , this can only mean that my Hero is already in attack range, so ATTACK!!
- This is where the BulletAttack() function gets triggered.
- In this function I start the m_BulletAttackTracker variable “timer” to increase its value by delta.time
- m_BulletAttackTracker is greater or equal to the m_BulletAttackRate (set to 2 in the editor),
- Set m_BulletAttackTracker back to 0
- Set my animation to play to “Attack”.
- Instantiate a new Game object which is called newBullet
- The new bullet game object is created 1 unit in front of where it is instantiated, and in the current rotation value as set in its transform values.
- Change newbullet GameObject transform position with the Lookat method (Rotates the transform so the forward vector points at /target/‘s current position.)
- This Lookat value, I made the targets position + vector3.up which in theory fires it towards the targets “head”.
- With my character being very low to the ground and abit dome shaped, the vector3.up method wasn’t enough to force the bullet to fire up. It was still firing almost through the ground.
- To fix this I updated the bullet script that is on the bullet prefab and added a new rigidbody.velocity method to this script. Now the bullet goes up and then down,
rb.velocity = new Vector3(0, 5, 0);
My desired effect with the script should therefore be: every 2 seconds attack my target with the animation playing of “ATTACK”and NOT walking, and firing bullets at my target.
We have one final ELSE in the Update() function,
- What if my Hero’s retreatHealth is 5 or less, then RUNNNNN AWWWAAAYYYYYYY!!!
- Call the function RunAwayFromTargetToBaseCamp()
- In this function I change the animation back to “WALK”
- Set NavMeshAgent destination method,
- Set the direction to the variable created earlier : m_HomeBase.
EASY PEASY, LEMON SQUEEZY!
- Navmesh not covering all terrain objects - I changed Navmesh radius to 0.1 in navigation panel
- Navmesh not selecting environmental objects - make all environmental objects static in the Inspector
- 2 characters colliding no matter what I made the stopping distance - change navmesh agent radius to 2
- Bullet not causing damage to health - I had my bulletPrefab ticked as trigger. When i unticked this it started causing damage
- OnCollisionEnter is destroying everything that has a collider on it - I needed to update my OnCollisionEnter function to say IF collision.gameObject.tag == “bullet”, then i updated my bulletPrefab with the “bullet” tag
Seeing my characters finding each other, changing animations, shooting, and running away gave me such a sense of achievement! Should I be concerned about the amount of satisfaction I got? I dont know, I will leave that to the professionals to discuss over a light tea…
To expand on this game, I want to add Melee attack. It shouldnt be much more work, but, that is my famous last words! Look out for part 2 of this blog to see how that went for me.