Search Unity

Simple Option Stacker - get started! API & Examples

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

  1. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    SimpleOptionStackerLogo1-624x225.png

    Welcome, ladies and gentlement, to a tutorial series explaining how to use our new asset store tool, named the Simple Option Stacker!


    What are Option Stacks?

    An Option Stack is a system often used for the AI in most AAA games. Most commonly used in coherence with the classical Finite State Machine architecture, and the more advanced planner architecture, but also with most other architectures. As you probably know by now, most AIs are built up using states or behaviours. Each state defines how the AI should react to the current situation, and depending on the arcitecture, what conditions lead to other behaviours.

    An Option Stack keeps a list of states that the AI is currently in. On top of the stack is the most recently called state. (Normally you would only execute the state that is ontop of the stack.)
    This means all other states would temporarily suspend while the top state is being executed.

    Let's look at this example:
    An RTS AI unit is ordered to attack player 2's base. This will push the state "AttackBuilding()" to the top of the stack. However the unit is not whitin range, so it will also push "GoToLocation()" ontop of that. This means that once GoToLocation has finished, it will be removed from the stack, and the unit will proceed to AttackBuilding(). This can easily be hardcoded into an FSM, but things get exponentially more difficult when things get more complex.

    Since while the unit is in the GoToLocation state, it walks into an ambush set up by player 3! Now the unit will push something like "ReactToAttack". The first thing ReactToAttack requires is to take cover, so lets have that as a seperate state. That means "TakeCover" will be pushed ontop of the stack. But on its way to cover, the unit notices a live grenade thrown at its feet! Now it will push "ReactToGrenade" ontop of that. A state where the AIs only goal is to survive the grenade attack. The stack now looks like this:​
    "AttackBuilding < GoToLocation < ReactToAttack < TakeCover < ReactToGrenade"
    The units ultimate goal is to complete the task on the very left, but to do so it must first complete every task in order from the right to the left!​



    Option Stack principles.

    Every Option Stack system MUST contain 3 simple methods:
    1. PushState - This will put the specified state ontop of the stack.
    2. PopState - This will remove the top state from the stack.
    3. GetTop - This will return the top state of the stack.

    Every behaviour MUST pop itself after it has completed or is no longer relevant. This is easy, as any state can only pop itself using PopState, because only the top state can be exectued.

    Note that at the moment our Simple Option Stacker natively supports states as methods and coroutines, also known as voids and IEnumerators, but the system can easily be modified to work with classes!



    Tutorial post list:
    Basic
    1.1
    How to start using Simple Option Stacker Basic
    1.2 Features included in the basic edition





    Remember that the newest version of Simple Option Stacker always will be available at lustrousgames.com/downloads before the asset store!

    If you have found any problems with the pack, found an insignificant spelling error in a thread, or you want to make a suggestion/request for the next patch, feel free to post them on this, or preferrably this other thread, or send me a private message!

    If you do not own Simple Option Stacker, feel free to purchase the package at either our website or on the asset store!
     
    Last edited: Jan 1, 2016
  2. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    1.1: How to start using Simple Option Stacker Basic

    Simple Option Stacker Basic contains only the essential Option Stack methods, and therefor is significantly cheaper than its extended counterpart.

    To begin using any Simple Option Stacker version, you have to include its directive at the beginning of your scripts! This is very simple:
    Code (CSharp):
    1. using OptionStack;
    The second step in using Simple Option Stacker is to create a new stack as a variable. To start a Basic Stack put this in your scripts variable declaration:
    Code (CSharp):
    1. public BasicStacker stack = new BasicStacker();
    The last step in setting up Simple Option Stacker is to make sure the top state is beign executed as it should be. Normally this normally means calling the state every frame. As Extended 1.2 makes this alot easier, we will not go into differing between coroutines and invoking states, just remember that SOS's getTop() function will return the top states name as a string! To execute a state method every frame we put this inside the scripts Update():
    Code (CSharp):
    1. Invoke(stack.getTop(), 0);
    2. //Or if you want to start a coroutine:
    3. StartCoroutine(stack.getTop());
    With Simple Option Stacker Extended you can easily update the stack using stack.Update() instead of manually invoking the state. Simple Option Stacker Extended, the only version currently available on the Asset Store, also will automatically pause your coroutines when they are not on top of the stack, and resume them where they left of when they are.
     
    Last edited: Jan 1, 2016
  3. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    1.2: Basic features

    Continuing with the written script, looking something like this:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using OptionStacker;                                         //Include OptionStacker
    4.  
    5. public class BasicExample : MonoBehaviour {
    6.     internal BasicStacker stack = new BasicStacker();        //Make a new stack
    7.  
    8.     void Update()    {
    9.         Invoke(stack.getTop(), 0);                           //Manually invoke top state
    10.     }
    11. }

    Simple Option Stacker Basic contains 4 features:
    1. pushState(string nameOfState)

    - Calling pushState("StateHere") will push the specified state to the top of the stack. Method(string).
    2. popState()
    - Calling popState() will remove the top state from the stack. Method().
    3. getTop()
    - Calling getTop() will return the state ontop of the stack. Returns string.
    4. stackLength()
    - Calling stackLength() will return the length of the stack. Returns int.
     
    Last edited: Jan 1, 2016
  4. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Simple Option Stacker Extended is now available at the asset store!
     
  5. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Is there any sort of video tutorial of using this for a simple character or webplayer to try out? It looks good on paper but without anything really tangible, its hard to discern if this is something that will or will not help me. Thanks.
     
  6. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    @Ghosthowl Im working on it. I want to put the RTS example scene on the webplayer, but I have to make a few minor changes to make that possible.

    Also I have a video planned, but actually making it hasn't been so easy, due to a lack of time and several problems with my PC. I should be able to get this up on youtube by the end of next week. This video will be how you can make a fairly advanced stealth action AI with the help of Simple Option Stacker, because I already have made this for a previous project. But if there are any other ideas, I see no reason why I wouldn't be able to make more examples!

    For the time being I can post the AI for the player controlled unit in the RTS example. It might be a bit complicated to read the attack section. That has nothing to do with Simple Option Stacker, but rather I don't really know how to efficiently work with coroutines! My apologizes.
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using OptionStacker;                                        //Include OptionStacker package directive
    5.  
    6. [AddComponentMenu("AI/Simple Option Stacker/Examples/RTS AI")]
    7. public class RTSAIScript : MonoBehaviour, IDamageable_SOS {
    8.     ExtendedStacker stack = new ExtendedStacker();            //Make a new Extended Stacker and call it 'stack'
    9.     NavMeshAgent agent;
    10.  
    11.     [Tooltip("What color will this AIs gunshots be?")]
    12.     public Color attackColor = Color.blue;
    13.     [Tooltip("How much health does this AI have?")]
    14.     public float health = 100;
    15.     [Tooltip("What is the maximum attack range for this AI?")]
    16.     public float attackRange = 5;
    17.     [Tooltip("How much damage does this AI do per shot?")]
    18.     public float damage = 10;
    19.     [Tooltip("How many bursts this unit fires before reloading.")]
    20.     public int bursts = 3;
    21.     [Tooltip("How many seconds does this AI use to reload?")]
    22.     public float reload = 1;
    23.  
    24.     float IDamageable_SOS.Health    {
    25.         get    { return health; }
    26.     }
    27.  
    28.     [Header("Observe - Don't touch")]
    29.     [Tooltip("This is the stack itself. The AI executes the behaviours from the bottom of the inspector to the top.")]
    30.     public List<string> currentStack;
    31.     [Tooltip("Fairly self-explanatory. Is this unit selected or not?")]
    32.     public bool isSelected;
    33.  
    34.     private Vector3 startPos;
    35.     private Vector3 endPos;
    36.  
    37.     void Awake()    {
    38.         agent = GetComponent<NavMeshAgent>();
    39.  
    40.         stack.StartStack(this);                                //Initialize the stack
    41.         stack.PushState("Idle");                            //Push default state
    42.     }
    43.  
    44.     #region Selection
    45.     void Update()    {
    46.         if(Input.GetMouseButton(0) || Input.GetMouseButtonUp(0))    {
    47.             if(Input.GetMouseButtonDown(0))    {
    48.                 startPos = Input.mousePosition;
    49.             } else if(Input.GetMouseButtonUp(0)) {
    50.                 endPos = Input.mousePosition;
    51.                 CheckTarget();
    52.             }
    53.         }
    54.  
    55.         if(Input.GetMouseButtonUp(1)) {
    56.             isSelected = false;
    57.         }
    58.  
    59. //        if(stack.getTop == null)    {                        //This is sometimes a necessary check if the stack is reset at any time in your script. Use stack.SetBack() to preserve the base behaviour!
    60. //            stack.PushState("Idle");                        //Idle is the default state
    61. //        }
    62.  
    63.         stack.Update();                                        //Update the stack
    64.         currentStack = stack.getStack;                        //Display the entire stack in the inspector
    65.     }
    66.  
    67.     //This method will check what the player clicked
    68.     void CheckTarget()    {
    69.         if(startPos.sqrMagnitude > endPos.sqrMagnitude + 100 || endPos.sqrMagnitude > startPos.sqrMagnitude + 100)    {    //If this selection is a drag select
    70.             #region Drag Select
    71.             Camera mainCam = GameObject.Find("Main Camera").GetComponent<Camera>();                    //Refrence to the camera
    72.             Ray rayX = mainCam.ScreenPointToRay(startPos);                                            //Convert the click position to world coordinates...
    73.             Ray rayY = mainCam.ScreenPointToRay(endPos);                                            //... Through raycasting
    74.             RaycastHit hitX, hitY;
    75.             if(Physics.Raycast(rayX, out hitX))    {
    76.                 startPos = hitX.point;
    77.                 if(Physics.Raycast(rayY, out hitY))    {
    78.                     endPos = hitY.point;
    79.                 } else {
    80.                     isSelected = false;
    81.                     return;
    82.                 }  
    83.             } else {
    84.                 isSelected = false;
    85.                 return;
    86.             }
    87.  
    88.             float tx = transform.position.x;
    89.             float x1 = startPos.x;
    90.             float x2 = endPos.x;
    91.             if((x1 > tx && tx > x2) || (x2 > tx && tx > x1))    {                                    //If this AIs position is inside the drag selection on the X axis...
    92.                 float tz = transform.position.z;                                                    //We are converting from a Vector2 to a Vector3, so the orignal Y is actually the new Z
    93.                 float z1 = startPos.z;
    94.                 float z2 = endPos.z;
    95.                 if((z1 > tz && tz > z2) || (z2 > tz && tz > z1))    {                                //... And on the Y axis
    96.                     isSelected = true;
    97.                 } else {
    98.                     isSelected = false;
    99.                     return;
    100.                 }
    101.             } else {
    102.                 isSelected = false;
    103.                 return;
    104.             }
    105.             #endregion
    106.         } else {                                                                                                        //Or if this selection is a click
    107.             #region Click Select
    108.             RaycastHit hit;
    109.             Camera mainCam = GameObject.Find("Main Camera").GetComponent<Camera>();                        //Refrence to the camera
    110.             Ray rayFromCam = mainCam.ScreenPointToRay(Input.mousePosition);                                //Make a ray from the clicked position
    111.             if(Physics.Raycast(rayFromCam, out hit))    {
    112.                 string tag = hit.transform.tag.ToLower();                                                //Store the tag of the hit object for further
    113.              
    114.                 if(hit.transform == transform)    {                                                        //Select this AI if it is clicked
    115.                     isSelected = true;
    116.                 } else {
    117.                     if(tag == "ground" && isSelected)    {                                                            //If the hit object is walkable, walk there
    118.                         if(Input.GetKey(KeyCode.LeftShift))    {
    119.                             if(stack.getTop == "WalkToLocation")    {
    120.                                 stack.QueState("WalkToLocation", stack.getAmount("WalkToLocation"), hit.point);        //Que command if shift is held  
    121.                             } else {
    122.                                 stack.QueState("WalkToLocation", stack.getLength-1, hit.point);
    123.                             }
    124.                         } else {
    125.                             stack.SetBack();                                                                //Replace the entire stack if shift isn't held down
    126.                             stack.PushState("WalkToLocation", hit.point);                                    //SetBack will reset the entire stack except for the bottom state
    127.                         }
    128.                     } else if(tag == "enemy" && isSelected)    {                                            //If the hit object is an enemy, attack it
    129.                         if(Input.GetKey(KeyCode.LeftShift))    {
    130.                             stack.QueState("AttackTarget", stack.getLength-1, hit.transform);
    131.                         } else {
    132.                             stack.SetBack();
    133.                             stack.PushState("AttackTarget", hit.transform);  
    134.                         }
    135.                     } else {
    136.                         if(tag == "player" && !Input.GetKey(KeyCode.LeftShift))    {                                                            //Can select multiple units if shift is held down
    137.                             isSelected = false;
    138.                         }
    139.                     }
    140.                 }
    141.             } else {
    142.                 isSelected = false;
    143.             }
    144.             #endregion
    145.         }
    146.     }
    147.     #endregion
    148.  
    149.     //In this script, every AI behaviour has been made public, this is NOT required!
    150.     #region Idle
    151.     public void Idle()    {
    152.         //Since this is the default state it doesn't need to pop itself
    153.     }
    154.     #endregion
    155.  
    156.     #region Walk to Location
    157.     public void OnWalkToLocation(Vector3 location)    {                                //OnFunctions/OnExitFunctions can take the same arguments as its main function, or none at all!
    158.         agent.SetDestination(location);
    159.         stack.PopState();
    160.     }
    161.  
    162.     public void WalkToLocation(Vector3 location)    {                                //Do nothing but walk to the selected location
    163.         Debug.DrawRay(location, Vector3.up, Color.blue);
    164.         if(Vector3.Distance(transform.position, location) < 0.75f)    {                //Pop the WalkToLocation command when the location is aproximatly reached
    165.             stack.PopState();
    166.         }
    167.     }
    168.     #endregion
    169.  
    170.     #region Attack
    171.                                                                                                     //Note that the On/OnExit methodtypes doesn't have to match the main methods type.
    172.     public void OnAttackTarget(Transform target)    {                                                //A void Something can have an IEnumerator OnSomething etc!
    173.         if(target.Equals(null) || target.gameObject.activeSelf == false)    {                        //If the target doesn't exist, dont attack it
    174.             stack.PopState();
    175.             stack.PopState();
    176.         } else {
    177.             if(Vector3.Distance(transform.position, target.position) > attackRange)    {                    //Check if the target is in attack range
    178.                 NavMeshHit navHit;
    179.                 Vector3 direction = (target.position - transform.position).normalized*(attackRange-1f);    //Prepear to find the shortest path to being in range of the target
    180.                 if(NavMesh.SamplePosition(target.position - direction, out navHit, 50, -1))    {            //Make sure the new position is on a navmesh
    181.                     stack.PushState("WalkToLocation", navHit.position);                                    //Walk to the target if its not in attack range
    182.                 }
    183.             } else {
    184.                 stack.PopState();                                                                        //If the target is in range, attack it
    185.             }  
    186.         }
    187.     }
    188.  
    189.     public IEnumerator AttackTarget(object[] parameters)    {                        //Coroutines can only contain a single argument, which also must be an array of objects
    190.         Transform target = parameters[0] as Transform;                                //A simple way to bypass this is to use the array of objects as the only parameter and explicitly convert the objects like this
    191.  
    192.         if(target.Equals(null))    {
    193.             stack.PopState();
    194.             yield return false;
    195.         }
    196.  
    197.         IDamageable_SOS idmg = null;                                                                    //Check if the target implements the interface IDamagable
    198.  
    199. //        if(parameters.Length > 1)    {                                                                    //This is a simple way of doing optional arguments with coroutines
    200. //            idmg = parameters[1] as IDamageable_SOS;                                                    //We do this because we restart the coroutine, and we don't want to redo the interface check
    201. //        } else {
    202.             foreach(MonoBehaviour checkInterface in target.gameObject.GetComponents<MonoBehaviour>())    {    //Find all scripts on the target and check for the interface
    203.                 if(checkInterface is IDamageable_SOS)    {                                                    //If target is damageble
    204.                     idmg = checkInterface as IDamageable_SOS;                                                //Assign interface
    205.                     break;                                                                                    //Only find the first damagable script
    206.                 }
    207.             }  
    208. //        }
    209.  
    210.         yield return new WaitForEndOfFrame();                                        //Give the target some time to Destroy() itself before attacking it
    211.  
    212.         if(target.Equals(null) || idmg == null || idmg.Health <= 0)    {                //If the target doesn't exist anymore or isn't damageable or is destroyed
    213.             stack.PopState();                                                        //Stop attacking
    214.             yield return false;
    215.         }
    216.  
    217.         yield return new WaitForSeconds(.25f);                                        //First attack isn't instant
    218.         object[] args = new object[]{ target, idmg};                                //Prepare the list of arguments for FireAt();
    219.         for(int i = 0; i <= bursts; i++)    {
    220.             stack.QueState("FireAt", i, args);                                        //QueState can work like PushState if the 'by amount' int is set to 0
    221.         }
    222.  
    223.         for(int inf = 0; inf < 1; inf--)    {                                        //Use an infinite loop and the option stack to pause the coroutine while firing
    224.             yield return new WaitForEndOfFrame();
    225.  
    226.             if(stack.getTop == "AttackTarget")    {                                    //Don't keep reloading if this isn't the current behaviour
    227.                 break;
    228.             } else {
    229.                 inf = 0;
    230.             }
    231.         }
    232.  
    233.         yield return new WaitForSeconds(reload-.25f);                                //Fire 3 bursts before reloading
    234.      
    235.         if(idmg.Health > 0 && !target.Equals(null))    {                                //If the target is still alive
    236.             stack.QueState("AttackTarget", 1, parameters);                            //Restart the coroutine manually
    237.             stack.QueState("Stun", 1, 0.22f);                                        //Repurpose a behaviour! Give the game a frame to update when restarting coroutines manually!
    238.         }
    239.  
    240.         stack.PopState();
    241.     }
    242.  
    243.     public IEnumerator FireAt(object[] parameters)    {                                //Will fire at the first parameters position, and do damage to the second parameter.
    244.         Transform target = parameters[0] as Transform;                                //Get the target
    245.         IDamageable_SOS idmg = parameters[1] as IDamageable_SOS;                    //Get what script to damage
    246.  
    247.         yield return new WaitForEndOfFrame();
    248.  
    249.         if(target.Equals(null) || idmg.Health <= 0)    {                                //Make sure the target isn't already destroyed
    250.             stack.PopState();
    251.             yield return false;
    252.         }
    253.  
    254.         Debug.DrawLine(transform.position, target.position, attackColor, 0.225f);    //Temporary quickfix for displaying laserbeams
    255.         idmg.TakeDamage(damage, gameObject);                                        //Do the damage
    256.  
    257.         yield return new WaitForSeconds(0.25f);                                        //Wait a bit for realism
    258.         stack.PopState();                                                            //Finish this behaviour
    259.     }
    260.     #endregion
    261.  
    262.     #region Health
    263.     public void TakeDamage(float dmg, GameObject attacker)    {
    264.         health -= dmg;                                                                //Damage the unit when it is attacked
    265.  
    266.         if(health <= 0)    {                                                            //See if the unit should die
    267.             Destroy(gameObject);                                                    //Kill the unit
    268.         }
    269.  
    270.         if(stack.getTop != "AttackTarget" && stack.getTop != "Stun")    {            //Only do this if the AI isn't in combat already:
    271.             stack.PushState("AttackTarget", new object[1]{attacker.transform});        //Start attacking the attacker
    272.             stack.PushState("Stun", dmg/10);                                        //Stun the unit when its attacked
    273.         }
    274.     }
    275.  
    276.     public IEnumerator Stun(object[] sec)    {                        //Stun the unit and suspend all actions for S seconds
    277.         float s = (float)sec[0];                                    //Get S seconds from the parameters
    278.         yield return new WaitForSeconds(s);                            //Wait S seconds
    279.         stack.PopState();                                            //Finish the stun
    280.         stack.StartCooldown("Stun", s);                                //When a unit has been stunned, they cannot be stunned again for atleast X seconds
    281.     }
    282.     #endregion  
    283. }
    Code (CSharp):
    1. //This is required for taking/dealing damage
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. public interface IDamageable_SOS    {
    7.     float Health { get; }
    8.  
    9.     void TakeDamage(float dmg, GameObject attacker);
    10. }
     
  7. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    In the progress of making a full stealth AI tutorial series! This series uses some of the Simple Option Stacker features, so check it out if you have time. Click here to check it out!
     
  8. Lahzar

    Lahzar

    Joined:
    May 6, 2013
    Posts:
    87
    Update 1.4 is live! API coming ASAP.

    @Ghosthowl I made a very simplistic example, and uploaded it as a webplayer! Click here to try it out.
    Click to move. Shift-Click to que moves. Press F2 to make the scout wait for 2 seconds. The scout will wait for 1 second after every move, to showcase the OnExitFunction.
     
    Last edited: Jan 1, 2016