Unity'de Bir Geyiğin Yapay Zekası Nasıl Yapılır?

Oyun geliştirmede Yapay Zeka eklemek, oyun varlığını herhangi bir harici giriş olmadan kontrol edecek kod yazmak anlamına gelir.

Oyunlardaki hayvan yapay zekası, gerçekçi bir deneyim yaratmak için hayvan davranışlarını oyunun dijital ortamına dönüştürmeyi amaçlayan bir yapay zeka dalıdır.

Bu derste, Unity'de boşta ve kaçan olmak üzere iki duruma sahip basit bir hayvan (Geyik) yapay zekasının nasıl yapılacağını göstereceğim.

Adım 1: Sahneyi ve Geyik Modelini Hazırlayın

Bir seviyeye ve bir geyik modeline ihtiyacımız olacak.

Seviye için biraz çimen ve ağaç içeren basit bir Arazi kullanacağım:

Geyik modeli için sadece bazı Küpleri birleştirdim (ancak bu geyik modelini kullanabilirsiniz):

Şimdi kodlama kısmına geçelim.

2. Adım: Oynatıcı Denetleyicisini Ayarlayın

Etrafta dolaşıp yapay zekayı test edebilmemiz için bir Oyuncu Kontrol Cihazı kurarak başlıyoruz:

  • yeni bir komut dosyası oluşturun, onu SC_CharacterController olarak adlandırın ve içine aşağıdaki kodu yapıştırın:

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);
        }
    }
}

  • Yeni bir GameObject oluşturun ve onu "Player" olarak adlandırın ve etiketini şu şekilde değiştirin: "Player"
  • Yeni bir Kapsül oluşturun (GameObject -> 3D Nesne -> Kapsül), ardından onu "Player" Nesnesinin alt nesnesi yapın, konumunu (0, 1, 0) olarak değiştirin ve CapsuleCollider bileşenini kaldırın.
  • Ana Kamerayı "Player" Nesnesinin içine taşıyın ve konumunu (0, 1,64, 0) olarak değiştirin
  • SC_CharacterController betiğini bir "Player" Nesnesine ekleyin (Karakter Denetleyicisi adında başka bir bileşen de ekleyeceğini fark edeceksiniz. Merkezi değerini (0, 1, 0) olarak ayarlayın)
  • Ana Kamerayı SC_CharacterController'da "Player Camera" değişkenine atayın ve ardından Sahneyi Kaydedin

Oynatıcı Denetleyicisi artık hazır.

Adım 3: Geyik Yapay Zekasını Programlayın

Şimdi Deer AI programladığımız kısma geçelim:

  • yeni bir komut dosyası oluşturun ve onu SC_DeerAI olarak adlandırın (bu komut dosyası AI hareketini kontrol edecektir):

SC_DeerAI'yi açın ve aşağıdaki adımlara devam edin:

Betiğin başlangıcında gerekli tüm sınıfların (özellikle UnityEngine.AI) dahil edildiğinden emin oluruz:

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

public class SC_DeerAI : MonoBehaviour
{

Şimdi tüm değişkenleri ekleyelim:

    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>(); 

Daha sonra void Start() içindeki her şeyi başlatıyoruz:

    // 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);
    }

(Gördüğünüz gibi Tetikleyici olarak işaretlenmiş bir Küre Çarpıştırıcısı ekliyoruz. Bu çarpıştırıcı, düşman girdiğinde farkındalık alanı görevi görecek).

Gerçek AI mantığı, bazı yardımcı işlevlerle birlikte void Update() içinde yapılır:

    // 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;
    }

(Her Durum, değerleri ve bir sonraki durum için NavMesh Agent hedefini başlatır. Örneğin, Boşta durumunun 2 olası sonucu vardır; düşman mevcutsa Koşma durumunu veya farkındalık alanını hiçbir Düşman geçmediyse Yeme durumunu başlatır.

Yeni hedefe gitmek için Yeme durumları arasında yürüme durumu kullanılır.

Koşma durumu, doğrudan oradan koşmak için düşmanın konumuna göre yönü hesaplar.

Yapay zeka, köşede sıkışıp kalırsa önceden kaydedilmiş Boş konumlardan birine geri çekilir. Yapay zeka düşmandan yeterince uzaklaştıktan sonra düşman kaybolur).

Ve son olarak, Küre Çarpıştırıcısını (aka Farkındalık Alanı) izleyecek ve düşman çok yaklaştığında Koşu durumunu başlatacak bir OnTriggerEnter olayı ekliyoruz:

    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);
    }

Oyuncu tetiğe girer girmez düşman değişkeni atanır ve Boşta durumu başlatılır, ardından Çalıştırma durumu başlatılır.

Aşağıda son SC_DeerAI.cs betiği verilmiştir:

//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'nin atanması gereken yalnızca bir değişkeni vardır: "Animator".

Animatör bileşeni, 4 animasyona sahip bir Denetleyici gerektirir: Boşta Animasyon, Yürüyüş Animasyonu, Yeme Animasyonu ve Koşu Animasyonu ve 3 bool parametresi: isEating, isRunning ve isWalking:

buraya tıklayarak basit bir Animatör Denetleyicisinin nasıl kurulacağını öğrenebilirsiniz

Her şey atandıktan sonra yapılacak son bir şey kalıyor, o da NavMesh'i pişirmek.

  • Statik olacak tüm Sahne Nesnelerini seçin (Örn. Arazi, Ağaçlar vb.) ve bunları "Navigation Static": olarak işaretleyin.

  • Gezinme Penceresine gidin (Pencere -> AI -> Gezinme) ve "Bake" sekmesine, ardından "Bake" düğmesine tıklayın. NavMesh pişirildikten sonra şöyle görünmelidir:

NavMesh pişirildikten sonra AI'yı test edebiliriz:

Sharp Coder Video oynatıcı

Her şey beklendiği gibi çalışıyor. Geyik, düşman yaklaştığında kaçar ve düşman yeterince uzaklaştığında olağan faaliyetlerine devam eder.

Kaynak
📁DeerAI.unitypackage3.36 MB
Önerilen Makaleler
Unity'de Düşmanın Yapay Zekasının Uygulanması
Unity'de Yapay Zeka Desteğiyle FPS Nasıl Yapılır?
Unity'de Oyuncuyu Takip Eden bir NPC Oluşturun
Unity'de NavMeshAgent ile çalışma
Unity 2D Platform Oyununa Düşman Ekleyin
Unity Asset Store Paketinin İncelenmesi - Zombi Yapay Zeka Sistemi
Unity'nin Animasyon Sistemine Giriş