IA y Mecánicas de juego


Deberás ponerte en la piel de un soldado superando sus pruebas de combate simulado en un entorno virtual controlado.
Escondete, asomate para ancitiparte, coge a tus enemigos por sorpresa y sobre todo, evita ser visto o escuchado mientras te diriges al punto de estracción.


Información del proyecto

Plataforma

Motor

Lenguajes

C# y Unity Javascript

Finalizado

21/04/2012

Descarga para Windows

Demo IA y Mecánicas para Windows

Descarga para MacOS

Demo IA y Mecánicas para MacOS

Equipo

Diseño:
José Miguel Barros Reche
Cristian Soriano Crespo
Modelado 3D:
José Miguel Barros Reche
Escultura 3D:
José Miguel Barros Reche
Animación 3D:
José Miguel Barros Reche
Programación mécanicas de juego:
Cristian Soriano Crespo
Programación IA:
Cristian Soriano Crespo
Programación interfaz de usuario:
José Miguel Barros Reche
Efectos de sonido:
José Miguel Barros Reche
Voz personaje principal:
José Miguel Barros Reche
Voz patrulla 1:
Christian Rodríguez Elvira
Voz patrulla 2:
Cristian Soriano Crespo
Música ambiente:
Nota: Debido a que en un futuro alguno de mis proyectos podría no estar disponible en los lugares de publicación, añado una versión descargable para que sea posible probarlo. Así mismo también añado un apartado de capturas de pantalla y gameplays por si en un futuro no pudiesen ejecutarse en los sistemas en rigor.
Ejemplo de código

/****************************************DECLARACION DE VARIABLES******************************************|
| TIPOS DE VARIABLES:                                                                                      |
| public : Aparece un campo en el editor, si es un array aparecerá un desplegable                          |
| private : No aparece en el editor                                                                        |
| @HideInInspector : Declarar variables publicas sin que aparezcan en el editor,(por ejemplo una Global)   |
***********************************************************************************************************/

@script RequireComponent(CharacterController)// Se requiere un character controller anidado al mismo game object
public var pathFinding : GameObject[];//PathFinding array vacia para poder elegir la candtidad en el editor
private var tiempoEspera : float = 0.0f;//Variable global que guarda el tiempo de espera en el punto de patrulla actual
public var colliderMuerto : GameObject;//Para activar el collider cuando mueres en el suelo
public var mensajes : mensaje_final;

//************VARIABLES DE ANIMACIONES**************/
public var idleAnimation : AnimationClip;
public var walkAnimation : AnimationClip;
public var saludarParadoAnimation : AnimationClip;
public var saludarCaminandoAnimation : AnimationClip;
public var avisarAnimation : AnimationClip;
public var deadAnimation : AnimationClip;
public var arrestarAnimation : AnimationClip;
//************FIN DE ANIMACIONES**************//

//************VARIABLES DE AUDIO**************/
public var audioSaludar : AudioClip;
public var audioCaminar : AudioClip;
public var audioAvisarRuido : AudioClip;
public var audioAvisarMuerto : AudioClip;
public var audioArrestar : AudioClip;
public var audioPreocuparse : AudioClip;
public var fxSource : GameObject;
//************FIN DE AUDIO**************/

public var walkMaxAnimationSpeed : float = 0.75;//Velocidad de la animación de caminar
public var saludarCaminandoMaxAnimationSpeed : float = 0.75;//Velocidad de la animación de saludar caminando
private var _animation : Animation;//Variable animation de tipo Animation para el control de las animaciones
public var distanciaVision : float = 0.0f;//Distancia a la que te ve el enemy
public var distanciaSaludo : float = 0.0f;//Distancia a la que los enemigos se saludan
public var anguloVision : float = 0.0f;//Angulo de vision
public var boleanaMoverse : boolean = false;//Boleana para saber si el enemigo se tiene que mover una vez expirado el tiempo
public var distanciaEscucha : float = 0.0f;//Distancia a la que te oye el enemy
private var teSaludan : boolean = false;//Boleana para cambiar el estado a saludarcaminando o parado

/*****************ESTADOS ANIMADOS DEL ENEMIGO (TIPOS)***********************/
enum EnemyState{
	Nada = 0,
	Idle = 1,
	Walking = 2,
	SaludarParado = 3,
	SaludarCaminando = 4,
	Avisar = 5,
	Arrestar = 6,
	Dead = 7
}
/*****************FIN ESTADOS ANIMADOS DEL ENEMIGO (TIPOS)*******************/

private var enemyState : EnemyState;//Variable de tipo enemystate (el enum)

var walkSpeed : float = 2.0;// Velocidad del enemy al moverse
private var moveDirection : Vector3 = Vector3.zero;// La direccion actual en la que te mueves x-z
private var moveSpeed : float = 0.0f;// Velocidad actual de movimiento en x-z
private var isMoving : boolean = false;// Nos estamos moviendo ?
private var puntoActual : int = 0;// Punto actual al que dirigirse
private var controller : CharacterController;//Para usar con character controller
public var enemigoVigilar : EnemigoVigilar[];//Hemos creado un tipo con la clase EnemigoVigilar y poniendo el tipo como array obtenemos un listado en el editor
private var calculoTiempoAviso : float = 0.0f;//Variable de tiempo para calcular con tiempoaviso y time.time comprobacion de aviso

/**********************************************CREACION DE UNA CLASE***************************************/
class EnemigoVigilar extends System.Object//Extension, hereda de System.Object
{
	public var companero : Collider;//Guardamos el collider del enemigo para cuando lo detectemos comprobar si es él
	public var tiempoAviso : float = 0.0f;//Variable de tiempo para el aviso introducimos en segundos
	@HideInInspector
	var tiempoCalculo : float = 0.0f;//Variable de tiempo calculado escondida en el editor
	@HideInInspector
	var hasSaludado : boolean = false;//Variable boleana para saber si nos ha saludado escondida en el editor
}

/****************************************************FUNCIONES PRINCIPALES(MAIN)************************************************/
//funcion Awake se llama una sola vez al ejecutar el programa
//funcion Update se llama en cada repetición mientras se este ejecutando el programa
//funcion start solo llamaría una vez en ese script

function Awake (){
	moveDirection = transform.TransformDirection(Vector3.forward);//Inicializamos la orientacion del enemigo al forward por defecto de la scena
	enemyState = EnemyState.Nada;	//Inicializamos la primera vez a idle la animacion
	controller = this.GetComponent(CharacterController);// Include del script Character controller y lo guardamos en una variable
	_animation = this.GetComponent(Animation);// Include del script Animation y lo guardamos en una variable
	
	/****************************CONTROL DE ERRORES*************************************/
	if(!_animation){
		Debug.Log("No esta Animation incluido");
	}
	if(!idleAnimation){
		_animation = null;
		Debug.Log("No idle animation found. Turning off animations.");
	}
	if(!walkAnimation){
		_animation = null;
		Debug.Log("No walk animation found. Turning off animations.");
	}
	if(!saludarParadoAnimation){
		_animation = null;
		Debug.Log("No saludar animation found. Turning off animations.");
	}
	if(!saludarCaminandoAnimation){
		_animation = null;
		Debug.Log("No saludar animation found. Turning off animations.");
	}
	if(!avisarAnimation){
		_animation = null;
		Debug.Log("No avisar animation found. Turning off animations.");
	}
	if(!deadAnimation){
		_animation = null;
		Debug.Log("No dead animation found. Turning off animations.");
	}
	if(!arrestarAnimation){
		_animation = null;
		Debug.Log("No arrestar animation found. Turning off animations.");
	}
  /****************************FIN CONTROL DE ERRORES*************************************/
}

function Update (){	
	if(enemyState != EnemyState.Dead && enemyState != EnemyState.Arrestar && enemyState != EnemyState.Avisar){
		enemyState = EnemyState.Nada;
		Buscar ();
		//Comprobamos que tenga enemigos que vigilar y llamamos a la funcion/Boleanamoverse para que no salude mientras patrulla
		if(enemigoVigilar.Length > 0 && boleanaMoverse == false){
			ComprobarTiemposEnemigo ();
			//ComprobarEnemigo ();
		}
		PathFinding ();
		
		if(isMoving){
			//Normalizamos el moveDirection a través de la funcion para guardarlo normalizado en la variable
			moveDirection = pathFinding[puntoActual].transform.position - this.transform.position;
			moveDirection.y = 0;
			moveDirection.Normalize();
			this.transform.rotation = Quaternion.LookRotation(moveDirection);//Orientamos el player
		}
		else{
			moveDirection = Vector3.zero;
		}
		
		if(enemyState == EnemyState.Nada){
			if(teSaludan == true){
				if(isMoving){
					enemyState = EnemyState.SaludarCaminando;
				}
				else{
					enemyState = EnemyState.SaludarParado;
				}
				teSaludan = false;
			}
			else{
				if(isMoving){
					enemyState = EnemyState.Walking;
				}
				else{
					enemyState = EnemyState.Idle;
				}
			}
		}
		
		if(enemyState == EnemyState.Walking || enemyState == EnemyState.SaludarCaminando){
			moveSpeed = walkSpeed;
		}
		//Aplicamos el movimiento
		var movement = moveDirection * moveSpeed;
		movement *= Time.deltaTime;
		controller.Move(movement);
	}
	
	if(_animation != null){
		Animaciones();
	}
}
/****************************************************FIN DE FUNCIONES PRINCIPALES(MAIN)************************************************/

/***************************FUNCIONES Y PROCEDIMIENTOS**************************************/
function Animaciones(){
	if(enemyState != EnemyState.Walking){
		if(this.audio.clip == audioCaminar){
			this.audio.clip  = null;
			this.audio.Stop();
		}
	}	
	switch(enemyState){
		case EnemyState.Idle:
			_animation.CrossFade(idleAnimation.name);
		break;
	
		case EnemyState.Walking:
			_animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, walkMaxAnimationSpeed);
			_animation.CrossFade(walkAnimation.name);
			if(this.audio.clip != audioCaminar){
				this.audio.clip  = audioCaminar;
				this.audio.Play();
			}
		break;
			
		case EnemyState.SaludarCaminando:
			_animation[saludarCaminandoAnimation.name].wrapMode = WrapMode.Once;
			_animation[saludarCaminandoAnimation.name].layer = 1;
			_animation[saludarCaminandoAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, saludarCaminandoMaxAnimationSpeed);
			_animation.CrossFade(saludarCaminandoAnimation.name);
			fxSource.audio.PlayOneShot(audioSaludar, 1);
		break;	
		
		case EnemyState.SaludarParado:
			_animation[saludarParadoAnimation.name].layer=1;//ponemos una capa por encima para que no espere a terminar el idle
			_animation.CrossFade(saludarParadoAnimation.name);
			fxSource.audio.PlayOneShot(audioSaludar, 1);
		break;
		
		case EnemyState.Avisar:
			_animation[avisarAnimation.name].layer=1;//ponemos una capa por encima para que no espere a terminar el idle
			_animation.CrossFade(avisarAnimation.name);
		break;
		
		case EnemyState.Dead:
			_animation[deadAnimation.name].layer=1;//ponemos una capa por encima para que no espere a terminar el idle
			_animation.CrossFade(deadAnimation.name);
			colliderMuerto.active = true;
		break;
		
		case EnemyState.Arrestar:
			_animation[arrestarAnimation.name].layer=1;//ponemos una capa por encima para que no espere a terminar el idle
			_animation.CrossFade(arrestarAnimation.name);
		break;
	}
}

function Buscar (){
	//OverlapSphere Esfera de raycast para evaluar todo lo que entre dentro de ella, con mascara evita objetos 
	var hit : RaycastHit;//Variable para guardar el objeto entero con que impacta el raycast
	var raycastMask = 1 << 8 | 1 << 9;//Mascara del player y el enemigo,para que los localice solo a ellos en la overlapsphere
	var colliders : Collider[] = Physics.OverlapSphere(this.transform.position,distanciaVision,raycastMask);//Lo lanzamos desde la posicion del enemigo y distancia vision calcula el radio de amplitud, playermask solo detecta al player.

	//FOREACH
	for(var other : Collider in colliders){
		if(other.tag == "Player"){
			//Sacar el angulo utilizando el vector forward del enemigo y el vector entre el player 
			//y el enemigo para determinar si estamos en el rango de vision
			var vectorPlayerEnemy : Vector3 = other.transform.position - this.transform.position;
			var anguloPlayerEnemy : float = Vector3.Angle(vectorPlayerEnemy,this.transform.forward);

			if(anguloPlayerEnemy <= anguloVision){
				if(Physics.Raycast(this.transform.position + Vector3.up, vectorPlayerEnemy.normalized,hit,distanciaVision)){
					if(hit.collider.tag == "Player"){
						//Gameover y boton de retry o exit
						enemyState=EnemyState.Arrestar;
						fxSource.audio.PlayOneShot(audioArrestar, 1);
						mensajes.MostrarMensaje(Mensaje.Visto);
					}
				}
			}
			break;
		}
		else if (other.tag == "Enemy"){
			//Sacar el angulo utilizando el vector forward del enemigo y el vector entre él y el segundo enemigo para determinar si están en el rango de visión.
			var vectorEnemyEnemy : Vector3 = other.transform.position - this.transform.position;
			var anguloEnemyEnemy : float = Vector3.Angle(vectorEnemyEnemy,this.transform.forward);

			if(anguloEnemyEnemy <= anguloVision){
				var enemyMask = 1 << 15;
				enemyMask = ~ enemyMask;//invertimos para que lo coja todo menos la mascara que le damos
				if(Physics.Raycast(this.transform.position + Vector3.up, vectorEnemyEnemy.normalized,hit,distanciaVision,enemyMask)){
					if(hit.collider.tag == "Enemy"){
						var auxScript = other.GetComponent("enemy_ai");
						var auxEstado : EnemyState = auxScript.GetEstado();
						//Gameover y boton de retry o exit
						if(auxEstado == EnemyState.Dead){
							enemyState = EnemyState.Avisar;
							fxSource.audio.PlayOneShot(audioAvisarMuerto, 1);
							mensajes.MostrarMensaje(Mensaje.Cadaver);
						}
						else{
							for(var tmp : EnemigoVigilar in enemigoVigilar){
								//Este if reinicia el contador si hit contiene al compañero a vigiliar, tambien se evalua si se le ha 
								//saludado y si es así, no saludarlo de nuevo mientras siga en la distanciaSaludo
								if(hit.collider == tmp.companero){
									if(Vector3.Distance(this.transform.position , hit.transform.position) <= distanciaSaludo){
										if(tmp.hasSaludado == false){
											enemyState = EnemyState.SaludarParado;
											tmp.hasSaludado = true;
											auxScript.Saludar();
										}
									}
									else{
										tmp.hasSaludado = false;
									}
									tmp.tiempoCalculo = Time.time + tmp.tiempoAviso;
								}
							}
						}
					}
				}
			}
		}
	}
	
	var playerMask = 1 << 8;//Mascara del player,para que lo localice solo a el en la overlapsphere
	//Comprobación del audio
	colliders = Physics.OverlapSphere(this.transform.position,distanciaEscucha,playerMask);
	for(var other : Collider in colliders){
		if(other.tag == "Player"){
			var scriptPlayer : Tercera_persona_demo = other.GetComponent("Tercera_persona_demo");
      //Si no esta agachado y entra en el rango de escucha, lo delatamos y activamos el Game Over.
			if(scriptPlayer.GetEstado() == CharacterState.Walking){
				enemyState=EnemyState.Avisar;
				fxSource.audio.PlayOneShot(audioAvisarRuido, 1);
				mensajes.MostrarMensaje(Mensaje.Oido);
			}
			break;
		}
	}
}

function PathFinding (){
	if(	//Si estamos llegando aproximadamente al punto de patrulla que se dirija al nuevo punto
    Mathf.Approximately(Mathf.RoundToInt(this.transform.position.x),Mathf.RoundToInt(pathFinding[puntoActual].transform.position.x)) && 
    Mathf.Approximately(Mathf.RoundToInt(this.transform.position.z),Mathf.RoundToInt(pathFinding[puntoActual].transform.position.z))
  ){
		moveDirection = Vector3.zero;
		isMoving = false;
		puntoActual = (puntoActual + 1) % pathFinding.Length;//Sacamos el modulo de la division para no sobrepasar el máximo del array
		tiempoEspera = 0.0f;
	}
	else if(tiempoEspera == 0){
		var tmp : int = puntoActual - 1;
		if(tmp < 0){
			tmp = pathFinding.Length - 1;
		} 
		var scriptGizmo : ponerGizmo = pathFinding[tmp].GetComponent("ponerGizmo");
		tiempoEspera = Time.time + scriptGizmo.GetEspera();
	}
	else if(tiempoEspera <= Time.time){
		isMoving = boleanaMoverse;
	}
}

function ComprobarTiemposEnemigo (){
	for(var tmp : EnemigoVigilar in enemigoVigilar){
    //Si ha pasado el tiempo y uno de los enemigos que debe vigilar el otro enemigo no ha llegado,
    //activamos el ir a buscarlo y los signos de preocuparse.
		if(Time.time >= tmp.tiempoCalculo && tmp.tiempoCalculo != 0){
			boleanaMoverse = true;
      fxSource.audio.PlayOneShot(audioPreocuparse, 1);
			break;
		}
	}
}

public function Saludar (){
	yield WaitForSeconds (1);
	teSaludan = true;
}

public function teMatan (){
	enemyState = EnemyState.Dead;
}

public function GetEstado() : EnemyState
{
	return enemyState;
}
Vídeos
Capturas de pantalla
IA y Mecánicas Póster
IA y Mecánicas Inicio de partida
IA y Mecánicas Asomarse
IA y Mecánicas Avanzar agachado
IA y Mecánicas Visto
IA y Mecánicas Oído
IA y Mecánicas Cadáver encontrado