En el mundo real, el movimiento es mucho más complejo de lo que hemos visto hasta ahora. Hay fuerzas que lo producen, otras que lo mantienen y otras que lo frenan. Un objeto en movimiento está sometido al menos, a dos de ellas: aceleración y fricción. En nuestra sencilla simulación veremos cómo la aceleración pone en movimiento un objeto en la dirección deseada y cómo la fricción frena el movimiento y al mismo tiempo limita la velocidad máxima. Aunque la escena de naves que usaremos para montar un juego de asteroides al final de esta serie de artículos se desarrolla en el espacio, es obvio que en éste no hay atmósfera y por tanto no puede haber fricción, pero para que el juego tenga una mínima jugabilidad y a efectos de estudio, se ha optado por hacer uso de estas características. También veremos cómo se relacionan entre sí estas fuerzas entendidas como vectores y cómo extraer un vector de dirección a partir del ángulo que forma el eje del objeto con el eje de coordenadas X.
Este es el efecto que conseguimos al mover la nave. Podemos girar en cualquier dirección, pero la aceleración sólo se produce en la dirección en la que apunta la nave, que es independiente de la trayectoria y aceleración que tuviera anteriormente.
Si recordamos entradas anteriores, vimos que para poner en movimiento un objeto bastaba con sumar el vector de velocidad a la posición actual de dicho objeto, siempre teniendo en cuenta que debemos hacerlo para cada componente de un punto XY separadamente. Ahora bien, el sentido del movimiento lo determina el punto dado por la velocidad. Para mover nuestra «nave» en una dirección determinada por su orientación necesitamos conocer dicha orientación y el ángulo que forma en relación a un eje. Pero primero debemos ser capaces de hacer rotar la nave sobre sí misma de una forma predecible, sabiendo cuál es su ángulo en cada momento.
El movimiento circular y la velocidad angular
Nuestra nave ha de girar sobre sí misma para poder orientarla hacia una dirección de avance. No profundizaré mucho en esto, ya que para nosotros resulta muy sencillo hacerlo, simplemente asignando un valor de ángulo de rotación al objeto objeto brush de la clase Paint, guardando una variable global con el valor del ángulo en radianes y finalmente sumando o restando un valor constante que representa la velocidad angular. Recomiendo leer el artículo de Wikipedia sobre el tema y tratar de entender el concepto. Una vez leído, vamos a ver las analogías en el código de gambas.
1 2 3 4 5 6 | Private Const TURN_VEL As Float = 0.05 Private $angle As Float = 0 Private $angle_vel As Float ' update ship $angle += $angle_vel |
Si consideramos el punto O como el centro de nuestra nave, el ángulo de giro lo representamos con la letra griega phi (φ) en referencia al eje X, tenemos que la variable $angle variará en función de la velocidad angular representada por omega (ω), que en nuestro caso es la constante TURN_VEL. Sumando en cada ciclo de tiempo la velocidad angular al ángulo en cada momento, el resultado es que el objeto gira sobre sí mismo.
Pero además nos interesa conocer el vector de velocidad que resulta en cada cambio del valor del ángulo, ya que éste será el que sumado a la velocidad anterior nos dará la nueva trayectoria de movimiento. Para ello basta saber que dado el ángulo, el coseno de dicho ángulo será igual al punto X del punto final del vector OA y que el seno del ángulo dado será el punto Y final del mismo vector. O sea:
De manera que en gambas podemos hacer una función muy sencilla que nos devuelve el vector de velocidad dependiendo del ángulo u orientación de la nave:
1 2 3 4 5 6 7 8 9 | '' returns a vector from a given angle Private Function angleToVector(angle As Float) As Float[] Dim vector As Float[] vector = [Cos(angle), Sin(angle)] Return vector End |
Así que como de costumbre, para actualizar la posición de un punto:
posición += velocidad
Y para indicar hacia dónde debe desplazarse la nave en función de su orientación:
velocidad += trayectoria
Siendo trayectoria el resultado devuelto por la función anterior, pero atención, sólo mientras estamos acelerando. Lo vemos mejor en gambas:
1 2 3 4 5 6 7 8 | $ship_pos[0] += $ship_vel[0] $ship_pos[1] += $ship_vel[1] forward = angleToVector(- $angle) If $trusting Then $ship_vel[0] += forward[0] $ship_vel[1] += forward[1] Endif |
Fricción
La fricción es una fuerza que se opone al avance de un cuerpo, dicho de una manera excesivamente simple. Podemos decir en matemáticas que es un valor opuesto al de la velocidad y la manera de obtenerlo es multiplicando la velocidad por un valor constante de signo opuesto:
fricción = velocidad * - c
por lo que:
velocidad = (1 - c) * velocidad
En nuestro caso, la constante c es un valor escogido en base a las magnitudes que manejamos, un poco mediante prueba y error. Hay que tener en cuenta que en la física del mundo real es mucho más complejo que todo esto. A efectos de simulación en un plano, donde no hay masas ni superficies, recurrimos a esta simplificación que es completamente válida para nuestro propósito. Dicho esto, veamos el código en gambas:
1 2 3 4 | Private Const FRICTION As Float = 0.03 $ship_vel[0] *= (1 - FRICTION) $ship_vel[1] *= (1 - FRICTION) |
Hemos declarado una constante global con un determinado valor (con el que podemos experimentar para ver su efecto) que en la fórmula anterior se lo restamos a 1. De manera que el resultado es un número siempre inferior a la unidad, provocando que al multiplicar la velocidad por ese número, el resultado siempre será menor que el anterior hasta que llegue a 0, deteniendo el objeto.
Hasta aquí la teoría. Voy a aclarar un par de cosas relativas al dibujo y rotación de la nave a tener en cuenta.
Dibujando la nave
Cuando hice pruebas con la clase Paint de gambas, vi el método Paint.DrawImage con el que resultaba muy sencillo pintar una imagen dentro del DrawingArea. Pero pronto me dí cuenta de que no era posible hacerla rotar sobre sí misma, algo que me dejó un poco extrañado. Profundizando más en la clase Paint, descubrí el objeto Brush (Realmente PaintBrush, ya que Brush es el método de acceso para crear dicho objeto) y ví que la forma correcta de pintar imágenes es hacerlo usando este objeto, y que también podía usarlo para rotar la imagen en cualquier momento.
Para empezar, tenemos que crear un objeto PaintBrush a partir de la imagen de la nave:
1 2 3 4 5 6 7 8 | ' cargamos la imagen en un objeto Image Private $ship_image As Image = Image.Load("tron.png") ' dentro del evento draw creamos un objeto PaintBrush Dim hBrush As PaintBrush ' el método PaintImage devuelve un objeto PaintBrush a partir ' de sus tres parámetros imagen, X, Y hBrush = Paint.Image($ship_image, $ship_pos[0], $ship_pos[1]) |
Ya tenemos la «brocha» o el pincel para pintar, pero para pintar una imagen no podemos hacerlo directamente sobre la superficie del DrawingArea, sino que debemos usar la brocha para rellenar un polígono o un círculo. En este caso he elegido un círculo justo porque la forma de la nave es circular y lamentablemente no he encontrado ninguna otra manera de solucionar un problema que paso a explicar.
El objeto PaintBrush es un pincel o brocha que es obligatorio usar para rellenar un objeto o figura ya sea de un color, gradiente o una imagen. En el caso de la imagen, resulta que el comportamiento es que el pincel es un patrón, es decir, contiene la imagen repetida infinitas veces, a modo de tiles o baldosas, por lo que cuando llenamos con el contenido del pincel una figura que es mayor que las dimensiones de la imagen, podemos ver cómo se repite el patrón.
Además, resulta que el punto de origen del pincel no es el centro de la imagen, sino su esquina superior izquierda, por lo tanto también es necesario trasladar ese punto al centro real de la imagen para que coincida en este caso con el centro del círculo y punto de rotación al mismo tiempo, ya que lo que se rota es el pincel y no el objeto que rellenamos con él. ¿Un poco de lío, no? Lo vemos más claro en la imagen que he preparado y en el código más abajo:
La figura 1 muestra la imagen original con la que creamos el pincel o PaintBrush. En un objeto image el origen siempre se encuentra en la esquina superior izquierda. Como el PaintBrush se crea a partir de la misma imagen, el origen sigue siendo el mismo.
En la imagen 2 creamos un círculo con el método Arc que posteriormente rellenaremos con el pincel previamente creado.
En la imagen 3 tenemos el círculo ya pintado con el la imagen del pincel. Si intentamos rotar el pincel con el método hbrush.Rotate(angulo en radianes) observamos que el pivote aún se encuentra en el origen de la imagen.
En la imagen 4 corregimos esto, usando hbrush.Translate(x, y) para desplazar el punto de origen del pincel. Generalmente, los valores de x, y serán siempre la mitad del ancho/alto de la imagen. En realidad lo que se desplaza es la imagen del pincel hacia el origen, y no al contrario… aunque el razonamiento es válido para ambos casos.
Estas operaciones son necesarias en la mayoría de los casos cuando trabajamos con imágenes. Aunque pueda parecer tedioso o inusual, la realidad es que todos los métodos de la clase Paint, al estar basados en Cairo, imitan las clases y métodos nativos.
Bien, pues una vez tenemos nuestra nave dibujada en pantalla, la forma de rotarla sobre sí misma es tan simple como rotar el pincel asignando una variable al ángulo que actualizaremos mediante eventos de teclado.
Lo vemos en el código a continuación:
1 2 3 4 5 6 7 8 9 | Dim hBrush As PaintBrush 'setup ship image hBrush = Paint.Image($ship_image, $ship_pos[0], $ship_pos[1]) hBrush.Rotate($angle) hBrush.Translate(-50, -50) Paint.Brush = hBrush Paint.Arc($ship_pos[0], $ship_pos[1], 50) Paint.Fill |
Aludiendo a los «magic-numbers» de un artículo anterior, podríamos haber creado una constante con las dimensiones de la imagen que usamos para crear el pincel de la nave y usarla como argumentos del método hbrush.Translate. Como la imagen mide 100×100 y el desplazamiento necesario para centrar su eje son 50 pixel hacia el origen, es decir, negativos, he preferido usar los números directamente para hacerlo más evidente.
Pues es todo de momento. Os dejo el proyecto en gambas para descargarlo y os animo a que experimentéis con él, cambiando valores, imágenes, jugando con las brochas, los valores de velocidad y fricción, etc hasta que se comprenda su funcionamiento. En próximas entradas, trataré de construir el juego de Asteroides completo mediante orientación a objetos (OOP) usando clases para las imágenes usadas, manejo de sprites, colisiones entre asteroides y proyectiles o la nave, explosiones, etc. Hasta entonces, a seguir practicando!