Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Spaceship Enemy AI

Discussion in 'Scripting' started by Cyrussphere, Sep 10, 2016.

  1. Cyrussphere

    Cyrussphere

    Joined:
    Jul 1, 2016
    Posts:
    129
    Hey all,

    So I am finally trying to tackle enemy AI..one of the things I've been dreading the most. There are tons of AI stuff out there but not so much on Space Sim type game play. I found a tutorial out there for AI avoiding obstacles which was fairly useful but aside from that I haven't successfully found anything online that could help with a proper behavior of an enemy spaceship when it encounters the player.

    So far I have the enemy waiting until the player is within a set distance, it then heads towards the player while avoiding obstacle which is great. But I am having an issue trying to wrap my head around the behavior beyond that. So far the ship just heads to the player and currently just flies in an orbit around the player. Unfortunately I am not quite sure how to have it react and how to code it..I imagine the ship wants to fly past the player, turn around at some point and then fire again? Was hoping someone could point me to a better tutorial or give some helpful advise on how to tackle this monster..

    Here is my code so far;
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. public class Enemy_Movement : MonoBehaviour
    7. {
    8.     private Transform target;
    9.     private Transform myTransform;
    10.  
    11.     Vector3 storeTarget;
    12.     Vector3 newTargetPos;
    13.     bool savePos;
    14.     bool overrideTarget;
    15.     Transform obstacle;
    16.     public List<Vector3> EscapeDirections = new List<Vector3>();
    17.  
    18.     //AI Checks
    19.     public bool chasing = false;
    20.     public bool evading = false;
    21.  
    22.     //AI stuff
    23.     public float rotationSpeed;
    24.     public float maxDist;
    25.     public float minDist;
    26.     public float fireDist;
    27.  
    28.     //AI Speeds
    29.     public float moveSpeed;
    30.  
    31.     void Start()
    32.     {
    33.         target = GameObject.FindGameObjectWithTag("Player").transform;
    34.         myTransform = this.transform;
    35.     }
    36.  
    37.     void FixedUpdate ()
    38.     {
    39.         //Find Distance to target
    40.         var distance = (target.position - myTransform.position).magnitude;
    41.  
    42.         if(chasing)
    43.         {
    44.             //Rotate to look at player
    45.             myTransform.rotation = Quaternion.Slerp(myTransform.rotation, Quaternion.LookRotation(target.position - myTransform.position), rotationSpeed * Time.deltaTime);
    46.             //Move towards the player
    47.             myTransform.position += myTransform.forward * moveSpeed * Time.deltaTime;
    48.  
    49.             //Moves too far away from player
    50.             if (distance > maxDist)
    51.             {
    52.                 chasing = false;
    53.             }
    54.  
    55.             //Attack if close enough
    56.             if (distance < fireDist)
    57.             {
    58.                 //insert attack commands here when ready
    59.             }
    60.  
    61.             if (distance < minDist)
    62.             {
    63.                 myTransform.rotation = Quaternion.Slerp(myTransform.rotation, Quaternion.LookRotation(myTransform.position - target.position), rotationSpeed * Time.deltaTime);
    64.                 myTransform.position += myTransform.forward * moveSpeed * Time.deltaTime;
    65.             }
    66.         }
    67.         else
    68.         {
    69.             if (distance < maxDist)
    70.             {
    71.                 chasing = true;
    72.             }
    73.         }
    74.  
    75.         ObstacleAvoidance(transform.forward, 0);
    76.  
    77.     }
    78.        
    79.     void ObstacleAvoidance(Vector3 direction, float offsetX)
    80.     {
    81.         RaycastHit[] hit = Rays(direction, offsetX);
    82.  
    83.         for(int i = 0; i < hit.Length -1; i++)
    84.         {
    85.             //So we dont detect ourself as a hit collision
    86.             if (hit[i].transform.root.gameObject != this.gameObject)
    87.             {
    88.                 if(!savePos)
    89.                 {
    90.                     storeTarget = target.position;
    91.                     obstacle = hit[i].transform;
    92.                     savePos = true;
    93.                 }
    94.  
    95.                 FindEscapeDirections(hit[i].collider);
    96.             }
    97.         }
    98.  
    99.         if (EscapeDirections.Count > 0)
    100.         {
    101.             if (!overrideTarget)
    102.             {
    103.                 newTargetPos = getClosests();
    104.                 overrideTarget = true;
    105.             }
    106.         }
    107.         float distance = Vector3.Distance(transform.position, target.position);
    108.  
    109.         if(distance < 5)
    110.         {  
    111.             if (savePos)
    112.             {
    113.                 //if we reach the target
    114.                 target.position = storeTarget;
    115.                 savePos = false;
    116.             }
    117.             else
    118.             {
    119.                 //if we had waypoints
    120.                 //index++
    121.             }
    122.             overrideTarget = false;
    123.             EscapeDirections.Clear();
    124.         }          
    125.     }
    126.  
    127.     Vector3 getClosests()
    128.     {
    129.         Vector3 clos = EscapeDirections[0];
    130.         float distance = Vector3.Distance(transform.position, EscapeDirections[0]);
    131.  
    132.         foreach(Vector3 dir in EscapeDirections)
    133.         {
    134.             float tempDistance = Vector3.Distance(transform.position, dir);
    135.             if(tempDistance < distance)
    136.             {
    137.                 distance = tempDistance;
    138.                 clos = dir;
    139.             }
    140.         }
    141.         return clos;
    142.  
    143.     }
    144.  
    145.     void FindEscapeDirections(Collider col)
    146.     {
    147.         //Check for obstacles above
    148.         RaycastHit hitUp;      
    149.         if(Physics.Raycast(col.transform.position, col.transform.up, out hitUp, col.bounds.extents.y * 2 + 5))
    150.         {
    151.  
    152.         }
    153.         else
    154.         {
    155.             //if there is something above
    156.             Vector3 dir = col.transform.position + new Vector3(0, col.bounds.extents.y * 2 + 5, 0 );
    157.  
    158.             if (!EscapeDirections.Contains(dir))
    159.             {
    160.                 EscapeDirections.Add(dir);
    161.             }
    162.         }
    163.  
    164.         //Check for obstacles below
    165.         RaycastHit hitDown;    
    166.         if(Physics.Raycast(col.transform.position, -col.transform.up, out hitDown, col.bounds.extents.y * 2 + 5))
    167.         {
    168.  
    169.         }
    170.         else
    171.         {
    172.             //if there is something below
    173.             Vector3 dir = col.transform.position + new Vector3(0, -col.bounds.extents.y * 2 - 5, 0 );
    174.  
    175.             if (!EscapeDirections.Contains(dir))
    176.             {
    177.                 EscapeDirections.Add(dir);
    178.             }
    179.         }
    180.         //Check for obstacles to the Right
    181.         RaycastHit hitRight;      
    182.         if(Physics.Raycast(col.transform.position, col.transform.right, out hitRight, col.bounds.extents.x * 2 + 5))
    183.         {
    184.  
    185.         }
    186.         else
    187.         {
    188.             //if there is something to the right
    189.             Vector3 dir = col.transform.position + new Vector3(col.bounds.extents.x * 2 + 5, 0, 0 );
    190.  
    191.             if (!EscapeDirections.Contains(dir))
    192.             {
    193.                 EscapeDirections.Add(dir);
    194.             }
    195.         }
    196.         //Check for obstacles to the Left
    197.         RaycastHit hitLeft;    
    198.         if(Physics.Raycast(col.transform.position, -col.transform.right, out hitLeft, col.bounds.extents.x * 2 + 5))
    199.         {
    200.  
    201.         }
    202.         else
    203.         {
    204.             //if there is something to the right
    205.             Vector3 dir = col.transform.position + new Vector3(-col.bounds.extents.x * 2 - 5, 0, 0 );
    206.  
    207.             if (!EscapeDirections.Contains(dir))
    208.             {
    209.                 EscapeDirections.Add(dir);
    210.             }
    211.         }
    212.     }
    213.  
    214.     RaycastHit[] Rays(Vector3 direction, float offsetX)
    215.     {
    216.         Ray ray = new Ray(transform.position + new Vector3(offsetX,0,0), direction);
    217.         Debug.DrawRay(transform.position + new Vector3(offsetX,0,0), direction * 10 * moveSpeed, Color.red);
    218.  
    219.         float distanceToLookAhead = moveSpeed * 5;
    220.         //Adjust 5 to proper radius around object to pick up raycast hits
    221.         RaycastHit[] hits = Physics.SphereCastAll(ray, 5, distanceToLookAhead);
    222.  
    223.         return hits;      
    224.     }
    225.  
    226.  
    227. }
    228.  
     
  2. ajaykewat

    ajaykewat

    Joined:
    Jul 25, 2012
    Posts:
    22
  3. takatok

    takatok

    Joined:
    Aug 18, 2016
    Posts:
    1,496
    That page is a great tutorial, but its for basically for a 1D space shooter. Where the you just scroll side to side and shoot things that come from the top of the screen.

    I assume from your code and comments this is a full 2D (or maybe fully 3D?) space game?

    You already have the basis of a finite state machine going on.
    State 0: Initial State -> Does nothing.
    If (distanceToPlayer < maxDist) -> Transition to State 1
    State 1: Rotate towards player every Update, and move towards him
    if (distanceToPlayer > maxDist) -> Transition to State 0
    if (distanceToPlayer < min Dist) -> Transition to STate 2
    State 2: Rotate towards player and move to him. Then Rotate towards player and move to him a second time.
    if (distance to Player > min Dist) -> Transition to STate 1
    if (distance to Player > max Dist) -> Transition to State 0

    I'm not sure why State 2 you decide to double move each update.. but it what your code is doing :)

    Here is a pretty good article on how to use and implement a FSM:
    http://www.voidinspace.com/2013/05/a-simple-finite-state-machine-with-c-delegates-in-unity/

    If you refactored your code to this model, I think you find that defining more behaviours for your Enemy AI, will be easier to implement and if it gets complicated much easier to read than 3 pages of if statements in the Update() function.

    As to ideas of what to do when your Enemy gets close, you could have him move into a new State 3. When he's in this state he slows down to 80% of the player's speed and keeps pointing towards the player and shooting him. Then say after 10 seconds or if he moves too far behind, he switches to State 1.

    You could also implement code that checks his angle compared to the player's angle. If they are facing each other maybe he goes into State 4: accelerate super fast to zoom past the player for X seconds.. then goes into State 1 to hunt him again.

    I think switching to a FSM will help to logically structure your code, but to make the AI work well; you are probably going to have to implement some semi-complicated code that not only checks distance, but relative positions (Is the enemy in Quadrant I, II, III , or IV relative to the player. which way is the Enemy facing.) , relative speeds, etc.

    Another idea is instead of always pointing towards the player, try to calculate an intercept point. Based on the player's angle and speed figure out where he'll be in X seconds.. Point the AI there. Then when he gets close enough he switches out of Intercept State and goes into follow close and shoot state.
     
    Cyrussphere likes this.
  4. Cyrussphere

    Cyrussphere

    Joined:
    Jul 1, 2016
    Posts:
    129
    Sorry, when I first posted I was on the edge of coming down with a flu, i should have been more specific that this is for a 3d Space Sim, and yeah the space tutorial was a great tutorial when I went through it but need something more for the 3D AI setting :).

    Thanks for the link to the article, ill give it a good read. I am surprised there aren't too many tutorials on this type of AI, found a few that lead up to something but then just stop.
     
  5. takatok

    takatok

    Joined:
    Aug 18, 2016
    Posts:
    1,496
    If its fully 3D space shooter, try expanding your search to flight simulators and DogFigthing AI. it will be really close to what you want and I bet there is a million articles on these.
     
  6. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,353
    If your are looking for techniques to better organize your AI code, you might be interested by Behaviour Tree. I'm the author of Panda BT, a scripting framework based on this technique. I'll demonstrate how your AI can be implemented using this framework.

    Let's see how your AI can be build around a behaviour tree. Your AI consists of 3 main phases, which are:
    - Idle: the player is too far away, so just wait.
    - Approach: the player is close by, so approach him.
    - Attack: The player is within the attack range, so fire at him.

    In a behaviour tree, each of these phases can be mapped to a tree. You can think of a tree as some sort of function that get executed at an appropriate time.

    The goal of your AI is ultimately to attack the player, so this is the top priority action. When attacking the player is not possible (because the player is out of the attack range), the next best thing to do is to approach the player. If that's not possible, then, the only thing left to do is to idle. This type of priority list is implemented using a "fallback" node. In a BT script this is implemented as follow:
    Code (CSharp):
    1. fallback
    2.      tree("Attack")
    3.      tree("Approach")
    4.      tree("Idle")
    This is how your AI behave at its top level, or at it's bird eye view strategy. But we need to define what we mean by "Attack", "Approach" and "Idle", so we need to define these sub-trees.

    By attacking we mean, turn towards the player and fire. And that's when the player is within the attack range:
    Code (CSharp):
    1. tree("Attack")
    2.     while IsPlayerWithinAttackRange
    3.         sequence
    4.             RotateTowardsPlayer
    5.             Fire
    By approaching, we mean to continuously rotate towards the player and at the same time (in parallel) continuously move forwards:
    Code (CSharp):
    1. tree("Attack")
    2.     while IsPlayerWithinApproachRange
    3.         parallel
    4.             repeat RotateTowardsPlayer
    5.             repeat MoveForward
    Finally, by idling we mean... well doing nothing, so:
    Code (CSharp):
    1. tree("Idle")
    2.     Succeed
    3.  
    This is it for the definition of a behaviour tree. We just need to define IsPlayerWithinAttackRange, IsPlayerWithinApproachRange, RotateTowards_Player and MoveForward. Those are "tasks", which are the node in the tree that does the low level action, which are implemented as function in a MonoBehaviour. I've refactored your code in order to be used from a BT script (I've omitted the obstacle avoidance part in order to simplify this demonstration, but It could be easily added without complication). Here is what your MonoBehaviour could be simplified to:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using Panda;
    7.  
    8. public class EnemyAI : MonoBehaviour
    9. {
    10.     private Transform target;
    11.     private Transform myTransform;
    12.  
    13.     //AI stuff
    14.     public float rotationSpeed;
    15.     public float maxDist;
    16.     public float fireDist;
    17.  
    18.     //AI Speeds
    19.     public float moveSpeed;
    20.  
    21.     void Start()
    22.     {
    23.         target = GameObject.FindGameObjectWithTag("Player").transform;
    24.         myTransform = this.transform;
    25.     }
    26.  
    27.     [Task]
    28.     void RotateTowardsPlayer()
    29.     {
    30.         myTransform.rotation = Quaternion.Slerp(myTransform.rotation, Quaternion.LookRotation(target.position - myTransform.position), rotationSpeed * Time.deltaTime);
    31.     }
    32.  
    33.     [Task]
    34.     void MoveForward()
    35.     {
    36.         myTransform.position += myTransform.forward * moveSpeed * Time.deltaTime;
    37.     }
    38.  
    39.     [Task]
    40.     bool IsPlayerWithinApproachRange()
    41.     {
    42.         var distance = (target.position - myTransform.position).magnitude;
    43.         return distance < maxDist;
    44.     }
    45.  
    46.     [Task]
    47.     bool IsPlayerWithinAttackRange()
    48.     {
    49.         var distance = (target.position - myTransform.position).magnitude;
    50.         return distance < fireDist;
    51.     }
    52.  
    53.     [Task]
    54.     void Fire()
    55.     {
    56.         // Fire code here
    57.         Task.current.Succeed();
    58.     }
    59. }
    And for completness, this is the full BT script describing the spaceship behaviour:
    Code (CSharp):
    1. tree("root")
    2.     fallback
    3.         tree("Attack")
    4.         tree("Approach")
    5.         tree("Idle")
    6.    
    7. tree("Attack")
    8.     while IsPlayerWithinAttackRange
    9.         sequence
    10.             RotateTowardsPlayer
    11.             Fire
    12.        
    13. tree("Approach")
    14.     while IsPlayerWithinApproachRange
    15.         parallel
    16.             repeat RotateTowardsPlayer
    17.             repeat MoveForward
    18.        
    19. tree("Idle")
    20.     Succeed
    I'll be glad to help, if you have any question about using this framework or any question about Behaviour Tree in general.
     
    Last edited: Sep 13, 2016
    KevRev and LiterallyJeff like this.
  7. pixelone

    pixelone

    Joined:
    Apr 16, 2010
    Posts:
    157
    Was testing out this example you shared... It never Exits to Fire()? Why is that?
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,814
    Since this post and solution is nearly five years old, it's likely nobody will be able to answer this.

    To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run? what order does it run in?
    - what are the values of the variables involved? Are they initialized? Are the values reasonable?

    Knowing this information will help you reason about the behavior you are seeing.

    You can also put in Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene

    You could also just display various important quantities in UI Text elements to watch them change as you play the game.

    If you are running a mobile device you can also view the console output. Google for how on your particular mobile target.

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494
     
  9. pixelone

    pixelone

    Joined:
    Apr 16, 2010
    Posts:
    157
    Thank you for responding. I made a mistake. My Attack distance never evaluated to true simply because the value was set too low. It was never being ticked. You tips of Debug.Log() did help me rectify it. Even though this thread is old, it is still relevant. Prior to this my code look like spaghetti on steroids.
     
    Kurt-Dekker likes this.