First person shooter

Vídeo

Video explicativo del videojuego.

Nivel

El nivel está diseñado con 3 zonas diferenciadas. La parte superior, zona de inicio situada en lo alto de una montaña, una base con diferentes bifurcaciones y pequeñas zonas exteriores y una amplia zona exterior con un río de lava.

En la primera parte tenemos enemigos básicos y una puerta cerrada que enlaza con la siguiente zona. La puerta no se abrirá hasta que consigamos la llave correspondiente, en este caso, la tarjeta naranja que soltará el enemigo rojo de la zona.

Una vez entramos en la base tenemos un ascensor que une la parte superior con la inferior. La base inferior tiene pasillos que dan a pequeñas zonas exteriores donde hay más enemigos y objetos almacenables. Por un lado, tiene la salida del mapa, que solo nos dejará pasar si tenemos la tarjeta azul, y por el otro lado podremos salir al exterior.

La tercera zona, la zona exterior, es una amplia extensión de desierto dividida por una grieta con lava, el jugador tendrá que derrotar al jefe final, que es un dron grande, para obtener la llave azul y poder así poder salir del mapa.

En todas las zonas se han añadido prefabricados y grupos de los mismos como decoración: Cajas, barriles y rocas.

Armas

Las armas que he implementado son pistola, metralleta, escopeta y granada. Las he implementado usando scriptable objects para guardar la información de funcionamiento del arma: daño, velocidad de disparo, tiempo de recarga… también para guardar la información del jugador relacionada con ellas: cuantos cargadores tiene, cuantas balas en el cargador actual y si tiene o no el arma.

El WeaponController administra el funcionamiento de las armas. Se encarga de los disparos y de la recarga según el tipo de arma.

El WeaponInventory se encarga de gestionar cuando recojamos un arma o cambiamos de arma activa. El GameObject del jugador tiene los GameObjects de las 4 armas como hijos. Esto nos permite activar y desactivar las armas según las seleccione el jugador.

La pistola y la metralleta funcionan igual, la única diferencia es que la metralleta es automática. Esto se controla mediante un booleano. Si es automática, podremos dejar el ratón pulsado y el arma continuará disparando respetando los tiempos entre disparos. La recarga funciona descartando el cargador actual, vacío o con alguna bala dentro y reemplazándole por un cargador entero. Las balas restantes en el cargador se pierden. Podría guardar el número de balas en lugar del número de cargadores para evitar que se pierdan, pero no me parece realista y otorga al juego un poco más de gestión.

La escopeta usa un método distinto de recarga, tiene una capacidad de balas en el arma y se recargan de forma individual. Las balas dentro del arma no se perderán al recargar, lo que nos permite recargar en cualquier momento.

Cuando seleccionamos la granada, solo muestra el número de granadas que tenemos. La recarga no existe, únicamente hay que esperar un tiempo para poder volver a lanzar otra granada. A diferencia de las otras armas, al lanzar una granada esta se instancia y se le aplica una fuerza inicial. Incorpora un contador de tiempo que cuando llega a 0 explota dañando en área. El daño que hace depende de la distancia entre la granada y el objetivo, disminuyendo el daño de manera lineal hasta el radio de explosión.

HUD

He creado un HUD en la parte inferior izquierda. En él se muestra la barra de vida, la barra de escudo, el estado de la munición y tres indicadores para las tarjetas.

He creado dos atributos en el script PlayerController: salud (health) y escudo (shield). Mediante un algoritmo el daño se distribuye entre el escudo y la vida si tenemos escudo. En caso de tener el escudo ya agotado, todo el daño irá a la vida.

Las barras de vida y escudo funcionan igual. En la componente imagen he cambiado el ajuste de tipo de imagen a filled, ajustando el parámetro fill amount ajustamos el tanto por 1 de relleno de la imagen.

Para la mayoría de armas, la información de la munición tiene dos números: la munición en el cargador actual y el número de cargadores que tenemos. En el caso de armas arrojadizas, solo existe el número de objetos arrojadizos disponible. Cada arma tiene en su scriptableObject de configuración los iconos de bala y cargador para mostrarlo en el HUD. En el caso de las granadas, solamente tiene un icono y el segundo se pone como una imagen transparente para que no muestre el cuadrado en blanco por defecto de la componente Imagen.

A mayores he añadido 3 imágenes para representar si el jugador tiene o no tiene las llaves para abrir las puertas, tarjetas en nuestro caso. Las imágenes están difuminadas con alpha y cambiará a alpha 1 cuando las recojamos.

Plataformas móviles

Un sencillo script controla el movimiento de la plataforma y la presencia del jugador para iniciar el movimiento. Cuando el jugador sube a la plataforma comienza un retraso en el inicio del movimiento. Para suavizar el movimiento, la plataforma se pone como padre del objeto del jugador. Al salir de la plataforma se elimina esta asignación.

Para controlar la plataforma se usan dos transforms que le indican el punto de inicio y de final. Como las transforms se mueven con la plataforma, las posiciones de ambas transforms se guardan en variables en la función Start.

El movimiento se hace con interpolación lineal Vector3.Lerp.

Con este algoritmo podemos utilizar el mismo prefab para crear rutas entre dos puntos cualesquiera del mapa.

Puertas cerradas

Partiendo del script de las puertas automáticas que hemos visto en teoría, he agregado el requisito de que el GameObject que active la puerta tenga que tener la componente de KeyInventory. KeyInventory se encarga de decir si el GameObject tiene o no la llave, en nuestro caso tarjeta.

El script KeyInventory administra las llaves del jugador, proporcionando métodos para recogerlas y para que la puerta u otros GameObjects puedan comprobar si el jugador la tiene.

IA básica

He usado los scripts de la teoría con la misma máquina de estados. He añadido algunas mejoras:

-Los sonidos de disparar, recibir impacto, muerte y movimiento.

-He modificado la forma en la que la IA busca al jugador. Mientras rota para buscar el jugador, emite diferentes Raycast formando un abanico vertical para intentar encontrar al jugador emulando un campo de visión. En el caso de tener el jugador localizado, cuando tiene que disparar, comprueba que tena tiro limpio al jugador. Esto evita que el enemigo dispare a través de objetos sin tener una visión clara del jugador. Si detecta que no tiene tiro limpio, buscará al jugador de nuevo y si no lo encuentra, volverá a patrullar.

Sistema de drop

He implementado estos objetos usando prefabs y scriptable objects como configuración.

Los objetos almacenables tienen un script que cuando el jugador está a rango, comprueban la componente correspondiente del jugador y si el jugador lo ha podido recoger, desaparecen. Los objetos de munición interactúan con la componente WeaponInventory, los de salud y escudo con la de PlayerController y las llaves (tarjetas) con la de KeyInventory.

Si no se cumplen las condiciones, el jugador no las recogerá y no desaparecerán, quedándose disponibles para más adelante. Por ejemplo, si el jugador tiene la salud llena, no recogerá botiquines, igual que el escudo y la munición.
El sistema de drop tiene dos partes, la parte aleatoria y la fija. De esta forma podemos hacer que un jefe siempre suelte una llave concreta, pero que suelte munición, botiquines y escudo aleatorios. Todo esto se configura con scriptable objects. Podemos añadir el objeto que queremos soltar con la probabilidad que queremos.

Configuración de objetos y probabilidad.

También podemos configurar cómo se comporta el sistema de drop. Cuantos objetos como máximo, la fuerza con la que salen despedidos y el ángulo máximo. Por si nos interesan enemigos con muertes más espectaculares.

Configuración del drop.

Estado de juego

El control de juego se hace mediante el GameplayManager. Inicialmente, el juego empieza en pausa y hay que esperar unos segundos para empezar a jugar.
Si llegamos a la salida del nivel se iniciará una cuenta atrás para confirmar que el jugador quiere terminar la pantalla. Una vez activada la cuenta atrás, podemos salirnos de la zona de salida para interrumpir el contador, en caso de que queramos volver para recoger vida o investigar todos los rincones.

Si el jugador muere aparecerá la pantalla de GameOver, dando la posibilidad de reiniciar el nivel o salir al menú.

En caso de caer en la lava, la muerte será instantánea. Aparecerá la pantalla de GameOver. Y un efecto rojo en la pantalla.

Si caemos del mapa, la muerte será también instantánea.

El control de juego tiene pausa incorporada. Mediante eventos, los objetos de juego saben cuándo el juego está en marcha o en pausa, pudiendo detener al jugador y a los enemigos cuando cargamos el menú de juego.

Deja un comentario