En esta ocasión vamos a estar viendo la teoría y practica detrás de la puntería dinámica. Esta mecánica es usada en prácticamente todos los juegos de acción, se trata de como la precisión de un arma de fuego decrece a medida que se dispara en modo automático o ráfaga. También veremos como la distancia afecta la puntería y cómo manejar la distancia como un factor.
Teoría
Para entender cómo funciona la aplicación que vamos a estar usando es necesario entender aún mas sobre matemática vectorial y proyección de rayos. Básicamente el comportamiento al que queremos llegar es que, al disparar nuestra arma, el impacto no sea siempre certero según donde indica nuestra mira. Desde un punto de vista geométrico, convertimos un punto en una esfera de imprecisión.
Una vez que tenemos definida la distancia de puntería, tomamos un punto al azar dentro de la esfera. Con ese punto y teniendo en cuenta que nuestro punto de vista es el objeto de escena conocido como Cámara. Podemos trazar un rayo desde el arma hasta el punto.

El detalle con esto es que nuestra perspectiva como jugador y el rifle no están centrados en el mismo punto. Acá comienza el problema de la perspectiva en tercera persona. Lo que si podemos hacer es encontrar el punto al que la cámara está centrada y hacer que el rifle dispare hacia ese punto. De esta manera estamos trazando un rayo desde la cámara hasta el punto generado y desde el rifle hasta el punto donde la cámara encontró un obstáculo.
Con esta solución además cubrimos el caso donde la cámara se centra en un objeto que para el rifle esta ocluido por otro objeto, pero para la cámara no.
Practica
Esta práctica no tiene muchas clases pero si son extensas. Podemos comenzar con nuestra clase de rifle, sabemos que va a tener varias propiedades para definir su comportamiento. Estas incluyen, puntería, distancia de puntería, velocidad de disparo, referencias a prefabs, etc.
Una de nuestras propiedades expuestas al editor es puntería, pero en realidad para facilitarnos los cálculos es mejor trabajar internamente con el concepto de imprecision o inaccuracy.

Cuando se trata del comportamiento principal, el de disparar es cuando entramos en una sección un poco más compleja, pero por suerte podemos separar diferentes secciones en diferentes métodos para mejorar la legibilidad. Primero podemos conseguir el punto aleatorio dentro de la esfera de imprecisión. Acto seguido, proyectamos el rayo desde el rifle hasta el punto de foco de la cámara. Según el resultado de esa proyección, decidimos si mostramos el impacto o solo la línea de disparo.

Physics.Raycast(Ray ray, out RaycastHit hit, float range) proyecta un rayo (línea recta) cubriendo una distancia dada y devuelve un bool. El bool es true si el rayo intersecta cualquier collider en su trayecto, y la información de la intersección se guarda en el 2do valor retornado, el RaycastHit. Esta clase contiene informacion muy útil como el punto exacto de colisión y una referencia explícita al collider detectado.
Un detalle no menor es el rayo proyectado desde el arma. aimPoint es un punto en el espacio que denota una posición hacia donde queremos mirar. El detalle es que Ray(Vector3 from, Vector3 direction) usa el 2do vector como dirección y no como punto espacial. Todo Vector3 usado como dirección es trasladado hacia el punto de referencia antes de ser usado como posición espacial. Por eso la resta de vectores (aimPoint – firePosition) basicamente lo que estamos diciendo es que se trace un rayo desde firePosition en dirección hacia el punto espacial de aimPoint.
El beneficio de adoptar la práctica de hacer métodos sin implementación previa al uso es que podemos separar en pasos nuestro flojo de código, previamente ya sabíamos que necesitaríamos un Vector3 que era nuestro punto al azar, y es mejor trabajar primero en el flujo general, todo lo que sea un requisito de ese flujo general lo podemos delegar a funciones sin implementación. Esto nos ayuda a tener una mejor visión del caso de uso sin tener que interrumpir la cadena de pensamiento para implementar algún algoritmo.
Ahora implementemos la generación del punto aleatorio. Podemos usar una función muy cómoda del Random de Unity que nos devuelve un Vector3 aleatorio que se ubica dentro de una esfera de radio 1. A este vector lo podemos multiplicar por la imprecisión, y aquí es la razón por la cual nos conviene a nivel de cálculo usar imprecisión y no puntería. ¡La imprecisión es un factor y no un divisor!
Una vez obtenido ese punto lo que tenemos que hacer es trasladarlo a la distancia de puntería, esto nos termina dando un punto aleatorio en una esfera de imprecisión a la distancia adecuada. Una vez tenemos este punto, trazamos un rayo desde la cámara, la idea de este rayo es detectar si hay algún objeto entre el punto donde terminaría nuestro disparo y la cámara. Si existe un objeto, vamos a devolver el punto de intersección, sino, el calculado al azar. La razón detrás de esto esta explicada en la teoría, no podemos simplemente hacer que el arma dispare hacia el punto calculado porque si hay un objeto de por medio, nos va a mostrar un impacto lejos del punto de mira. Geometricamente no es que este mal, pero para el jugador se va a sentir muy extraño que el personaje este disparando al costado de donde nuestra perspectiva está apuntando.

Por ultimo tenemos la cortina que nos permite repetir la acción de disparo solo si se cumple el tiempo de velocidad de disparo. Y la estructura básica de la clase Rifle.
A nivel de estructura básica lo único que nos tenemos que asegurar es que la puntería actual tienda a regresar al valor por defecto y actualizar la mira dinámica.
El efecto que se busca con esta práctica es que la imprecisión dependa de la distancia y del arma, por ende, la agrupación de impactos va a ser menor en obstáculos más cercanos.
Mira Dinámica
Nuestro objeto de mira dinámica es bastante sencillo de implementar, pero no vamos a estar manejando escalas ni posición especifica de ninguno de los 4 rectángulos que conforman la mira. Vamos a usar un acercamiento mas ingenioso, usando las anclas de los componentes de UI de Unity para forzar que los rectángulos siempre estén en la posición relativa adecuada y luego lo que hacemos es ajustar las dimensiones del rectángulo padre.

Los 4 componentes de la mira van a estar anidados bajo un padre que solo tendrá el comportamiento de la mira avanzada. La idea de este script es modificar el sizeDelta del rectángulo (que se traduce al ancho y alto en la transform) según la imprecisión del arma.


Aquí abajo vemos el efecto deseado, agregue una imagen blanca que cubre toda la dimensión del padre mira para demostrar el punto de las Anclas.
Movimiento, Cámara y control
Por último, sin entrar mucho en detalles, este es el código para controlar el personaje y la cámara. Es bastante estándar y no es relevante al concepto de puntería en sí.