Search Unity

Advanced/Intermediate 3D Artificial Intelligence Tutorial Series

Discussion in 'Community Learning & Teaching' started by Lahzar, Aug 25, 2015.

  1. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Artificial Intelligence is the future! Will you be part of it? This tutorial series will elevate your knowledge of Artificial Intelligence to a AAA level, free of charge"! Be part of the future. It all starts here.
    General Info
    This series will give you what you need to create a very high quality stealth AI. It is built up in tutorial sections, with their own sub-sections. I release one tutorial section at a time. At the end of every sub-section, there should be a suggestion box, which gives some good ideas on how you can improve this AI. I have made all of these on my own original stealth AI, so I know they won't be to difficult to include. The suggestion box is optional, and only for those who want to really improve! I might include bonus tutorials, covering for example the suggestion boxes, after a tutorial has been added. Feedback is appreciated!
    Third Party Assets
    We are going to use two very helpful tools, which can be downloaded from the asset store, free options are available for both. The first asset is the A* Project, made by Aron Granberg. And the second asset is my Simple Option Stacker, made by.. well, me. If you purchase Simple Option Stacker trough our website, lustrousgames.com, you can use the discount code 15FUTURE for a $5 discount! That's more than 15% off!
    Work in Progress
    Finishing this series will take quite some time. Because I am a very busy man. When the whole written series is complete, I will make a video version of the whole thing.
    Under this you will find the tutorial list. Every tutorial marked with a <> is published, the rest will be published in order over a longer period of time.​





    Tutorial List

    Section 1 - Planning <>
    1.1 Setting up Aron Granbergs A* Project <>
    1.2 Setting up Simple Option Stacker <>
    1.3 Planning the behaviours <>

    Section 2 - Basic Behaviours <>
    2.1 Patrolling <>
    2.2 Alerting <>
    2.3 Taking damage <>
    2.4 Setting up the cover base and teamwork blackboard/framework <>

    Section 3 - Sneaky Stealth Section/Advanced Detection
    3.1 Line of Sight
    3.2 Sweetspots
    3.3 Shadows

    Section 4 - Combat
    4.1 Taking cover
    4.2 Tactical pathfinding
    4.3 Returning fire
    4.4 Flanking

    Section 5 - Searching
    5.1 Check player position
    5.2 Searching for the player using influencemapping and teamwork
     
    Last edited: Nov 8, 2015
  2. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 1 - Planning


    1 - Setting up Aron Granbergs The A* Project

    Using specifically The A* Project isn't necessary. You can use any implementation of A*. But this tutorial will only cover how to do e.g. tactical pathfinding on A*. Doing this on a NavMesh etc. is not impossible, but it is much more difficult to do.


    Import
    So let's get into the A* Project! First thing you have to do is naturally to download the asset from the asset store, or Arons website, http://arongranberg.com/astar/download, and import it to your unity project.


    Setup A* Grid
    Now that we have imported the asset, open up a scene, and create a new gameobject. This object will store the A* grid, so name it accordingly. With said object selected, click on Add Component > Pathfinding > Pathfinder. This will add the grid. It will ask you if you are using javascript, which isn't necessary in our case.

    Click on Graphs and Add New Graph. There are plenty of fun graphs here, in the pro version atelast, but I will use a normal grid graph for this. Its going to popup with tons of options. You might want to play around with these settings to acheive the optimal effect later, but for now we will focus on just a few. First of all, the node size. You want this as low as possible, without experience any performance effect. I usually use .5 or something. The width/depth should cover your area, not necessarily a very large area. (You can use up to 16 graphs in The A* Project.)

    Next is the collision testing options. In the "Mask" option you want to check every layer which contains only objects the AI can collide with. Its usually a good idea to make a designated layer for this! Now go to the height testing "Mask" and select any mask the AI can interract with. So the layer(s) the AI can walk on, AND the layer(s) you already checked on the previous Mask options.

    The settings tab is also very much worth playing around with! But for now, just set path debug mode to Tags.


    Scripting
    Now we need to do some simple scripting so we can use the grid we just set up! Make a new script. To keep things simple we will just need 1 script, 1 class, nothing more!

    Go to the very top of your script, to the directives, and write "using Pathfinding;"
    Create two new variables. The first is a Seeker, and the second is an AIPath. The AIPath is the AI pathfinder which comes default with the A* Project, and its very robust and usable, especially for being 'just' an example.

    Replace the Awake function already present in your script with a Start function. Will cover why later. Inside the start function we now need to assign these variables. But also create a target for the AI. This target is something used by the A* Project. Its simply a transform, which your AI will always follow. So instead of using, say unitys bultin agent.setDestination(destination.position); we have to use target = destination.position;

    And that's it! We have now successfully set up the A* Project! Your script should look something like this:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using Pathfinding;
    4.  
    5. public class myAIscript : MonoBehaviour {
    6.     Seeker seeker;
    7.     AIPath agent;
    8.  
    9.     void Start() {
    10.         #region Get Agent
    11.         seeker = GetComponentInChildren<Seeker>();
    12.         agent = GetComponentInChildren<AIPath>();
    13.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    14.         agent.target = target;
    15.         #endregion
    16.     }
    17. }


    Suggestion list:
    Make your own AIPath script -
    Make all target objects part of a single parent object to keep things cleaner -
     
    Last edited: Sep 12, 2015
  3. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 1 - Planning


    2 - Setting up Simple Option Stacker
    (Alternative tutorial available in documentation in my signature.) Simple Option Stacker is an Option Stack tool, with only two words as its primary development philosofy: Efficieny & Simplicity

    Yes, I know. I made it. *Shameless advertisment* If you don't know what an Option Stack is: it basically keeps a list of behaviours currently running, while it only executes the top one. You can read more about it on the Asset Store or the documentation thread avialable in my signature! (Asset Store Thread Here)


    Set up
    If you do not already own Simple Option Stacker, you can buy it on the Asset Store, or our website! Again, import the asset as per usual. You will only need the OptionStack.cs.

    If you need a free alternative, you can use System.Collections.Generic; and System; while simply using a Stack<Action> and only executing the .Peek() value of said stack. Simple Option Stacker is *a bit* more complex, however.


    Script
    To use it in a script, we again need to include its directive at the top: using OptionStacker;
    Create a new variable called stack. This will be an ExtendedStacker.
    Now go down to your Start function and write two lines of code:
    Code (CSharp):
    1. stack.StartStack(this); //Will soon be replaced with stack.Begin(this);
    2. stack.PushState("Idle"); //Will soon be replaced with stack.Push("Idle");
    The first line will start the option stack, and the second line will assign the default state. Now we need to set up this state real quick, so the code is runnable. Just make this new function:
    Code (CSharp):
    1. void Idle() {
    2.     //Suggestion: Play animation
    3.     //Do nothing else. Do not pop itself, because it is the default state. No state should be below it.
    4.     if(target.position != transform.position) {
    5.         target.position = transform.position;    //Don't go anywhere!
    6.     }
    7. }
    Last thing is inside the scripts Update function. At the very bottom of it, we need to put stack.Update(). And that's it. Now that we have set up both the A* project AND Simple Option Stacker, we are left with this code:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using Pathfinding;
    4. using OptionStacker;
    5.  
    6. public class myAIscript : MonoBehaviour {
    7.     ExtendedStacker stack = new ExtendedStacker();
    8.     Seeker seeker;
    9.     AIPath agent;
    10.  
    11.     void Start() {
    12.         #region Get Agent
    13.         seeker = GetComponentInChildren<Seeker>();
    14.         agent = GetComponentInChildren<AIPath>();
    15.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    16. //        target.position = transform.position;
    17.         agent.target = target;
    18.         #endregion
    19.  
    20.         stack.StartStack(this);
    21.         stack.PushState("Idle");
    22.     }
    23.  
    24.     void Update() {
    25.         stack.Update(); //Will soon be replaced with .Execute(optional params[]);
    26.     }
    27.  
    28.     void Idle() {
    29.         //Play animation
    30.         //Do nothing else
    31.     }
    32. }
     
    Last edited: Sep 12, 2015
  4. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 1 - Planning


    3 - Planning
    We won't go throught this in immense detail, but I want you to know that it is really important to plan your AI!
    The most common way to do this is to make a diagram, explaining the transitions between states.
    A pen and a piece of paper is my preferred way of doing this, but if you want to make it digital, you can head to draw.io, and sign in with dropbox or google+ etc. Draw.io is completely free and it will store your diagrams 'in the cloud'.

    Our AI will have 4 base behaviours; Patrol, Alert, Combat and Search. It is relatively simple, as many AAA AIs are built up of hundreds of smaller behaviours! This AI will also have an Idle state, which will be the default/fallback state. Our AI also needs to react to getting shot! This means we need a ReactToDamage/Stun state aswell!

    I won't go into explaining the transitions, as you can easily see them on the diagram I made:
    SOSFMLD.png

    So with this covered, let's get started with the actual AI!
     
    Last edited: Sep 12, 2015
  5. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 2 - Basic Behaviours


    1 - Patrolling

    Before we can start getting into the advanced goodies, we have to lay the base foundations. All of our behaviours will each be a set of functions.

    Editor
    By default, our AI should be patrolling along a predefined path. We'll do this pretty much like Unity did in their Unity 4 stealth tutorial! We will begin in the editor. Create a new empty gameObject. It won't matter where you place this. This will be a path. Now we need some waypoints which our AI will traverse between. Spawn some new empty gameobjects, and make them children of the path gameobject. Name them in order of what waypoints the AI should go to first, and rank them in the hierarchy first to last.
    Namnlös.png

    Scripting - Assigning Waypoints
    For the scripting, we will start by adding a new directive. Write using System.Collections.Generic; at the top of your script. This is because we want to use lists. This will be more important later on, but we can also use this when patrolling.

    Back inside your base class, make a new Vector[] array, which will be all the waypoints positions. Head down to your Start() function. Now we need to assign the waypoints positions to the array. Make a List<Vector3> inside the start function. It might be a neat idea to make the capacity of this List the amount of waypoints. Your list should look something like this:
    Code (CSharp):
    1. List<Vector3> size = new List<Vector3>(path.GetComponentsInChildren<Transform>().Length-1);
    Proceed to make a foreach loop below it. This will loop through all the children of the path object. To make sure the path itself isn't counted as a waypoint, this loop will also make sure the child its checking isn't the path object, and add it to the temporary List<Vector3>. Finally we will convert the list to an array and assign it as our waypoints. Our Start() code should look something like this now:

    Code (CSharp):
    1. #region Set up Patrol Path
    2.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    3.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    4.             if(p != patrolPath.transform)    {
    5.                 size.Add(p.position);
    6.             }
    7.         }
    8.         point = size.ToArray();
    9. #endregion
    The Idle behaviour is not needed, because we can make Patrol our default behaviour. So replace Idle() with Patrol(), and remove everything inside it. Go back to where you pushed the "Idle" state into the Simple Option Stacker, and replace "Idle" with "Patrol". Now your entire script should look something like this:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using OptionStacker;
    5. using Pathfinding;
    6.  
    7. public class myAIscript : MonoBehaviour {
    8.     ExtendedStacker stack = new ExtendedStacker();
    9.     Seeker seeker;
    10.     AIPath agent;
    11.  
    12.     private Transform target;
    13.     private Vector3[] point;
    14.  
    15.     void Start() {
    16.         #region Get Agent
    17.         seeker = GetComponentInChildren<Seeker>();
    18.         agent = GetComponentInChildren<AIPath>();
    19.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    20.         target.position = transform.position;
    21.         agent.target = target;
    22.         #endregion
    23.  
    24. #region Set up Patrol Path
    25.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    26.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    27.             if(p != patrolPath.transform)    {
    28.                 size.Add(p.position);
    29.             }
    30.         }
    31.         point = size.ToArray();
    32. #endregion
    33.  
    34. #region Simple Option Stacker
    35.         stack.StartStack(this);
    36.         stack.PushState("Patrol");
    37. #endregion
    38.     }
    39.  
    40.     void Update() {
    41.         stack.Update();
    42.     }
    43.  
    44.     void Patrol() {
    45.         //Do nothing yet
    46.     }
    47. }

    Scripting - Patrolling Behaviour
    If you try playing right now, nothing will happen. We will change this now, by making the AI actually move between the waypoints.

    First thing we need to do inside the Patrol() behaviour, is to make sure the AI is only selecting the next waypoint when it has reached the current one. This is simple enough. Using The A* Project we can check if(agent.TargetReached). But this only updates as fast as your path updates, while we check it every frame. That's why we have to also check the distance between the player and the current waypoint.

    We'll make a new float variable which will keep track of the distance between the target transform and the AI's feet. Go down to your Update() function, and use either .magnitude or Vector3.Distance to find the distance. We can use the A* Project to find the AIs feet position!
    Code (CSharp):
    1. distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
    Back in our Patrol() function we now have to use this new distance variable. Using The A* Project we can also use another variable from the agent, instead of making a new one. So add if(distance <= agent.endReachedDistance). Agent.endRachedDistance, just like any other agent variables, can be edited in the AIPath script attached to the AI in the inspector.

    Before we can actually update the AIs path, we need first of all to have an indexer int which will keep track of what waypoint in the waypoints array is the current waypoint. Then we need to check if this index exists, and if not, reset it. Then we can finally set the position of the target transform, to the index'th waypoint in our array.

    This is all good and well, but we also want our AI to walk or run at different speeds, depending on their behaviour. Make a patrolSpd float variable. But instead of updating this every frame, we'll use the Simple Option Stacker On/OnExit feature. This means that if a method called OnPatrol() exists when Patrol() is called, OnPatrol() will be called first. So we simply set the speed inside an OnPatrol(), and then pop it immediately!

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using OptionStacker;
    5. using Pathfinding;
    6.  
    7. public class myAIscript : MonoBehaviour {
    8.     ExtendedStacker stack = new ExtendedStacker();
    9.     Seeker seeker;
    10.     AIPath agent;
    11.  
    12.     private Transform target;
    13.     private float distance;
    14.  
    15.     [Header("Patrol")]
    16.     private Vector3[] point;
    17.     private int indexer;
    18.     public float patrolSpd = 1;
    19.  
    20.     void Start() {
    21.         #region Get Agent
    22.         seeker = GetComponentInChildren<Seeker>();
    23.         agent = GetComponentInChildren<AIPath>();
    24.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    25.         target.position = transform.position;
    26.         agent.target = target;
    27.         #endregion
    28.  
    29. #region Set up Patrol Path
    30.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    31.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    32.             if(p != patrolPath.transform)    {
    33.                 size.Add(p.position);
    34.             }
    35.         }
    36.         point = size.ToArray();
    37. #endregion
    38.  
    39. #region Simple Option Stacker
    40.         stack.StartStack(this);
    41.         stack.PushState("Patrol");
    42. #endregion
    43.     }
    44.  
    45.     void Update() {
    46.         distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
    47.  
    48.         stack.Update();
    49.     }
    50.  
    51. #region Patrol
    52.     void OnPatrol() {
    53.         agent.speed = patrolSpd;
    54.         stack.popState();
    55.     }
    56.  
    57.     void Patrol() {
    58.         if(agent.TargetReached && distance <= agent.endReachedDistance) {
    59.             indexer++;
    60.             if(indexer >= point.Length) indexer = 0;
    61.             target.position = point[indexer].position;
    62.         }
    63.     }
    64. #endregion
    65. }

    Suggestion list:
    Precalculate all the paths between the waypoints and store them instead of waypoints -
    Stop at certain, not all, waypoint and play an animation -
     
    Last edited: Sep 12, 2015
  6. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 2 - Basic Behaviours


    2 - Alert

    Before we can start getting into the advanced goodies, we have to lay the base foundations. The alert function will trigger when the AI is supicious, and think they know where the player are. For now we will stick with just seeing the player.

    Editor
    Naturally, our AI shouldn't have an infinite view range. My favourite way of doing this is using a trigger. Having a custom collider could acheive some cool effects, but a standard unity sphere collider will do just fine! Attach a spherecollider to your AI, and adjust the radius. My AIs spherecollider has set trigger to enabled. Doing this means your player needs to have a rigidbody.

    Make a new layer called Shadows. Find the head of your AI, and attach a camera.
    Set its clear flags to depth only or solid color.
    Set its culling mask to Player and Shadows.
    Change the Field of View to what you want your AIs FoV to be. I set mine to 120.
    Clipping Planes Far can be set to the same distance as your previously added sphere colliders radius.
    For best results I would seriously recommend using the Deferred Rendering Path even though its much more resource intesive. The other camera variables can be set to whatever suites your needs, except for Target Texture! This is an essential part of checking wether or not the player is concealed by the shadows, later in the Darkness Detection Section (3.3)!

    Make a new Render Texture in the editor, just like you would make a new material.
    Right click > Create > Render Texture.
    This will act as a template for the Darkness Detection later. Set the Size to whatever you want. But remember that a higher size will be slower, but yield better accuracy. A good number would be 256x256.
    The rest of the variables doesn't matter too much, but should be tweaked for performance!
    However, the Color Format should be ARGB32. This means it uses Alpha, Red, Green and Blue colours using a 32 bit platte. As far as I know, this is the industry standard for colours.

    Scripting - Variables
    We start by adding some variables. First thing we need is a refrence to the player. Later we will place this refrence in our teamwork script, but for now we have to assign it to every AI we have manually. We will need a public float variable for speed, and a public float variable for how long the player can be visible before being spotted. A public LayerMask for checking if something is blocking the AIs vision. In this LayerMask you should enable any layer that could block the AIs vision, so not "See-Through" or "Transparent FX" etc. We also need some private/internal variables: a float which tells us how long the player has been seen, and a boolean which will be true if the player is in sight. Last private variable is a Vector3, which will be where this AI last saw the player.

    TLDR:
    - public Transform player
    - public LayerMask seeMask
    - public float alertSpeed
    - public float timeBeforeSpotted
    - private Vector3 lastPrivateSpot
    - private float spot
    - private bool inSight

    Scripting - Visibility
    Before the AI can react to seeing something, it needs to actually see something. For now we will keep it as simple as possible, but we will come back and have some fun with realistic visibility later. Remember adding that spherecollider? It's time to use it!

    Make a OnTriggerStay function, which takes a Collider as an argument. I called my Collider o, for other. Unity will automatically call this function when something is inside the spherecollider.
    Inside it we first need to check if the object inside the trigger is the player, and not just some random wall. Simplest way to do this is to use our player variable and check if it matches o.transform. The rest of our code will take place inside this if statement.
    The next step seems counterintuitive, and its optional, but I like having it. Set inSight to false at the very start. This will make more sense later, as we can simply do return; instead of setting inSight = false;

    Checking wheter or not the AI is actually in sight, can be very easy to implement, but you can also go completely overboard with fancy features. For now we will just check if the player is inside the AI's FoV, and if there is something blocking the AIs vision.

    Time and time again you see people getting the angle between the forward direction vector of the AI and the player. We will not do this because its simply overused. Instead we will create a fustrum from the previously added Camera, and check if the players bounding box is inside this fustrum. I like this method much more. Doing this is simpler than it sounds. First create a local array of Planes. And use GeometryUtility to get the planes of the Camera fustrum. Note that this also requires a global-scope variable refrencing either the camera or the head object! Also we need to get the bounds of the skinned mesh renderer of our player.
    Instead of checking if the player is inside the fustrum, we check if they are outside it. If they are not outside it, we can simply stop the code here.
    Code (CSharp):
    1. Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(head.GetComponent<Camera>());
    2. if(!GeometryUtility.TestPlanesAABB(frustumPlanes, o.transform.FindChild("EthanBody").GetComponent<SkinnedMeshRenderer>().bounds))    {    //I am using the Ethan model, which is part of the unity standard assets!
    3.     return;
    4. }


    Scripting - Line of Sight
    After our first FoV check, we need to see if something is blocking the view. Later we will add some more advanced Line of Sight checks, but for now we will simply raycast from the AIs camera position to the players position. Remember to use the layermask we created. If there is something in the way, the player is not in sight. If not, the player is in sight.
    Code (CSharp):
    1. Vector3 dir = player.position - head.position;
    2. float d = Vector3.Distance(player.position, head.position);
    3.  
    4. if(Physics.Raycast(head.position, dir, d, seeMask))    {
    5.     return;
    6. } else {
    7.     inSight = true;
    8. }


    Scripting - Alert Behaviour
    After setting up some basic visibility, we can write the actual Alert behaviour! This behaviour will be relatively simple. If the player is seen, a timer will tick up, and if not it will tick down. If the timer reaches 0, the AI will continue patrolling, if it reaches the variable we previously set, timeBeforeSpotted, the AI will return to patrolling.

    First we need to set the speed of the AI in the OnAlert() function, before immidiatly popping this state.
    In our Alert() function we will increase the spot variable if the player is insight, and if they are not, we will decrease it at a slower speed.
    After this we need to check wether we should push combat, stay in alert, or go back to patrolling. We do this using again the spot variable.
    Code (CSharp):
    1.  
    2.     #region Alert
    3.     public void OnAlert()    {
    4.         agent.speed = alertSpeed;
    5.         stack.popState();
    6.     }
    7.  
    8.     public void Alert()    {
    9.         if(inSight)    {
    10.             spot+=1*Time.deltaTime;
    11.         } else {
    12.             spot-=0.5f*Time.deltaTime;
    13.         }
    14.  
    15.         if(spot < timeBeforeSpotted && spot > 0)    {
    16.             if(spot > 0.25f)    {
    17.                 target.position = lastPrivateSpot;
    18.             }
    19.         } else if(spot >= timeBeforeSpotted)    {
    20.             spot = timeBeforeSpotted;
    21.             stack.popState();
    22.   //        stack.pushState("Combat");    //We have not made this function yet
    23.         } else {
    24.             agent.speed = patrolSpd;    //Simple Option Stacker will have an OnReturnFunction() feature in the next update
    25.             spot = 0;
    26.             stack.popState();
    27.         }
    28.     }
    29.     #endregion
    30.  

    Finally we need to transition from Patrol() to Alert() when the player is inSight. Simply pushState("Alert") if the AI is patrolling, and the player is inSight. This is very simple, so I will end this tutorial as per usual with the current script and a suggestion list.

    Scripting - Tutorial Results
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using OptionStacker;
    5. using Pathfinding;
    6.  
    7. public class myAIscript : MonoBehaviour {
    8.     #region Variables
    9.     ExtendedStacker stack = new ExtendedStacker();
    10.     Seeker seeker;
    11.     AIPath agent;
    12.  
    13.     private Transform target;
    14.     private float distance;
    15.  
    16.     private Vector3 lastPrivateSpot;
    17.     private float spot;
    18.  
    19.     private Vector3[] point;
    20.     private int indexer;
    21.  
    22.     [Header("General")]
    23.     public Transform player;
    24.     public Transform head;                //NOTE: AI's head
    25.     public LayerMask seeMask;
    26.     public float timeBeforeSpotted;
    27.  
    28.     [Header("Patrol")]
    29.     public GameObject patrolPath;
    30.     public float patrolSpd = 1;
    31.  
    32.     [Header("Alert")]
    33.     public float alertSpeed;
    34.     private bool inSight;
    35.     #endregion
    36.  
    37.     void Start() {
    38.         #region Get Agent
    39.         seeker = GetComponentInChildren<Seeker>();
    40.         agent = GetComponentInChildren<AIPath>();
    41.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    42.         target.position = transform.position;
    43.         agent.target = target;
    44.         #endregion
    45.      
    46.         #region Set up Patrol Path
    47.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    48.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    49.             if(p != patrolPath.transform)    {
    50.                 size.Add(p.position);
    51.             }
    52.         }
    53.         point = size.ToArray();
    54.         #endregion
    55.      
    56.         #region Simple Option Stacker
    57.         stack.StartStack(this);
    58.         stack.PushState("Patrol");
    59.         #endregion
    60.     }
    61.  
    62.     void Update() {
    63.         distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
    64.      
    65.         stack.Update();
    66.     }
    67.  
    68.     #region Patrol
    69.     void OnPatrol() {
    70.         agent.speed = patrolSpd;
    71.         stack.PopState();
    72.     }
    73.  
    74.     void Patrol() {
    75.         if(inSight || spot > 0)    {
    76.             stack.PushState("Alert");
    77.             return;
    78.         }
    79.  
    80.         if(agent.TargetReached && distance <= agent.endReachedDistance) {
    81.             indexer++;
    82.             if(indexer >= point.Length) indexer = 0;
    83.             target.position = point[indexer];
    84.         }
    85.     }
    86.     #endregion
    87.     #region Alert
    88.     public void OnAlert()    {
    89.         agent.speed = alertSpeed;
    90.         stack.PopState();
    91.     }
    92.  
    93.     public void Alert()    {
    94.         if(inSight)    {
    95.             spot+=1*Time.deltaTime;
    96.         } else {
    97.             spot-=0.5f*Time.deltaTime;
    98.         }
    99.      
    100.         if(spot < timeBeforeSpotted && spot > 0)    {
    101.             if(spot > 0.25f)    {
    102.                 target.position = lastPrivateSpot;
    103.             }
    104.         } else if(spot >= timeBeforeSpotted)    {
    105.             spot = timeBeforeSpotted;
    106.             stack.PopState();
    107.             stack.PushState("Combat");
    108.         } else {
    109.             agent.speed = patrolSpd;    //OnReturnPatrol()
    110.             spot = 0;
    111.             stack.PopState();
    112.         }
    113.     }
    114.     #endregion
    115.  
    116.     #region Vision
    117.     void OnTriggerStay(Collider o) {
    118.         inSight = false;
    119.  
    120.         Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(head.GetComponent<Camera>());
    121.         if(!GeometryUtility.TestPlanesAABB(frustumPlanes, o.transform.FindChild("EthanBody").GetComponent<SkinnedMeshRenderer>().bounds))    {    //I am using the Ethan model, which is part of the unity standard assets!
    122.             return;
    123.         }
    124.  
    125.         Vector3 dir = player.position - head.position;
    126.         float d = Vector3.Distance(player.position, head.position);
    127.      
    128.         if(Physics.Raycast(head.position, dir, d, seeMask))    {
    129.             return;
    130.         } else {
    131.             inSight = true;
    132.         }
    133.     }
    134.     #endregion
    135. }


    Suggestion list:
    Use Simple Option Stackers Cooldown() function to apply a cooldown to Alert() after returning to Patrol() -
     
    Last edited: Sep 12, 2015
  7. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 2 - Basic Behaviours


    3 - Taking Damage

    Our AI isn't supposed to be immortal, in this case atleast, so they will need to take damage in some way or another. Your game might have a custom damage system, but in this tutorial we will cover a very easy but powerful way of dealing and taking damage, using interfaces.

    Interfaces - What are they?
    An interface is a script which can be used by other scripts in a certain way. An interface only contains unassigned variables and empty methods. For a script to use an interface, the implementing script MUST contain the same variables and methods as the interface. What the implementing script then does with this interface, is up to you. But why would this be useful? If a script knows another script is an interface, it can interract with this script through the interface. It's difficult to understand me, I know, so take this example.

    What does a Human NPC and a Window Pane have in common in a game? They can both be damaged. How do they differ? In so many ways, but the important thing is that they react differently to being damaged. The human gets hurt and dies, while the window pane just shatters.

    Say we have a bullet script, which will try to damage everything it hits. If we tried to damage the window pane and the human with this script, it would look something like this (psuedocode).
    Code (CSharp):
    1. if HitObject is Human
    2.     HitObject.GetComponent<Human>().Hurt(29.5f)
    3. else if HitObject is Window
    4.     HitObject.GetComponent<Window>().Shatter()
    You can obviously see how this will get overly complicated when you have just a few different items that can take damage. Image having hundreds of damageable items! This is where interface comes in to save the day.

    Using an interface we can make every damageable object have a take damage method which takes a float called damage.
    In our human, they would have an arbitrary amount of health, and they would loose an amount of health determined by the damage dealing object when damaged. When their health reached 0 or less they would die.
    In our window, it would not have any stored health. When the window took damage, it would ignore the damage input and just shatter.

    All we have done so far is moving code from the damage dealing object to the object taking the damage, and renamed everything so it all uses the same names. But its nothing we couldn't do without an interface, right?
    Right. For now comes the genius of interfaces. Instead of having individual commands for every single damageable item in our damage dealing script, we can just tell every object to take damage if it uses the damage interface.

    Code (CSharp):
    1. foreach(MonoBehaviour script in HitObject.GetComponents<MonoBehaviour>()) {
    2.     if(script is IDamagable)
    3.         (Script as IDamagable).TakeDamage(33);
    4. }
    This little loop will check every script that is attached to the hit object, and check if it using the IDamagable interface. If the script is using the IDamagable interface, it will deal 33 damage to said script. And thats it. This code will damage any script which can be damaged, without having to refrence every single damageable script.

    Scripting - Setting up an interface
    Setting up an interface is very easy. Create a new script and call it IDamagable. Its common practise to start every interface with a capital I, and some do an underscore aswell.
    First thing you want to do is to replace class with interface, at the very top of your script, left of the name. Then remove : MonoBehaviour aswell. Remove all the standard code aswell.
    Code (CSharp):
    1. public interface IBasicInterface    {
    2.  
    3. }

    Inside the interface you want to place any variable and method which MUST be included by any script which is going to use this interface. We only need a TakeDamage(float damage) method.
    So write void TakeDamage(float damage); inside the interface.
    Assigning variables must be done using get and set. You might want to look this up somewhere else, as this tutorial will not cover it.
    Code (CSharp):
    1. public interface IDamagable    {
    2.     void TakeDamage(float damage);
    3. }

    Scripting - Implementing the interface
    Now we need to go to the implementing script, the script which is using this interface. The first thing we have to do is tell our script to use the interface. This is called implementing. We do this at the top of our script where it says : MonoBehaviour and we add , IDamagable.
    Code (CSharp):
    1. public class AI : MonoBehaviour, IDamagable
    The colon means extends, and the comma means implements. You can implement as many interfaces as you want, but you can only extend up to one class. If you don't know about extending classes, you might want to look it up!

    Now we need to make a new variable, which will store the health of our AI. Then we need a public method called TakeDamage, which takes a float called damage, as demanded by the interface.

    Scripting - Taking damaging
    Now we need to actually take damage, so inside the TakeDamage method we will subtract the input damage from the base health. Then check if the health is less than or equal to 0. When the AIs health is 0 or lower, they should die. For easier reading, I like to make this a function of its own. In here we can play some animation or do some fancy stuff, but for simplicity we will only destroy this gameobject immediately.

    Scrpting - Reacting to damage
    Reacting to damage is one of those small things in game AI which really makes it or breaks it! Imagine holding a really cool, big, mean-looking sniper rifle, and shooting at an enemy. The whole screen shakes and you can hear the shot echo through the environment. The bullet hits the enemy so hard you can hear their bones break. The blood spews from their sides as the bullets goes straight through them. Then they yell "Ouch" and turn around and shoot you as if nothing happend.

    This really breaks the players immersion, and it can make a good game feel unsatisfying. Implementing this using Simple Option Stacker and an interface couldn't be simpler!

    If the health is greater than 0 after being damaged, we will push ReactToDamage. This will be an IEnumerator with a simple countdown. Since we are focusing strictly on the scripting part, this is all we will do. But if you want some really nice results, where the AI falls backwards into a roll or flinches properly when getting shot, you will have to play around with animations!

    Simple option stacker will also make sure you won't stack up multiple ReactToDamage functions. If you intend to stack up multiple identical states, you need to use .QueState(0, "ReactToDamage") or change the Simple Option Stacker Source Code manually.

    Code (CSharp):
    1.     #region Damage
    2.     public void TakeDamage(float damage)    {
    3.         health -= damage;
    4.  
    5.         if(health > 0)    {
    6.             stack.PushState("ReactToDamage");
    7.         } else {
    8.             Die();
    9.         }
    10.     }
    11.  
    12.     IEnumerator ReactToDamage()    {
    13.         Debug.Log("Ouch!");
    14.         yield return new WaitForSeconds(2);    //Simulate reaction
    15.         stack.PopState();
    16.     }
    17.  
    18.     void Die() {
    19.         DestroyImmediate(gameObject);
    20.     }
    21.     #endregion
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using OptionStacker;
    5. using Pathfinding;
    6.  
    7. public class myAIscript : MonoBehaviour, IDamageable {
    8.     #region Variables
    9.     ExtendedStacker stack = new ExtendedStacker();
    10.     Seeker seeker;
    11.     AIPath agent;
    12.  
    13.     private Transform target;
    14.     private float distance;
    15.  
    16.     private Vector3 lastPrivateSpot;
    17.     private float spot;
    18.  
    19.     private Vector3[] point;
    20.     private int indexer;
    21.  
    22.     [Header("General")]
    23.     public Transform player;
    24.     public Transform head;                //NOTE: AI's head
    25.     public LayerMask seeMask;
    26.     public float timeBeforeSpotted = 3;
    27.     public float health = 100;
    28.  
    29.     [Header("Patrol")]
    30.     public GameObject patrolPath;
    31.     public float patrolSpd = 1;
    32.  
    33.     [Header("Alert")]
    34.     public float alertSpeed = 0.75f;
    35.     private bool inSight;
    36.     #endregion
    37.  
    38.     void Start() {
    39.         #region Get Agent
    40.         seeker = GetComponentInChildren<Seeker>();
    41.         agent = GetComponentInChildren<AIPath>();
    42.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    43.         target.position = transform.position;
    44.         agent.target = target;
    45.         #endregion
    46.      
    47.         #region Set up Patrol Path
    48.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    49.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    50.             if(p != patrolPath.transform)    {
    51.                 size.Add(p.position);
    52.             }
    53.         }
    54.         point = size.ToArray();
    55.         #endregion
    56.      
    57.         #region Simple Option Stacker
    58.         stack.StartStack(this);
    59.         stack.PushState("Patrol");
    60.         #endregion
    61.     }
    62.  
    63.     void Update() {
    64.         distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
    65.      
    66.         stack.Update();
    67.     }
    68.  
    69.     #region Patrol
    70.     void OnPatrol() {
    71.         agent.speed = patrolSpd;
    72.         stack.PopState();
    73.     }
    74.  
    75.     void Patrol() {
    76.         if(inSight || spot > 0)    {
    77.             stack.PushState("Alert");
    78.             return;
    79.         }
    80.  
    81.         if(agent.TargetReached && distance <= agent.endReachedDistance) {
    82.             indexer++;
    83.             if(indexer >= point.Length) indexer = 0;
    84.             target.position = point[indexer];
    85.         }
    86.     }
    87.     #endregion
    88.     #region Alert
    89.     public void OnAlert()    {
    90.         agent.speed = alertSpeed;
    91.         stack.PopState();
    92.     }
    93.  
    94.     public void Alert()    {
    95.         if(inSight)    {
    96.             spot+=1*Time.deltaTime;
    97.         } else {
    98.             spot-=0.5f*Time.deltaTime;
    99.         }
    100.      
    101.         if(spot < timeBeforeSpotted && spot > 0)    {
    102.             if(spot > 0.25f)    {
    103.                 target.position = lastPrivateSpot;
    104.             }
    105.         } else if(spot >= timeBeforeSpotted)    {
    106.             spot = timeBeforeSpotted;
    107.             stack.PopState();
    108.             stack.PushState("Combat");
    109.         } else {
    110.             agent.speed = patrolSpd;    //OnReturnPatrol()
    111.             spot = 0;
    112.             stack.PopState();
    113.         }
    114.     }
    115.     #endregion
    116.  
    117.     #region Vision
    118.     void OnTriggerStay(Collider o) {
    119.         inSight = false;
    120.  
    121.         Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(head.GetComponent<Camera>());
    122.         if(!GeometryUtility.TestPlanesAABB(frustumPlanes, o.transform.FindChild("EthanBody").GetComponent<SkinnedMeshRenderer>().bounds))    {    //I am using the Ethan model, which is part of the unity standard assets!
    123.             return;
    124.         }
    125.  
    126.         Vector3 dir = player.position - head.position;
    127.         float d = Vector3.Distance(player.position, head.position);
    128.      
    129.         if(Physics.Raycast(head.position, dir, d, seeMask))    {
    130.             return;
    131.         } else {
    132.             inSight = true;
    133.         }
    134.     }
    135.     #endregion
    136.  
    137.     #region Damage
    138.     public void TakeDamage(float damage)    {
    139.         health -= damage;
    140.  
    141.         if(health > 0)    {
    142.             stack.PushState("ReactToDamage");
    143.         } else {
    144.             Die();
    145.         }
    146.     }
    147.  
    148.     IEnumerator ReactToDamage()    {
    149.         Debug.Log("Ouch!");
    150.         yield return new WaitForSeconds(2);    //Simulate reaction
    151.         stack.PopState();
    152.     }
    153.  
    154.     void Die() {
    155.         DestroyImmediate(gameObject);
    156.     }
    157.     #endregion
    158. }


    Suggestion List:
    Play an animation when taking damage -
    Play some audio when taking damage -
    Play an animation when dying -
     
    Last edited: Sep 12, 2015
  8. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Section 2 - Basic Behaviours


    4 - Setting up the teamwork blackboard/framework

    In this tutorial we will go over setting up a simple script which will convey crucial combat information between predetermined AIs. This means we get our AIs to plan and execute tactics together. Not only will this allow for team tactics, but it will help save processing power and space! If we were to put some of these checks in our AI script, every single AI would have to perform that check, but now we can do certain checks once. Or we can move every check from an AI into this script This is where I personally feel things start to get fun, when working with AI.

    In this tutorial we will only cover the basics of this script, and set up a line of sight check. We will come back to this later, when new checks and tactics are needed!

    Notice how I often use the word blackboard? This is an AI term. A blackboard simply refers to a set of variables which is equally accessible by multiple AIs. What we are making now is a blackboard script.
    NOTE: This system is best suited for having different squads of AI in a small open world, or for spawning enemies in waves. If you're trying to do something else, you might want to create a dynamic blackboard, or use a toolbar/singleton system.

    Scripting - Refrencing
    First of all, make a new C# script. This will be the squad system script, so name it appropriately.
    The squad script will need a refrence to all its AIs, and it would be best if the AI also know which squad they are a part of. Doing this is simple, and will require only a list of the agents in this squad. Remember to add the System.Collections.Generic; directive!
    In this tutorial we are only making a single kind of AI, and therefor I will only refrence the AI script we are making in this list. However, do note that it is easily possible to create a fully dynamic version using interfaces. See the previous tutorial (2.3) for more information!
    Code (CSharp):
    1. using System.Collections.Generic;
    2. public class Squad : MonoBehaviour    {
    3.     public List<myAIscript> agents = new List<myAIscript>();
    4. }

    Thats it. Only downside being we have to manually assign the AIs. See the suggestion list for improvement.
    Now our AI needs to know what squad they are in. Open up the main AI script and add a refrence to the squad script. Make it private or internal or add a [HideInInspector] attribute to it, so it isn't visible in the inspector.
    Code (CSharp):
    1. internal Squad sq;

    But why hide it in the inspector? How do we change it now? We'll do this in the squad script, in the Awake() function. Foreach ai in the list we made, we will change its squad script refrence to this squad script. (This is a good place to allow for multiple kinds of ai using an interface and linq)
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public class Squad : MonoBehaviour {
    6.     public List<myAIscript> agents = new List<myAIscript>();
    7.  
    8.     void Awake()    {
    9.         foreach(myAIscript ai in agents)    {
    10.             ai.gameObject.SetActive(true);        //Optional
    11.             ai.sq = this;
    12.         }
    13.     }
    14. }


    Scripting - Line of Sight Coroutine
    To help our AI make some crucial combat decisions, we need as much information as possible to be easily available for it. Some information is AI specific, like location and distance to the player etc. But Some info is the same for everyone. An example of this is the players Line of Sight. Naturally our AI would want to avoid the players Line of Sight when taking cover, or moving between cover. This is where the A* pathfinding becomes very handy. We can consider every node as a potential coverspot, as opposed to a navmesh where we need to generate lines and fustrums and everything gets very complicated! Note that the follow code is specific to the A* project by Aron Granberg. All the features should be available in the free version!

    First of all, start by adding the A* project pathfinding directive, before declaring all necessary variables. A refrence to the players transform. A layermask which will include any solid object which might block the players view, and exclude any see-through/transparent layers. An array of GridNodes which will contain all the nodes on the A* grid. And finally a float which will dictate how frequently the line of sight check will fire.
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using Pathfinding;
    5.  
    6. public class Squad : MonoBehaviour {
    7.     public Transform player;
    8.     public List<myAIscript> agents = new List<myAIscript>();
    9.     public LayerMask l;
    10.     public float freq = .5f;
    11.  
    12.     internal GridNode[] nodes;
    13.  
    14.     void Awake()    {
    15.         foreach(myAIscript ai in agents)    {
    16.             ai.gameObject.SetActive(true);        //Good idea
    17.             ai.sq = this;
    18.         }
    19.     }
    20. }

    Now we need to create a coroutine, and start it at the end of the Awake() function. The first thing we write inside this is a WaitForEndOfFrame command. This is because of what we do next. Assigning the GridNode[] nodes we declared earlier. This is a single line of the A* project.
    Code (CSharp):
    1. nodes = AstarPath.active.astarData.gridGraph.nodes;
    Now we need to set up a loop. In a coroutine we can use goto, or while(true) to acheive this effect. Inside the loop we will have another set of variables. This is the position of the player and the forward direction of the player. After that we will make yet another loop. This will be a foreach loop which will iterate through each GridNode in the nodes array [foreach(GridNode g in nodes)]. After this foreach has finished we will restart the while(true) loop with a new WaitForSeconds(freq).

    The rest of the code will be done inside the foreach loop. First of all we want to ignore any node which cannot be walked on. In the A* project we can check if a GridNode is .Walkable to see if it blocked or not. Check if this GridNode is walkable, and if not, continue; the foreach loop.
    Code (CSharp):
    1.     IEnumerator LoS()    {
    2.         yield return new WaitForEndOfFrame();
    3.         nodes = AstarPath.active.astarData.gridGraph.nodes;
    4.  
    5.         while(true)    {
    6.             Vector3 pos = player.position;
    7.             Vector3 fw = player.forward*0.1f;
    8.  
    9.             foreach(GridNode g in nodes)    {
    10.                 if(!g.Walkable)
    11.                     continue;
    12.             }
    13.  
    14.            yield return new WaitForSeconds(freq);
    15.        }
    16.    }

    Now we need some node specific local variables. First of all we need to convert the Vector2 position of the node to a normal Vector3 variable. Then we need the distance from the player to the node, and the angle between the players forward direction and the node, relative to the player.
    Code (CSharp):
    1. Vector3 p = new Vector3(((Vector3)g.position).x, fw.y, ((Vector3)g.position).z);  
    2. float d = Vector3.Distance(pos, p);                                                  
    3. float a = Vector3.Angle(fw, p-pos);
    Theory - Cover scoring system base
    Now comes the actual line of sight check. Since we aren't using the A* grid tags, we can utulize these. (If you need to use tags for something else, you can save these line of sight tags as an array of float in the squad script.) Not only will we check if a node can provide cover by using a line of sight check, but we will check if the AI have to crouch, or they can stand in this spot. I struggled for a long time to make an efficient solution to this, and I settled for a binary type system. We need two numbers. The first number from the right will be 0, 1 or 2, and will tell if there is no cover, waist-high cover, or full cover when standing.

    Namnlös.png
    To do this, we first need to check every node twice, so below everything we have written so far inside our foreach(GridNode g in nodes), we write a foreach loop which counts down from 1 to 0. If we start from the top, and a node is covered in the standing stance position, we can assume that this spot also covers the crouching stance, and simply continue to the next node. Remember to reset the nodes tag value to 0 before starting this loop.

    Scripting - Line of Sight
    The line of sight check is simple. First we check if this node is inside the players field of view, and then we raycast from the models eyes middle position to the node, and change the nodes tag based on these two returns. Starting from the top and going downwards in our loop means we can easily avoid duplicating scores.

    If the angle between the node and the players forward direction is less than the players cameras field of view, and this is the first node we check, set the tag to 10. Then if a linecast between the players eyes and the node returns true, we will add the current value of our for loop plus 1 to the tag and break out of the loop. (See the below code snippet if this doesn't make sense)
    Code (CSharp):
    1. g.Tag = 0;                                                                            //Reset the nodes tag
    2.                 for(int i = 1; i >= 0; i--)    {                                                        //Do all this twice:
    3.                     #region LoS
    4.                     if(a < player.GetComponentInChildren<Camera>().fieldOfView && i == 1)    {        //Assumes both points are in sight if one is
    5.                         g.Tag = 10;                                                                    /*LoS        |    (11 - Crouching Cover in LoS)    |    (01 - Crouching Cover outside LoS)
    6.                                                                                                                 |    (12 - Standing Cover in LoS)    |    (02 - Standing Cover outside LoS)
    7.                                                                                                                 |    (10 - No Cover in LoS            |    (00 - No Cover outside LoS)
    8.                                                                                                                 */
    9.                     }
    10.                     #endregion
    11.                     #region Cover
    12.                     if(Physics.Linecast(pos, p+(Vector3.up*i)+(Vector3.up*.25f), l))    {            //Raycast from player to node, decide height based on loop value
    13.                         g.Tag += (uint)i+1;                                                            //Cover     |    (01 - cover crouching)    | (02 - cover standing)
    14.                         break;                                                                        //NoCover    |    (00 - no cover)            | (01 - no standing cover)
    15.                     }
    16.                     #endregion
    17.                 }


    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using Pathfinding;
    5.  
    6. public class Squad : MonoBehaviour {
    7.     public Transform player;
    8.     public List<myAIscript> agents = new List<myAIscript>();
    9.     public LayerMask l;
    10.     public float freq = .5f;
    11.  
    12.     internal GridNode[] nodes;
    13.  
    14.     void Awake()    {
    15.         foreach(myAIscript ai in agents)    {
    16.             ai.gameObject.SetActive(true);        //Good idea
    17.             ai.sq = this;
    18.         }
    19.  
    20.         StartCoroutine(LoS());
    21.     }
    22.  
    23.     IEnumerator LoS()    {
    24.         yield return new WaitForEndOfFrame();
    25.         nodes = AstarPath.active.astarData.gridGraph.nodes;
    26.  
    27.         while(true)    {
    28.             Vector3 pos = player.position;
    29.             Vector3 fw = player.forward*0.1f;
    30.  
    31.             foreach(GridNode g in nodes)    {
    32.                 if(!g.Walkable)
    33.                     continue;
    34.  
    35.                 Vector3 p = new Vector3(((Vector3)g.position).x, fw.y, ((Vector3)g.position).z);    //Node position
    36.                 float d = Vector3.Distance(pos, p);                                                    //Distance from player to node
    37.                 float a = Vector3.Angle(fw, p-pos);                                                    //Angle between players forward direction and node relative to player
    38.  
    39.                 uint tagscore = 0;
    40.                 for(int i = 1; i >= 0; i--)    {                                                        //Do all this twice
    41.                     #region Cover
    42.                     if(Physics.Linecast(pos, p+(Vector3.up*i)+(Vector3.up*.25f), l))    {            //Raycast from player to node, decide height based on loop value
    43.                         tagscore += (uint)i+1;                                                        //Cover     |    (01 - cover crouching)    | (02 - cover standing)
    44.                     }                                                                                //NoCover    |    (00 - no cover)            | (01 - no standing cover)
    45.                     #endregion
    46.                     #region LoS
    47.                     if(a < player.GetComponentInChildren<Camera>().fieldOfView && i == 1)    {        //Assumes both points are in sight if one is
    48.                         tagscore += 10;                                                                /*LoS        |    (11 - Crouching Cover in LoS)    |    (01 - Crouching Cover outside LoS)
    49.                                                                                                                 |    (12 - Standing Cover in LoS)    |    (02 - Standing Cover outside LoS)
    50.                                                                                                                 |    (10 - No Cover in LoS            |    (00 - No Cover outside LoS)
    51.                                                                                                                 */
    52.                     }
    53.                     #endregion
    54.                 }
    55.                 g.Tag = tagscore;
    56.             }
    57.  
    58.             yield return new WaitForSeconds(freq);
    59.             yield return null;
    60.         }
    61.     }
    62. }
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using OptionStacker;
    5. using Pathfinding;
    6.  
    7. public class myAIscript : MonoBehaviour, IDamageable {
    8.     #region Variables
    9.     private ExtendedStacker stack = new ExtendedStacker();
    10.     internal Seeker seeker;
    11.     internal AIPath agent;
    12.     internal Squad sq;
    13.  
    14.     private Transform target;
    15.     private float distance;
    16.  
    17.     private Vector3 lastPrivateSpot;
    18.     private float spot;
    19.  
    20.     private Vector3[] point;
    21.     private int indexer;
    22.  
    23.     [Header("General")]
    24.     public Transform player;
    25.     public Transform head;                //NOTE: AI's head
    26.     public LayerMask seeMask;
    27.     public float timeBeforeSpotted = 3;
    28.     public float health = 100;
    29.  
    30.     [Header("Patrol")]
    31.     public GameObject patrolPath;
    32.     public float patrolSpd = 1;
    33.  
    34.     [Header("Alert")]
    35.     public float alertSpeed = 0.75f;
    36.     private bool inSight;
    37.     #endregion
    38.  
    39.     void Start() {
    40.         #region Get Agent
    41.         seeker = GetComponentInChildren<Seeker>();
    42.         agent = GetComponentInChildren<AIPath>();
    43.         target = new GameObject(gameObject.name + "'s pathfinding target").transform;
    44.         target.position = transform.position;
    45.         agent.target = target;
    46.         #endregion
    47.      
    48.         #region Set up Patrol Path
    49.         List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
    50.         foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>())    {
    51.             if(p != patrolPath.transform)    {
    52.                 size.Add(p.position);
    53.             }
    54.         }
    55.         point = size.ToArray();
    56.         #endregion
    57.      
    58.         #region Simple Option Stacker
    59.         stack.StartStack(this);
    60.         stack.PushState("Patrol");
    61.         #endregion
    62.     }
    63.  
    64.     void Update() {
    65.         distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
    66.      
    67.         stack.Update();
    68.     }
    69.  
    70.     #region Patrol
    71.     void OnPatrol() {
    72.         agent.speed = patrolSpd;
    73.         stack.PopState();
    74.     }
    75.  
    76.     void Patrol() {
    77.         if(inSight || spot > 0)    {
    78.             stack.PushState("Alert");
    79.             return;
    80.         }
    81.  
    82.         if(agent.TargetReached && distance <= agent.endReachedDistance) {
    83.             indexer++;
    84.             if(indexer >= point.Length) indexer = 0;
    85.             target.position = point[indexer];
    86.         }
    87.     }
    88.     #endregion
    89.     #region Alert
    90.     public void OnAlert()    {
    91.         agent.speed = alertSpeed;
    92.         stack.PopState();
    93.     }
    94.  
    95.     public void Alert()    {
    96.         if(inSight)    {
    97.             spot+=1*Time.deltaTime;
    98.         } else {
    99.             spot-=0.5f*Time.deltaTime;
    100.         }
    101.      
    102.         if(spot < timeBeforeSpotted && spot > 0)    {
    103.             if(spot > 0.25f)    {
    104.                 target.position = lastPrivateSpot;
    105.             }
    106.         } else if(spot >= timeBeforeSpotted)    {
    107.             spot = timeBeforeSpotted;
    108.             stack.PopState();
    109.             stack.PushState("Combat");
    110.         } else {
    111.             agent.speed = patrolSpd;    //OnReturnPatrol()
    112.             spot = 0;
    113.             stack.PopState();
    114.         }
    115.     }
    116.     #endregion
    117.  
    118.     #region Vision
    119.     void OnTriggerStay(Collider o) {
    120.         inSight = false;
    121.  
    122.         Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(head.GetComponent<Camera>());
    123.         if(!GeometryUtility.TestPlanesAABB(frustumPlanes, o.transform.FindChild("EthanBody").GetComponent<SkinnedMeshRenderer>().bounds))    {    //I am using the Ethan model, which is part of the unity standard assets!
    124.             return;
    125.         }
    126.  
    127.         Vector3 dir = player.position - head.position;
    128.         float d = Vector3.Distance(player.position, head.position);
    129.      
    130.         if(Physics.Raycast(head.position, dir, d, seeMask))    {
    131.             return;
    132.         } else {
    133.             inSight = true;
    134.         }
    135.     }
    136.     #endregion
    137.  
    138.     #region Damage
    139.     public void TakeDamage(float damage)    {
    140.         health -= damage;
    141.  
    142.         if(health > 0)    {
    143.             stack.PushState("ReactToDamage");
    144.         } else {
    145.             Die();
    146.         }
    147.     }
    148.  
    149.     IEnumerator ReactToDamage()    {
    150.         Debug.Log("Ouch!");
    151.         yield return new WaitForSeconds(2);    //Simulate reaction
    152.         stack.PopState();
    153.     }
    154.  
    155.     void Die() {
    156.         DestroyImmediate(gameObject);
    157.     }
    158.     #endregion
    159. }


    Suggestion List:
    Refrence multiple kinds of AI in the same list -
    Assign every AI in the scene to this list if it is empty -
    Make your AIs avoid any spot where any other AI has previously been damaged in -
    Only take cover at edge nodes to ensure cover is being taken next to e.g. a wall -
    Reserve coverspots, so no agents will ever try to take the same coverspot -
    Tweak line of sight frequency/accuracy variables through an options menu ingame -
    Fire the line of sight check every X seconds instead of X plus time-taken-to-complete seconds -
     
    Last edited: Nov 3, 2015
  9. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    I must personally apologize for the long wait! This tutorial is no longer being worked on as I simply dont have the time. Besides, forum tutorials are not a great idea due to the sheer inefficiency of reading and writing long explanations.

    Thank you for reading,
    Angelo
     
    Last edited: Jan 14, 2018
    Faahhx likes this.