एकता में हिरण का एआई कैसे बनाएं
गेम डेवलपमेंट में, आर्टिफिशियल इंटेलिजेंस जोड़ने का मतलब है कोड लिखना जो बिना किसी बाहरी इनपुट के गेम इकाई को नियंत्रित करेगा।
गेम्स में एनिमल एआई एआई की एक शाखा है जिसका उद्देश्य यथार्थवादी अनुभव बनाने के लिए जानवरों के व्यवहार को गेम के डिजिटल वातावरण में अनुवाद करना है।
इस ट्यूटोरियल में, मैं दिखाऊंगा कि Unity में एक साधारण जानवर (हिरण) AI कैसे बनाया जाए, जिसमें दो अवस्थाएं होंगी, निष्क्रिय और पलायन।
चरण 1: दृश्य और हिरण मॉडल तैयार करें
हमें एक लेवल और एक हिरण मॉडल की आवश्यकता होगी।
स्तर के लिए, मैं कुछ घास और पेड़ों के साथ एक साधारण इलाके का उपयोग करूंगा:
हिरण मॉडल के लिए मैंने बस कुछ क्यूब्स को संयोजित किया है (लेकिन आप इस हिरण मॉडल) का उपयोग कर सकते हैं:
अब कोडिंग भाग पर चलते हैं।
चरण 2: प्लेयर कंट्रोलर सेट करें
हम एक प्लेयर कंट्रोलर स्थापित करके शुरुआत करते हैं ताकि हम चारों ओर घूम सकें और एआई का परीक्षण कर सकें:
- एक नई स्क्रिप्ट बनाएं, इसे SC_CharacterController नाम दें और नीचे दिए गए कोड को इसके अंदर पेस्ट करें:
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);
}
}
}
- एक नया गेमऑब्जेक्ट बनाएं और इसे "Player" नाम दें और इसका टैग बदलें "Player"
- एक नया कैप्सूल बनाएं (गेमऑब्जेक्ट -> 3डी ऑब्जेक्ट -> कैप्सूल), फिर इसे "Player" ऑब्जेक्ट का चाइल्ड ऑब्जेक्ट बनाएं, इसकी स्थिति को (0, 1, 0) में बदलें, और इसके कैप्सूलकोलाइडर घटक को हटा दें।
- मुख्य कैमरे को "Player" ऑब्जेक्ट के अंदर ले जाएं और उसकी स्थिति को (0, 1.64, 0) में बदलें
- SC_CharacterController स्क्रिप्ट को "Player" ऑब्जेक्ट में संलग्न करें (आप देखेंगे कि यह कैरेक्टर कंट्रोलर नामक एक अन्य घटक भी जोड़ेगा। इसके केंद्र मान को (0, 1, 0) पर सेट करें)
- मुख्य कैमरे को SC_CharacterController पर "Player Camera" वेरिएबल पर असाइन करें, फिर दृश्य को सहेजें
प्लेयर कंट्रोलर अब तैयार है.
चरण 3: प्रोग्राम डियर एआई
अब आइए उस हिस्से पर चलते हैं जहां हम डियर एआई प्रोग्राम करते हैं:
- एक नई स्क्रिप्ट बनाएं और इसे SC_DeerAI नाम दें (यह स्क्रिप्ट AI मूवमेंट को नियंत्रित करेगी):
SC_DeerAI खोलें और नीचे दिए गए चरणों को जारी रखें:
स्क्रिप्ट की शुरुआत में, हम यह सुनिश्चित करते हैं कि सभी आवश्यक कक्षाएं शामिल हैं (विशेष रूप से UnityEngine.AI):
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;
}
(प्रत्येक राज्य अगले राज्य के लिए मानों और NavMesh एजेंट लक्ष्य को प्रारंभ करता है। उदाहरण के लिए, निष्क्रिय स्थिति में 2 संभावित परिणाम होते हैं, यह या तो दुश्मन मौजूद होने पर रनिंग स्थिति को प्रारंभ करता है या यदि कोई दुश्मन जागरूकता क्षेत्र को पार नहीं करता है तो खाने की स्थिति को प्रारंभ करता है।
नए गंतव्य पर जाने के लिए खाने की स्थिति के बीच चलने की स्थिति का उपयोग किया जाता है।
रनिंग स्टेट दुश्मन की स्थिति से सीधे भागने की दिशा की गणना करता है।
यदि कोने में फंस जाता है, तो एआई पहले से सहेजे गए निष्क्रिय पदों में से एक पर वापस आ जाता है। एआई के दुश्मन से काफी दूर होने के बाद दुश्मन हार जाता है)।
और अंत में, हम एक OnTriggerEnter इवेंट जोड़ते हैं जो स्फीयर कोलाइडर (उर्फ अवेयरनेस एरिया) की निगरानी करेगा और दुश्मन के बहुत करीब आने पर रनिंग स्टेट को इनिशियलाइज़ करेगा:
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.cs स्क्रिप्ट है:
//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);
}
}
- Deer model को दृश्य में रखें और इसमें NavMesh Agent, SC_DeerAI स्क्रिप्ट और एनिमेटर घटक संलग्न करें:
SC_DeerAI में केवल एक वेरिएबल है जिसे असाइन करने की आवश्यकता है जो कि "Animator" है।
एनिमेटर घटक को 4 एनिमेशन के साथ एक नियंत्रक की आवश्यकता होती है: आइडल एनीमेशन, वॉकिंग एनीमेशन, ईटिंग एनीमेशन, और रनिंग एनीमेशन, और 3 बूल पैरामीटर: इज़ईटिंग, इज़रनिंग, और इज़वॉकिंग:
आप यहां पर क्लिक करके सीख सकते हैं कि एक साधारण एनिमेटर कंट्रोलर कैसे सेटअप करें
सब कुछ सौंपे जाने के बाद एक आखिरी काम करना बाकी है, जो कि नवमेश को बेक करना है।
- सभी दृश्य ऑब्जेक्ट का चयन करें जो स्थिर होंगे (उदा. भू-भाग, पेड़, आदि) और उन्हें "Navigation Static" के रूप में चिह्नित करें:
- नेविगेशन विंडो (विंडो -> एआई -> नेविगेशन) पर जाएं और "Bake" टैब पर क्लिक करें और फिर "Bake" बटन पर क्लिक करें। NavMesh बेक होने के बाद इसे कुछ इस तरह दिखना चाहिए:
NavMesh के बेक हो जाने के बाद, हम AI का परीक्षण कर सकते हैं:
सब कुछ उम्मीद के मुताबिक काम करता है। जब दुश्मन करीब होता है तो हिरण भाग जाता है और जब दुश्मन काफी दूर हो जाता है तो वह अपनी सामान्य गतिविधियों को फिर से शुरू कर देता है।