Hoe je een AI van een hert in eenheid maakt

Bij het ontwikkelen van games betekent het toevoegen van Kunstmatige intelligentie het schrijven van code die de game-entiteit bestuurt zonder enige externe input.

Animal AI in games is een tak van AI die tot doel heeft het gedrag van dieren te vertalen naar de digitale omgeving van de game om zo een realistische ervaring te creëren.

In deze tutorial laat ik zien hoe je een eenvoudige AI voor dieren (herten) kunt maken in Unity die twee toestanden heeft: inactief en vluchten.

Stap 1: Bereid de scène en het hertenmodel voor

We hebben een niveau en een hertenmodel nodig.

Voor het level gebruik ik een eenvoudig terrein met wat gras en bomen:

Voor het hertenmodel heb ik eenvoudigweg een aantal kubussen gecombineerd (maar je kunt ook dit hertenmodel) gebruiken:

Laten we nu naar het codeergedeelte gaan.

Stap 2: Spelercontroller instellen

We beginnen met het opzetten van een Player Controller, zodat we rond kunnen lopen en de AI kunnen testen:

  • Maak een nieuw script, noem het SC_CharacterController en plak de onderstaande code erin:

SC_CharacterController.cs

using UnityEngine;

[RequireComponent(typeof(CharacterController))]

public class SC_CharacterController : MonoBehaviour
{
    public float speed = 7.5f;
    public float jumpSpeed = 8.0f;
    public float gravity = 20.0f;
    public Camera playerCamera;
    public float lookSpeed = 2.0f;
    public float lookXLimit = 45.0f;

    CharacterController characterController;
    Vector3 moveDirection = Vector3.zero;
    Vector2 rotation = Vector2.zero;

    [HideInInspector]
    public bool canMove = true;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        rotation.y = transform.eulerAngles.y;
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // We are grounded, so recalculate move direction based on axes
            Vector3 forward = transform.TransformDirection(Vector3.forward);
            Vector3 right = transform.TransformDirection(Vector3.right);
            float curSpeedX = speed * Input.GetAxis("Vertical");
            float curSpeedY = speed * Input.GetAxis("Horizontal");
            moveDirection = (forward * curSpeedX) + (right * curSpeedY);

            if (Input.GetButton("Jump"))
            {
                moveDirection.y = jumpSpeed;
            }
        }

        // Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below
        // when the moveDirection is multiplied by deltaTime). This is because gravity should be applied
        // as an acceleration (ms^-2)
        moveDirection.y -= gravity * Time.deltaTime;

        // Move the controller
        characterController.Move(moveDirection * Time.deltaTime);

        // Player and Camera rotation
        if (canMove)
        {
            rotation.y += Input.GetAxis("Mouse X") * lookSpeed;
            rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed;
            rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit);
            playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0);
            transform.eulerAngles = new Vector2(0, rotation.y);
        }
    }
}

  • Maak een nieuw GameObject en noem het "Player" en wijzig de tag in "Player"
  • Maak een nieuwe capsule (GameObject -> 3D-object -> Capsule), maak er vervolgens een onderliggend object van het "Player"-object van, verander de positie in (0, 1, 0) en verwijder de CapsuleCollider-component.
  • Verplaats de hoofdcamera binnen het object "Player" en wijzig de positie ervan in (0, 1,64, 0)
  • Voeg het SC_CharacterController-script toe aan een "Player"-object (u zult merken dat er ook een ander onderdeel wordt toegevoegd met de naam Character Controller. Stel de middelste waarde in op (0, 1, 0))
  • Wijs de hoofdcamera toe aan de variabele "Player Camera" bij SC_CharacterController en sla vervolgens de scène op

De spelercontroller is nu gereed.

Stap 3: Programmeer Deer AI

Laten we nu naar het gedeelte gaan waar we een Deer AI programmeren:

  • Maak een nieuw script en noem het SC_DeerAI (dit script bestuurt de AI-beweging):

Open SC_DeerAI en ga verder met de onderstaande stappen:

Aan het begin van het script zorgen we ervoor dat alle benodigde klassen zijn opgenomen (met name UnityEngine.AI):

using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;

public class SC_DeerAI : MonoBehaviour
{

Laten we nu alle variabelen toevoegen:

    public enum AIState { Idle, Walking, Eating, Running }
    public AIState currentState = AIState.Idle;
    public int awarenessArea = 15; //How far the deer should detect the enemy
    public float walkingSpeed = 3.5f;
    public float runningSpeed = 7f;
    public Animator animator;

    //Trigger collider that represents the awareness area
    SphereCollider c; 
    //NavMesh Agent
    NavMeshAgent agent;

    bool switchAction = false;
    float actionTimer = 0; //Timer duration till the next action
    Transform enemy;
    float range = 20; //How far the Deer have to run to resume the usual activities
    float multiplier = 1;
    bool reverseFlee = false; //In case the AI is stuck, send it to one of the original Idle points

    //Detect NavMesh edges to detect whether the AI is stuck
    Vector3 closestEdge;
    float distanceToEdge;
    float distance; //Squared distance to the enemy
    //How long the AI has been near the edge of NavMesh, if too long, send it to one of the random previousIdlePoints
    float timeStuck = 0;
    //Store previous idle points for reference
    List<Vector3> previousIdlePoints = new List<Vector3>(); 

Vervolgens initialiseren we alles in de lege Start():

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.stoppingDistance = 0;
        agent.autoBraking = true;

        c = gameObject.AddComponent<SphereCollider>();
        c.isTrigger = true;
        c.radius = awarenessArea;

        //Initialize the AI state
        currentState = AIState.Idle;
        actionTimer = Random.Range(0.1f, 2.0f);
        SwitchAnimationState(currentState);
    }

(Zoals je kunt zien, voegen we een Sphere Collider toe die is gemarkeerd als Trigger. Deze botser zal fungeren als een bewustzijnsgebied wanneer de vijand het betreedt).

De daadwerkelijke AI-logica wordt gedaan in de void Update() met enkele hulpfuncties:

    // Update is called once per frame
    void Update()
    {
        //Wait for the next course of action
        if (actionTimer > 0)
        {
            actionTimer -= Time.deltaTime;
        }
        else
        {
            switchAction = true;
        }

        if (currentState == AIState.Idle)
        {
            if(switchAction)
            {
                if (enemy)
                {
                    //Run away
                    agent.SetDestination(RandomNavSphere(transform.position, Random.Range(1, 2.4f)));
                    currentState = AIState.Running;
                    SwitchAnimationState(currentState);
                }
                else
                {
                    //No enemies nearby, start eating
                    actionTimer = Random.Range(14, 22);

                    currentState = AIState.Eating;
                    SwitchAnimationState(currentState);

                    //Keep last 5 Idle positions for future reference
                    previousIdlePoints.Add(transform.position);
                    if (previousIdlePoints.Count > 5)
                    {
                        previousIdlePoints.RemoveAt(0);
                    }
                }
            }
        }
        else if (currentState == AIState.Walking)
        {
            //Set NavMesh Agent Speed
            agent.speed = walkingSpeed;

            // Check if we've reached the destination
            if (DoneReachingDestination())
            {
                currentState = AIState.Idle;
            }
        }
        else if (currentState == AIState.Eating)
        {
            if (switchAction)
            {
                //Wait for current animation to finish playing
                if(!animator || animator.GetCurrentAnimatorStateInfo(0).normalizedTime - Mathf.Floor(animator.GetCurrentAnimatorStateInfo(0).normalizedTime) > 0.99f)
                {
                    //Walk to another random destination
                    agent.destination = RandomNavSphere(transform.position, Random.Range(3, 7));
                    currentState = AIState.Walking;
                    SwitchAnimationState(currentState);
                }
            }
        }
        else if (currentState == AIState.Running)
        {
            //Set NavMesh Agent Speed
            agent.speed = runningSpeed;

            //Run away
            if (enemy)
            {
                if (reverseFlee)
                {
                    if (DoneReachingDestination() && timeStuck < 0)
                    {
                        reverseFlee = false;
                    }
                    else
                    {
                        timeStuck -= Time.deltaTime;
                    }
                }
                else
                {
                    Vector3 runTo = transform.position + ((transform.position - enemy.position) * multiplier);
                    distance = (transform.position - enemy.position).sqrMagnitude;

                    //Find the closest NavMesh edge
                    NavMeshHit hit;
                    if (NavMesh.FindClosestEdge(transform.position, out hit, NavMesh.AllAreas))
                    {
                        closestEdge = hit.position;
                        distanceToEdge = hit.distance;
                        //Debug.DrawLine(transform.position, closestEdge, Color.red);
                    }

                    if (distanceToEdge < 1f)
                    {
                        if(timeStuck > 1.5f)
                        {
                            if(previousIdlePoints.Count > 0)
                            {
                                runTo = previousIdlePoints[Random.Range(0, previousIdlePoints.Count - 1)];
                                reverseFlee = true;
                            } 
                        }
                        else
                        {
                            timeStuck += Time.deltaTime;
                        }
                    }

                    if (distance < range * range)
                    {
                        agent.SetDestination(runTo);
                    }
                    else
                    {
                        enemy = null;
                    }
                }
                
                //Temporarily switch to Idle if the Agent stopped
                if(agent.velocity.sqrMagnitude < 0.1f * 0.1f)
                {
                    SwitchAnimationState(AIState.Idle);
                }
                else
                {
                    SwitchAnimationState(AIState.Running);
                }
            }
            else
            {
                //Check if we've reached the destination then stop running
                if (DoneReachingDestination())
                {
                    actionTimer = Random.Range(1.4f, 3.4f);
                    currentState = AIState.Eating;
                    SwitchAnimationState(AIState.Idle);
                }
            }
        }

        switchAction = false;
    }

    bool DoneReachingDestination()
    {
        if (!agent.pathPending)
        {
            if (agent.remainingDistance <= agent.stoppingDistance)
            {
                if (!agent.hasPath || agent.velocity.sqrMagnitude == 0f)
                {
                    //Done reaching the Destination
                    return true;
                }
            }
        }

        return false;
    }

    void SwitchAnimationState(AIState state)
    {
        //Animation control
        if (animator)
        {
            animator.SetBool("isEating", state == AIState.Eating);
            animator.SetBool("isRunning", state == AIState.Running);
            animator.SetBool("isWalking", state == AIState.Walking);
        }
    }

    Vector3 RandomNavSphere(Vector3 origin, float distance)
    {
        Vector3 randomDirection = Random.insideUnitSphere * distance;

        randomDirection += origin;

        NavMeshHit navHit;

        NavMesh.SamplePosition(randomDirection, out navHit, distance, NavMesh.AllAreas);

        return navHit.position;
    }

(Elke staat initialiseert de waarden en het doel van de NavMesh-agent voor de volgende staat. De inactieve staat heeft bijvoorbeeld twee mogelijke uitkomsten: hij initialiseert ofwel de status Running als de vijand aanwezig is, of de status Eating als er geen vijand het bewustzijnsgebied heeft overschreden.

Tussen de eetstaten wordt een loopstatus gebruikt om naar de nieuwe bestemming te gaan.

Running State berekent de richting ten opzichte van de vijandelijke positie, om er rechtstreeks vanaf te rennen.

Als de AI vastzit in de hoek, trekt hij zich terug naar een van de eerder opgeslagen inactieve posities. De vijand is verloren nadat de AI ver genoeg van de vijand verwijderd is).

En ten slotte voegen we een OnTriggerEnter-gebeurtenis toe die de Sphere Collider (ook bekend als Awareness Area) in de gaten houdt en de Running-status initialiseert zodra de vijand te dichtbij komt:

    void OnTriggerEnter(Collider other)
    {
        //Make sure the Player instance has a tag "Player"
        if (!other.CompareTag("Player"))
            return;

        enemy = other.transform;

        actionTimer = Random.Range(0.24f, 0.8f);
        currentState = AIState.Idle;
        SwitchAnimationState(currentState);
    }

Zodra de speler de trigger invoert, wordt de vijandelijke variabele toegewezen en wordt de inactieve status geïnitialiseerd, daarna wordt de actieve status geïnitialiseerd.

Hieronder vindt u het definitieve SC_DeerAI.cs-script:

//You are free to use this script in Free or Commercial projects
//sharpcoderblog.com @2019

using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;

public class SC_DeerAI : MonoBehaviour
{
    public enum AIState { Idle, Walking, Eating, Running }
    public AIState currentState = AIState.Idle;
    public int awarenessArea = 15; //How far the deer should detect the enemy
    public float walkingSpeed = 3.5f;
    public float runningSpeed = 7f;
    public Animator animator;

    //Trigger collider that represents the awareness area
    SphereCollider c; 
    //NavMesh Agent
    NavMeshAgent agent;

    bool switchAction = false;
    float actionTimer = 0; //Timer duration till the next action
    Transform enemy;
    float range = 20; //How far the Deer have to run to resume the usual activities
    float multiplier = 1;
    bool reverseFlee = false; //In case the AI is stuck, send it to one of the original Idle points

    //Detect NavMesh edges to detect whether the AI is stuck
    Vector3 closestEdge;
    float distanceToEdge;
    float distance; //Squared distance to the enemy
    //How long the AI has been near the edge of NavMesh, if too long, send it to one of the random previousIdlePoints
    float timeStuck = 0;
    //Store previous idle points for reference
    List<Vector3> previousIdlePoints = new List<Vector3>(); 

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.stoppingDistance = 0;
        agent.autoBraking = true;

        c = gameObject.AddComponent<SphereCollider>();
        c.isTrigger = true;
        c.radius = awarenessArea;

        //Initialize the AI state
        currentState = AIState.Idle;
        actionTimer = Random.Range(0.1f, 2.0f);
        SwitchAnimationState(currentState);
    }

    // Update is called once per frame
    void Update()
    {
        //Wait for the next course of action
        if (actionTimer > 0)
        {
            actionTimer -= Time.deltaTime;
        }
        else
        {
            switchAction = true;
        }

        if (currentState == AIState.Idle)
        {
            if(switchAction)
            {
                if (enemy)
                {
                    //Run away
                    agent.SetDestination(RandomNavSphere(transform.position, Random.Range(1, 2.4f)));
                    currentState = AIState.Running;
                    SwitchAnimationState(currentState);
                }
                else
                {
                    //No enemies nearby, start eating
                    actionTimer = Random.Range(14, 22);

                    currentState = AIState.Eating;
                    SwitchAnimationState(currentState);

                    //Keep last 5 Idle positions for future reference
                    previousIdlePoints.Add(transform.position);
                    if (previousIdlePoints.Count > 5)
                    {
                        previousIdlePoints.RemoveAt(0);
                    }
                }
            }
        }
        else if (currentState == AIState.Walking)
        {
            //Set NavMesh Agent Speed
            agent.speed = walkingSpeed;

            // Check if we've reached the destination
            if (DoneReachingDestination())
            {
                currentState = AIState.Idle;
            }
        }
        else if (currentState == AIState.Eating)
        {
            if (switchAction)
            {
                //Wait for current animation to finish playing
                if(!animator || animator.GetCurrentAnimatorStateInfo(0).normalizedTime - Mathf.Floor(animator.GetCurrentAnimatorStateInfo(0).normalizedTime) > 0.99f)
                {
                    //Walk to another random destination
                    agent.destination = RandomNavSphere(transform.position, Random.Range(3, 7));
                    currentState = AIState.Walking;
                    SwitchAnimationState(currentState);
                }
            }
        }
        else if (currentState == AIState.Running)
        {
            //Set NavMesh Agent Speed
            agent.speed = runningSpeed;

            //Run away
            if (enemy)
            {
                if (reverseFlee)
                {
                    if (DoneReachingDestination() && timeStuck < 0)
                    {
                        reverseFlee = false;
                    }
                    else
                    {
                        timeStuck -= Time.deltaTime;
                    }
                }
                else
                {
                    Vector3 runTo = transform.position + ((transform.position - enemy.position) * multiplier);
                    distance = (transform.position - enemy.position).sqrMagnitude;

                    //Find the closest NavMesh edge
                    NavMeshHit hit;
                    if (NavMesh.FindClosestEdge(transform.position, out hit, NavMesh.AllAreas))
                    {
                        closestEdge = hit.position;
                        distanceToEdge = hit.distance;
                        //Debug.DrawLine(transform.position, closestEdge, Color.red);
                    }

                    if (distanceToEdge < 1f)
                    {
                        if(timeStuck > 1.5f)
                        {
                            if(previousIdlePoints.Count > 0)
                            {
                                runTo = previousIdlePoints[Random.Range(0, previousIdlePoints.Count - 1)];
                                reverseFlee = true;
                            } 
                        }
                        else
                        {
                            timeStuck += Time.deltaTime;
                        }
                    }

                    if (distance < range * range)
                    {
                        agent.SetDestination(runTo);
                    }
                    else
                    {
                        enemy = null;
                    }
                }
                
                //Temporarily switch to Idle if the Agent stopped
                if(agent.velocity.sqrMagnitude < 0.1f * 0.1f)
                {
                    SwitchAnimationState(AIState.Idle);
                }
                else
                {
                    SwitchAnimationState(AIState.Running);
                }
            }
            else
            {
                //Check if we've reached the destination then stop running
                if (DoneReachingDestination())
                {
                    actionTimer = Random.Range(1.4f, 3.4f);
                    currentState = AIState.Eating;
                    SwitchAnimationState(AIState.Idle);
                }
            }
        }

        switchAction = false;
    }

    bool DoneReachingDestination()
    {
        if (!agent.pathPending)
        {
            if (agent.remainingDistance <= agent.stoppingDistance)
            {
                if (!agent.hasPath || agent.velocity.sqrMagnitude == 0f)
                {
                    //Done reaching the Destination
                    return true;
                }
            }
        }

        return false;
    }

    void SwitchAnimationState(AIState state)
    {
        //Animation control
        if (animator)
        {
            animator.SetBool("isEating", state == AIState.Eating);
            animator.SetBool("isRunning", state == AIState.Running);
            animator.SetBool("isWalking", state == AIState.Walking);
        }
    }

    Vector3 RandomNavSphere(Vector3 origin, float distance)
    {
        Vector3 randomDirection = Random.insideUnitSphere * distance;

        randomDirection += origin;

        NavMeshHit navHit;

        NavMesh.SamplePosition(randomDirection, out navHit, distance, NavMesh.AllAreas);

        return navHit.position;
    }

    void OnTriggerEnter(Collider other)
    {
        //Make sure the Player instance has a tag "Player"
        if (!other.CompareTag("Player"))
            return;

        enemy = other.transform;

        actionTimer = Random.Range(0.24f, 0.8f);
        currentState = AIState.Idle;
        SwitchAnimationState(currentState);
    }
}

SC_DeerAI heeft slechts één variabele die moet worden toegewezen, namelijk "Animator".

De animatorcomponent vereist een controller met 4 animaties: inactieve animatie, loopanimatie, eetanimatie en hardloopanimatie, en 3 bool-parameters: isEating, isRunning en isWalking:

U kunt leren hoe u een eenvoudige Animator Controller instelt door hier te klikken

Nadat alles is toegewezen, moet er nog één laatste ding worden gedaan, namelijk het bakken van een NavMesh.

  • Selecteer alle scèneobjecten die statisch zullen zijn (bijvoorbeeld terrein, bomen, enz.) en markeer ze als "Navigation Static":

  • Ga naar het navigatievenster (Venster -> AI -> Navigatie) en klik op het tabblad "Bake" en klik vervolgens op de knop "Bake". Nadat de NavMesh is gebakken, zou het er ongeveer zo uit moeten zien:

Nadat de NavMesh is gebakken, kunnen we de AI testen:

Sharp Coder Video speler

Alles werkt zoals verwacht. Het Hert rent weg als de vijand dichtbij is en hervat zijn gebruikelijke activiteiten zodra de vijand ver genoeg is.

Bron
📁DeerAI.unitypackage3.36 MB
Voorgestelde artikelen
Implementatie van AI van een vijand in eenheid
Creëer een NPC die de speler in eenheid volgt
Hoe je een FPS maakt met de AI-ondersteuning in Unity
Unity Voeg vijanden toe aan een 2D-platformgame
Werken met NavMeshAgent in Unity
Herziening van het Unity Asset Store-pakket - Zombie AI-systeem
Een jachtsimulator maken in Unity