Programación de Movimiento Aleatorio para Enemigos en Juegos de Rol
Resumen
La mejor forma de dar vida a un enemigo en Unity 2D es con una IA simple de movimiento aleatorio. Aquí verás cómo estructurar un EnemyController en C#, conectar Rigidbody2D y Animator, y controlar tiempos de paso con precisión. Todo está preparado para reutilizarlo en distintos tipos de enemigos sin cambiar el script, solo los gráficos.
¿Cómo crear e integrar el enemy controller?
Para empezar, se crea un script de C# llamado EnemyController y se arrastra al objeto enemigo en la jerarquía. Aunque se llame controller, no lo controlará el jugador: gobierna el movimiento autónomo del enemigo.
¿Qué requisitos debe tener el enemigo?
Tag "enemy" aplicada al objeto enemigo.
Sistema de animaciones con blend tree y parámetros "horizontal" y "vertical".
Collider configurado.
Rigidbody2D agregado.
¿Cómo resolver errores del editor?
Si Visual Studio marca todo en rojo sin motivo: guardar, cerrar y reabrir el script desde Unity.
¿Qué variables controlan la IA y el movimiento aleatorio?
La lógica se basa en dos temporizadores: tiempo entre pasos y tiempo de cada paso. Además, se registra si el enemigo está en movimiento y en qué dirección debe desplazarse. Se comunican los ejes al animator mediante constantes.
usingUnityEngine;publicclassEnemyController:MonoBehaviour{publicfloat enemySpeed =1f;// Velocidad del enemigo.privateRigidbody2D enemyRigidBody;// Referencia al Rigidbody2D.privatebool isMoving;// Estado de movimiento.publicfloat timeBetweenSteps =2f;// Tiempo en reposo entre pasos.privatefloat timeBetweenStepsCounter;// Contador interno de reposo.publicfloat timeToMakeStep =1.5f;// Duración de cada paso.privatefloat timeToMakeStepCounter;// Contador interno de paso.publicVector2 directionToMakeStep;// Dirección del movimiento.privateAnimator enemyAnimator;// Referencia al Animator.privateconststring Horizontal ="horizontal";// Parámetro del blend tree.privateconststring Vertical ="vertical";// Parámetro del blend tree.voidStart(){ enemyRigidBody =GetComponent<Rigidbody2D>(); enemyAnimator =GetComponent<Animator>();// Inicialización de contadores con los valores públicos. timeBetweenStepsCounter = timeBetweenSteps; timeToMakeStepCounter = timeToMakeStep;}
enemySpeed: controla la rapidez del desplazamiento.
isMoving: indica si se está moviendo o está quieto.
timeBetweenSteps/timeBetweenStepsCounter: reposo y su contador interno.
timeToMakeStep/timeToMakeStepCounter: duración del paso y su contador interno.
directionToMakeStep: vector de dirección aleatoria.
enemyRigidBody y enemyAnimator: componentes para movimiento y animación.
"horizontal" y "vertical": nombres exactos de los parámetros del animator.
¿Cómo se programa la lógica en update para moverse y parar?
La mecánica alterna entre reposo y movimiento. En movimiento, se consume el contador del paso y se aplica velocidad en la dirección elegida. En reposo, se espera hasta agotar el contador y se decide una nueva dirección aleatoria.
voidUpdate(){if(isMoving){// Mientras se mueve. timeToMakeStepCounter -= Time.deltaTime; enemyRigidBody.velocity = directionToMakeStep * enemySpeed;if(timeToMakeStepCounter <0f){// Fin del paso: parar y preparar reposo. isMoving =false; timeBetweenStepsCounter = timeBetweenSteps;// Ojo con asignar el correcto. enemyRigidBody.velocity = Vector2.zero;}}else{// Mientras está quieto. timeBetweenStepsCounter -= Time.deltaTime;if(timeBetweenStepsCounter <0f){// Toca iniciar un nuevo paso. isMoving =true; timeToMakeStepCounter = timeToMakeStep;// Dirección aleatoria en X e Y entre -1 y 1. directionToMakeStep =newVector2( Random.Range(-1f,1f), Random.Range(-1f,1f));}}// Notificar la dirección al Animator para el blend tree. enemyAnimator.SetFloat(Horizontal, directionToMakeStep.x); enemyAnimator.SetFloat(Vertical, directionToMakeStep.y);}}
¿Qué ocurre mientras se mueve?
Se decrementa el contador del paso con Time.deltaTime.
Se aplica velocity al rigidbody según la dirección y la velocidad.
Al agotar el tiempo del paso: se detiene, se pone Vector2.zero y se arma el reposo.
¿Qué ocurre mientras está quieto?
Se decrementa el contador de reposo con Time.deltaTime.
Al agotarse: se activa el movimiento, se reinicia el contador del paso y se elige una dirección aleatoria con Random.Range(-1, 1) en X e Y (puede salir diagonal).
¿Cómo sincronizar animaciones con el movimiento?
Se actualizan los parámetros "horizontal" y "vertical" mediante SetFloat.
El blend tree interpreta estos valores para girar la animación según la dirección.
Valores de ejemplo comprobados: enemySpeed = 1. Tiempo entre pasos = 2 s. Tiempo de paso = 1.5 s.
Consejo práctico: hay mucho código dentro de Update; avanza con cuidado para no mezclar contadores.
Próximo paso: crea un prefab con varios enemigos en pantalla y valida el comportamiento.
¿Tienes dudas o ideas para ampliar esta IA? Comparte tu pregunta y cuéntame cómo la adaptarías a tus enemigos.
using System.Collections;using System.Collections.Generic;using UnityEngine;publicclassEnemyController:MonoBehaviour{public float enemySpeed =1;//velocidad movimientoprivateRigidbody2D enemyRigidbody;//rigidbody enemigoprivate bool isMoving;//saber si se esta moviendo o nopublic float timeBetweenSteps;//tiempo entre movimientosprivate float timeBetweenStepsCounter;//contador cuanto tiempo a pasado desde el ultimo movimientopublic float timeToMakeStep;//el tiempo que pasa en hacer el paso de una celda a la siguienteprivate float timeToMakeStepCounter;//el condador de cuanto tiempo a pasado en hacer el pasopublicVector2 directionToMakeStep;//una direccion de movimientoprivateAnimator enemyAnimator;//para transmitir los parametros horizontal y vertical privateconst string horizontal ="Horizontal";//nombre de los parametros que estan en unityprivateconst string vertical ="Vertical";//nombre de los parametros que estan en unity// Start is called before the first frame updatevoidStart(){ enemyRigidbody =GetComponent<Rigidbody2D>();//inicilisamos las variabes enemyAnimator =GetComponent<Animator>();////inicilisamos las variabes timeBetweenStepsCounter = timeBetweenSteps;//se inicialice con la informacion que le ponemos en unity timeToMakeStepCounter = timeToMakeStep;//se inicialice con la informacion que le ponemos en unity}// Update is called once per framevoidUpdate(){if(isMoving){ timeToMakeStepCounter -=Time.deltaTime;//descuenta el tiempo del ultimo renderisado enemyRigidbody.velocity= directionToMakeStep;//movemos al enemigo a la direccionif(timeToMakeStepCounter <0)//si se acaba el tiempo de movimiento{ isMoving =false;//pone en falso el movimiento timeBetweenStepsCounter = timeBetweenSteps;//reinicia el contador enemyRigidbody.velocity=Vector2.zero;//para el movimiento}}else//si no se esta moviendo{ timeBetweenStepsCounter -=Time.deltaTime;//resta tiempo al contadorif(timeBetweenStepsCounter <0)//si se acaba el tiempo de espera para el siguiente{ isMoving =true;//ponemos en true para empesar a movernos timeToMakeStepCounter = timeToMakeStep;//re iniciamos el contador directionToMakeStep =newVector2(Random.Range(-1,2),Random.Range(-1,2))* enemySpeed;//nos movemos}} enemyAnimator.SetFloat(horizontal, directionToMakeStep.x);//lo movemos ya con los valores dados enemyAnimator.SetFloat(vertical, directionToMakeStep.y);//lo movemos ya con los valores dados}}
Muchas gracias por el aporte hermano, me ayudo a comprender mejor! c:
Hay algo que no se explica con el Random.Range y es que cuando se usan valores --int-- el segundo valor es exclusivo, esa es la razon por la que el personaje siempre baja en la demostracion, entonces deberia ser Random.Range(-1,2) o Random.Range(-1f,1f)
Tienes mucha razón, me di cuenta de eso también. Lo ideal es usarlo así Random.Range(-1,2), para que el personaje pueda subir y bajar
Excelente aporte!
Siempre es importante establecer y definirle una capa al enemigo.
Hice el codigo del enemigo similar al del jugador que cuando no se esta moviendo se queda parado viendo en la ultima dirrecion que este se movio
using System.Collections;using System.Collections.Generic;using UnityEngine;publicclassEnemyController:MonoBehaviour{public float enemySpeed =1;privateRigidbody2D enemyRigidbody;private bool isMoving =false;public float timeBetweenSteps;private float timeBetweenStepsCounter;public float timeToMakeStep;private float timeToMakeStepCounter;publicVector2 directionToMakeStep;privateVector2 lastMovement =Vector2.zero;privateAnimator enemyAnimator;privateconst string horizontal ="Horizontal";privateconst string vertical ="Vertical";privateconst string moving ="isMoving";privateconst string lastHorizontal ="LastHorizontal";privateconst string lastVertical ="LastVertical";// Start is called before the first frame updatevoidStart(){ enemyRigidbody =GetComponent<Rigidbody2D>(); enemyAnimator =GetComponent<Animator>(); timeBetweenStepsCounter = timeBetweenSteps *Random.Range(0.5f,1.5f); timeToMakeStepCounter = timeToMakeStep *Random.Range(0.5f,1.5f);}// Update is called once per framevoidUpdate(){if(isMoving){ timeToMakeStepCounter -=Time.deltaTime; enemyRigidbody.velocity= directionToMakeStep;if(timeToMakeStepCounter <0){ isMoving =false; timeBetweenStepsCounter = timeBetweenSteps; enemyRigidbody.velocity=Vector2.zero;}}else{ timeBetweenStepsCounter -=Time.deltaTime;if(timeBetweenStepsCounter <0){ isMoving =true; timeToMakeStepCounter = timeToMakeStep; directionToMakeStep =newVector2(Random.Range(-1,2),// de esta forma porque el Random.Range no llega al ultimoRandom.Range(-1,2)// de esta forma porque el Random.Range no llega al ultimo)* enemySpeed; lastMovement = directionToMakeStep;}} enemyAnimator.SetFloat(horizontal, directionToMakeStep.x); enemyAnimator.SetFloat(vertical, directionToMakeStep.y);if(lastMovement !=Vector2.zero){ enemyAnimator.SetBool(moving, isMoving);} enemyAnimator.SetFloat(lastHorizontal, lastMovement.x); enemyAnimator.SetFloat(lastVertical, lastMovement.y);}}
Dejo mi código mucho mas entendible. Creo que el profesor le erra al poner esos nombres a las variables. La traducción al español no es lo que está queriendo indicar.
Dejo mi codigo con nombres más amenos, optimización usando el FixedUpdate para el movimiento y el LateUpdate para las animaciones con un OnTriggerEnter2D para evitar que el personaje colisione con objetos y se quede caminando en el aire.
Ademas también sucede que si la velocidad del rigidbody es 0 pero el bool _isMoving = true, el tipo hace el moon walk a lo Michael Jackson. Con este código se soluciona en el IF que puse.
publicclassEnemyController:MonoBehaviour{[Header("Configuration")]public float enemySpeed =1.0f;public float timeBetweenSteps;public float timeToMove;privateRigidbody2D _rigidbody;private bool _isMoving;private float _timeWaiting;//Counter that indicates how much time enemy is in Idle until he moves again.private float _movingTime;//Counter that indicates how much time enemy moves in the same directionpublicVector2 directionToMakeStep;privateAnimator _animator;privateconst string _horizontal ="Horizontal";privateconst string _vertical ="Vertical";privatevoidAwake(){ _rigidbody=GetComponent<Rigidbody2D>(); _animator=GetComponent<Animator>();}// Start is called before the first frame updatevoidStart(){ _timeWaiting = timeBetweenSteps; _movingTime = timeToMove;}// Update is called once per framevoidUpdate(){}privatevoidFixedUpdate(){if(_isMoving){ _movingTime -=Time.deltaTime;//Counter starts decreasing _rigidbody.velocity= directionToMakeStep;//Enemy start moving in any directionif(_movingTime <=0)//Enemy stop moving and restarts counter to go in another direction{ _isMoving =false; _timeWaiting = timeBetweenSteps; _rigidbody.velocity=Vector2.zero;}}else{ _timeWaiting -=Time.deltaTime;if(_timeWaiting <0){//Stop waiting. Time to move _isMoving =true; _movingTime = timeToMove; directionToMakeStep =newVector2(Random.Range(-1,2),Random.Range(-1,2))* enemySpeed;}}}privatevoidLateUpdate(){//If Enemy is moving and velocity is not 0 then execute movement animationsif(_isMoving && _rigidbody.velocity!=Vector2.zero){ _animator.SetBool("IsMoving",true); _animator.SetFloat(_horizontal, directionToMakeStep.x); _animator.SetFloat(_vertical, directionToMakeStep.y);}else{//If enemy is not moving. Then Idle animations _animator.SetBool("IsMoving",false); _animator.SetFloat("LastHorizontal", directionToMakeStep.x); _animator.SetFloat("LastVertical", directionToMakeStep.y);}}privatevoidOnCollisionEnter2D(Collision2D collision){//If Enemy collision to an object. Then change _isMoving to false. That will make another move to other direction or stops.if(!collision.gameObject.CompareTag("Player")){ _isMoving =false;}}}