Cách tạo AI của một con nai trong Unity
Trong quá trình phát triển trò chơi, việc thêm Artificial Intelligence có nghĩa là viết mã sẽ kiểm soát thực thể trò chơi mà không cần bất kỳ đầu vào bên ngoài nào.
AI động vật trong trò chơi là một nhánh của AI nhằm mục đích chuyển hành vi của động vật sang môi trường kỹ thuật số của trò chơi để tạo ra trải nghiệm thực tế.
Trong hướng dẫn này, tôi sẽ hướng dẫn cách tạo AI động vật (Hươu) đơn giản trong Unity sẽ có hai trạng thái, nhàn rỗi và chạy trốn.
Bước 1: Chuẩn bị bối cảnh và mô hình chú nai
Chúng ta sẽ cần một cấp độ và một mô hình con nai.
Đối với cấp độ, tôi sẽ sử dụng Địa hình đơn giản với một số cỏ và cây cối:
Đối với mô hình Con nai, tôi chỉ cần kết hợp một số Khối (nhưng bạn có thể sử dụng mô hình con nai) này:
Bây giờ hãy chuyển sang phần mã hóa.
Bước 2: Thiết lập Trình điều khiển trình phát
Chúng tôi bắt đầu bằng cách thiết lập Bộ điều khiển người chơi để có thể đi lại và kiểm tra AI:
- Tạo một tập lệnh mới, đặt tên là SC_CharacterController và dán mã bên dưới vào bên trong 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);
}
}
}
- Tạo một GameObject mới và đặt tên là "Player" và đổi thẻ của nó thành "Player"
- Tạo một Capsule mới (GameObject -> Đối tượng 3D -> Capsule), sau đó biến nó thành đối tượng con của Đối tượng "Player", thay đổi vị trí của nó thành (0, 1, 0) và xóa thành phần CapsuleCollider của nó.
- Di chuyển Camera chính bên trong Đối tượng "Player" và thay đổi vị trí của nó thành (0, 1.64, 0)
- Đính kèm tập lệnh SC_CharacterController vào đối tượng "Player" (Bạn sẽ nhận thấy nó cũng sẽ thêm một thành phần khác gọi là Bộ điều khiển ký tự. Đặt giá trị trung tâm của nó thành (0, 1, 0))
- Gán Camera chính cho biến "Player Camera" tại SC_CharacterController sau đó Lưu cảnh
Trình điều khiển trình phát hiện đã sẵn sàng.
Bước 3: Lập trình AI cho hươu
Bây giờ hãy chuyển sang phần chúng ta lập trình AI cho Hươu:
- Tạo một tập lệnh mới và đặt tên là SC_DeerAI (tập lệnh này sẽ điều khiển chuyển động của AI):
Mở SC_DeerAI và tiếp tục các bước bên dưới:
Khi bắt đầu tập lệnh, chúng tôi đảm bảo rằng tất cả các lớp cần thiết đều được đưa vào (cụ thể là UnityEngine.AI):
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
public class SC_DeerAI : MonoBehaviour
{
Bây giờ hãy thêm tất cả các biến:
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>();
Sau đó chúng ta khởi tạo mọi thứ trong void 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);
}
(Như bạn có thể thấy, chúng tôi thêm Sphere Collider được đánh dấu là Trigger. Máy va chạm này sẽ hoạt động như một khu vực nhận thức khi kẻ thù xâm nhập vào nó).
Logic AI thực tế được thực hiện trong void Update() với một số hàm trợ giúp:
// 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;
}
(Mỗi Trạng thái khởi tạo các giá trị và mục tiêu Tác nhân NavMesh cho trạng thái tiếp theo. Ví dụ: trạng thái Không hoạt động có 2 kết quả có thể xảy ra, đó là khởi tạo trạng thái Đang chạy nếu kẻ thù có mặt hoặc trạng thái Ăn uống nếu không có Kẻ thù nào vượt qua khu vực nhận thức.
Trạng thái đi bộ được sử dụng giữa các trạng thái Ăn uống để di chuyển đến điểm đến mới.
Trạng thái chạy tính toán hướng tương ứng với vị trí của kẻ địch để chạy trực tiếp từ vị trí đó.
Nếu bị kẹt trong góc, AI sẽ rút về một trong các vị trí Không hoạt động đã lưu trước đó. Kẻ địch sẽ bị tiêu diệt sau khi AI ở đủ xa kẻ thù).
Và cuối cùng, chúng tôi thêm một sự kiện OnTriggerEnter sẽ giám sát Sphere Collider (còn gọi là Khu vực Nhận thức) và sẽ khởi tạo trạng thái Đang chạy khi kẻ thù đến quá gần:
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);
}
Ngay khi người chơi bấm cò, biến địch sẽ được gán và trạng thái Chờ được khởi tạo, sau đó, trạng thái Đang chạy được khởi tạo.
Dưới đây là tập lệnh SC_DeerAI.cs cuối cùng:
//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);
}
}
- Đặt Deer model trong Cảnh và đính kèm thành phần NavMesh Agent, SC_DeerAI và thành phần Animator vào nó:
SC_DeerAI chỉ có một biến cần được gán đó là "Animator".
Thành phần hoạt ảnh yêu cầu Bộ điều khiển có 4 hoạt ảnh: Hoạt ảnh nhàn rỗi, Hoạt ảnh đi bộ, Hoạt ảnh ăn uống và Hoạt ảnh đang chạy và 3 tham số bool: isEating, isRunning và isWalking:
Bạn có thể tìm hiểu cách thiết lập Bộ điều khiển hoạt hình đơn giản bằng cách nhấp vào đây
Sau khi mọi thứ đã được chỉ định, còn một việc cuối cùng phải làm, đó là tạo NavMesh.
- Chọn tất cả các Đối tượng Cảnh sẽ ở trạng thái tĩnh (Ví dụ: Địa hình, Cây cối, v.v.) và Đánh dấu chúng là "Navigation Static":
- Đi tới Cửa sổ điều hướng (Cửa sổ -> AI -> Điều hướng) và nhấp vào tab "Bake" sau đó nhấp vào nút "Bake". Sau khi NavMesh được nướng, nó sẽ trông giống như thế này:
Sau khi NavMesh được triển khai, chúng ta có thể kiểm tra AI:
Mọi thứ hoạt động như mong đợi. Hươu bỏ chạy khi kẻ thù đến gần và tiếp tục các hoạt động bình thường khi kẻ thù ở đủ xa.