3D Space AI

Hello Guys,

Yes, I know there are several threads about this concern, and I feel like I read everyone about it, but none has really helped me and I didn’t find a good solution to solve this.
I want to do a simple AI for a space game, which gets a target, approches it, shoots it, flies to a temp target to start a new attack run and repeat.

The problem I have is the obstacle Avoidance. For now, I am using a trigger to tell if an object is ahead and then use raycasts to figure out which location to go. This is not really working though. The fighters often get stuck on a collision mesh and behave weird.
The fighters have rigid body attached, to keep it simple I use drag to get some satisfying steering behavior.

Does anybody have any idea how I can improve the AI? Is there an alternate approach? Thank you very much!

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;

public class AIScript4 : NetworkBehaviour {

    public Transform target; // target the fighter is aiming for
    public Transform front;    //front of the fighter
    public Transform shipL;    //left wings of the fighter
    public Transform shipR;    //right wings of the fighter
    public Transform[] cannonEnds;    //Array in which the cannon-ends are listed
    public Transform bombSlot;

    public GameObject bulletPrefab; //Bullet which will be shot
    public GameObject ionShotPrefab;
    public GameObject explosionPrefab; //Explosion when the fighter gets destroyed

    public int ownTeam; //The team which will be targetted

    public int fireDistance; //range which the fighter can shoot
    public int sightDistance; // distance of the ray casts
    public int turnAwayDistance; //distance when the fighter turns away from the target and starts a new attack run
    public float speed; //speed of the fighter
    public float rotateSpeed; //speed of the rotation;
    public float rayAngle; //angle of the raycasts, optimized to save memory
    public int maxRayCasts;

    public bool  debug=true; //turn on to see messages and things like the raycasts
    public bool  isBomber;

    private bool  obstacle; //tells if there is an obstacle to be avoided
    private Vector3 dir;
    private Vector3 avoidDir;
    private Vector3 newDir;
    private int tempTime;
    private int tempTimeBombs;
    private int rayCount=0;
    private Transform saveTarget;

    void  Start (){
        AssignTarget();
    }


    void  FixedUpdate (){
        if(!isServer)
            return;
      
        if(target==null) //Assign next target when there is no target anymore
            AssignTarget();
        else{
            dir = (target.position - transform.position).normalized;
      
            if((Vector3.Distance(target.position, transform.position) < turnAwayDistance) && (saveTarget != target) && (saveTarget != null)){ //When the fighter is too close to the temporary target it should fly to the new target again
                Destroy(target.gameObject);
                target = saveTarget;
            }

            if(Vector3.Distance(target.position, transform.position) < turnAwayDistance)    //When the fighter is too close to the target, it should fly away to start a new attack run
                setTempTarget();

            if((rayCount > maxRayCasts) && (saveTarget == target)){    //when the fighter used too much rayCasts to find a way out, it means he is stuck and shall start a new attackrun
                rayCount = 0;
                setTempTarget();
            }
      
            if(Vector3.Distance(target.position, transform.position) < fireDistance){ //if target is in range
                if(Mathf.Abs(Vector3.Angle(target.position - transform.position, transform.forward)) < 45)  //if the angle between the fighter and the target is below 45 degrees, the fighter almost looks at it and it can shoot
                    fireLasers();
            }
            if(isBomber && (Vector3.Distance(target.position, transform.position) < fireDistance*1.5f)){
                fireBombs();
            }
        }

        if(obstacle)
            ObstacleCheck();

        moveShip();
}


    void  ObstacleCheck (){
        RaycastHit hit;
        if(Physics.Raycast(front.position, front.forward, out hit, sightDistance)){
            AvoidObstacle(front);
            dir = avoidDir;
        }
        else if(Physics.Raycast(shipL.position, shipL.forward, out hit, sightDistance)){ //if we have an incoming obstacle Infront of the Left Wings
            AvoidObstacle(shipL);
            dir = avoidDir;
        }
        else if(Physics.Raycast(shipR.position, shipR.forward, out hit, sightDistance)){ //if we have an incoming obstacle Infront of the Right Wings
            AvoidObstacle(shipR);
            dir = avoidDir;
        }
    }


    void  AvoidObstacle ( Transform rayTarget  ){
        int i=0;
        RaycastHit hit;
        while(i<30){    //we cycle through different vectors until we find the way out
            ////////////////////////////////////////////////////////////////////////
            //// DEBUG RAYS
            ///////////////////////////////////////////////////////////////////////
            if(debug){ ///In Debug Mode, the rays shall be shown
            if(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(0,-i*rayAngle,1)), out hit, sightDistance*(i+1))){
                    Debug.DrawLine(rayTarget.position, hit.point, Color.red,1);
            }
            if(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(0,i*rayAngle,1)), out hit, sightDistance*(i+1))){
                    Debug.DrawLine(rayTarget.position, hit.point, Color.red,1);
            }

            if(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(i*rayAngle,0,1)), out hit, sightDistance*(i+1))){
                    Debug.DrawLine(rayTarget.position, hit.point, Color.red,1);
            }
            if(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(-i*rayAngle,0,1)), out hit, sightDistance*(i+1))){
                    Debug.DrawLine(rayTarget.position, hit.point, Color.red,1);
                }
            }
            ////////////////////////////////////////////////////////////////////////
            //// CHECK IN UP DIRECTION
            ///////////////////////////////////////////////////////////////////////
            if(!(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(0,i*rayAngle,1)), out hit, sightDistance*(i+1)))){
                if(debug)
                    print("Ship Exit Up.");
                avoidDir = transform.TransformDirection(new Vector3(0,i,1));
                break;
            }
            ////////////////////////////////////////////////////////////////////////
            //// CHECK IN DOWN DIRECTION
            ///////////////////////////////////////////////////////////////////////
            if(!(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(0,-i*rayAngle,1)), out hit, sightDistance*(i+1)))){
                if(debug)
                    print("Ship Exit down.");
                avoidDir = transform.TransformDirection(new Vector3(0,-i,1));
                break;
            }
            ////////////////////////////////////////////////////////////////////////
            //// CHECK IN RIGHT DIRECTION
            ///////////////////////////////////////////////////////////////////////
            if(!(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(i*rayAngle,0,1)), out hit, sightDistance*(i+1)))){
                if(debug)
                    print("Ship Exit Right.");
                avoidDir = transform.TransformDirection(new Vector3(i,0,1));
                break;
            }
            ////////////////////////////////////////////////////////////////////////
            //// CHECK IN LEFT DIRECTION
            ///////////////////////////////////////////////////////////////////////
            if(!(Physics.Raycast(rayTarget.position, rayTarget.TransformDirection(new Vector3(-i*rayAngle,0,1)), out hit, sightDistance*(i+1)))){
                if(debug)
                    print("Ship Exit Left.");
                avoidDir = transform.TransformDirection(new Vector3(-i,0,1));
                break;
            }

        i++; //if no escape is in sight, raise the search-radius
        rayCount++;
        }
    }

    void AssignTarget (){
        string enemyTeam= "";
        if(ownTeam == 1)
            enemyTeam = "Team2";
        else if(ownTeam == 2)
            enemyTeam = "Team1";
          
        GameObject[] targets;
        targets = GameObject.FindGameObjectsWithTag(enemyTeam + "Target");
        int targetNr;  
        if (targets != null)
            targetNr = Random.Range (0, targets.Length);
        else
            targetNr = 0;
          
        if(targets[targetNr] != null)
            target = targets[targetNr].transform;
        saveTarget = target;
    }

    void  fireLasers (){
        if(tempTime == 30){ //Controls the reload time
            tempTime = 0;

            for(int i=0; i < cannonEnds.Length; i++){

                // Create the Bullet from the Bullet Prefab
                GameObject bullet = (GameObject) Instantiate(bulletPrefab, cannonEnds[i].position + new Vector3(0,0,bulletPrefab.transform.lossyScale.z), cannonEnds[i].rotation);

                // Add velocity to the bullet
                bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 700;
                //ToDo: bullet.GetComponent<LaserShotScript>().SetMotherShip(this.gameObject);

                NetworkServer.Spawn(bullet);

                // Destroy the bullet after 6 seconds
                Destroy(bullet, 2.0f);
            }
        }
        tempTime++;
    }

    void  fireBombs (){
        if(tempTimeBombs == 150){ //Controls the reload time
            tempTimeBombs = 0;
            GameObject ionShot = (GameObject) Instantiate(ionShotPrefab, bombSlot.position, bombSlot.rotation);

            ionShot.SendMessage("SetTarget", target);

            NetworkServer.Spawn(ionShot);

            Destroy(ionShot, 3.5f);
        }
        tempTimeBombs++;
    }

      
    void  OnTriggerStay (Collider col){ //If something enters the trigger, the fighter will make sure to avoid it
        if(col.gameObject != target){
            if(debug)
                Debug.Log("There is an Obstacle infront!");
            obstacle = true;
        }
    }

    void  OnTriggerEnter (Collider col){
        if(col.gameObject != target)
            obstacle = true;
    }

    void  OnTriggerExit (){
        obstacle = false;
    }

    void  setTempTarget (){    //When the fighter is close to the target, it should find another temporary target so it can start another attack run. This target is placed in the same direction as the real target, and a little random up, down, left or right.
        saveTarget = target;
        GameObject newTarget = (GameObject) GameObject.CreatePrimitive(PrimitiveType.Cube);
        newTarget.transform.position = target.position + target.forward * Random.Range(600.0f, 1000.0f) + target.up * Random.Range(300f, 700.0f) + new Vector3(0,Random.Range(-200.0f, 200.0f),Random.Range(-200.0f, -200.0f));
        target = newTarget.transform;
    }

    void  moveShip (){
        // YELLOW: the direction the target is
        // GREEN: the direction the ship is currently facing
        // WHITE: the avoid direction when there is an obstacle
        if(debug){ //when debug is active, it will draw the Directions
            Debug.DrawRay(transform.position, dir*180, Color.yellow);
            Debug.DrawRay(transform.position, transform.forward*100, Color.green);
            Debug.DrawRay(transform.position, avoidDir*60, Color.white);
            Debug.DrawRay(transform.position, newDir*120, Color.blue);
        }

        if(avoidDir != dir){
            if(!Physics.Raycast(transform.position, dir, sightDistance/2)){
                avoidDir = dir;
            }
        }

        newDir = avoidDir;


        Rigidbody rb = GetComponent<Rigidbody>();

        //get the angle between transform.forward and target delta
        float angleDiff = Vector3.Angle(transform.forward, newDir);

        // get its cross product, which is the axis of rotation to
        // get from one vector to the other
        Vector3 cross = Vector3.Cross(transform.forward, newDir);
          
        //apply torque along that axis according to the magnitude of the angle.
        rb.AddTorque(cross * angleDiff * rotateSpeed, ForceMode.Force);
          
        //apply velocity
        rb.velocity = transform.forward * speed * (180-angleDiff)/180 * Time.deltaTime * 1000;
    }

}

Well it depends on how complex you want to get.

If you want to get pre-planned routed paths, you can always use an A* algorithm (or other graph reduction algorithm) and calculate against a 3d graph of some sort. This would appear like thoughtful calculated pathing though 3-space. Though would probably require writing your own engine since most of the A* packages I’ve seen presume 2d graphs (even in 3d games… like walking around).

If you want simpler movement though, you could get away with simpler options. For example you could have a ‘heading’ vector, this is the direction you want to head (and update the position by). Then have a trigger collider out in front of the ship… or a raycast… or something physics test. And when it hits a collider find the center of it and then rotate the heading away from that center.

I’d recommend you continue pursuing this. I wrote something like this awhile back and it worked pretty well. I’d raycast straight ahead with a distance relative to forward velocity, and if the ray hit something, I’d raycast in different directions offset from the direction of travel to find a clear escape route.

I also set up a “shortcut” where ship #1 raycasts and gets a hit on ship #2, and ship #1 calls a method on ship #2 to notify it of the hit. That way ship #2 can also just go straight to avoidance code. There are edge cases where it doesn’t look right, but later I added an improvement where the notification only happens if the ray hit distance is relatively close and a nice side effect is that it looks like intelligent evasive manuvers in dogfight relationships.

Oh and speaking of dogfights, I also had to add “slow down” capability in case the ray hit the ship’s current target. You don’t want to pursue a target, then avoid it when you’re finally lined up behind it and trying to shoot it down. In that case I tested that the two ship’s forward vectors were reasonably aligned and if so, the pursuer just slowed down instead of turning to avoid.

You can get a lot of cool-looking behaviors without a lot of effort by playing around with this approach.

Almost forgot – here’s an interesting forum post I found awhile back about how they approached the problem with a “less is more” attitude on a series of very successful titles.

https://www.gamedev.net/topic/604440-space-combat-game-ai-problem/#entry4825820