top of page
Buscar

6. Movimiento de Objetos

Foto del escritor: Fernando SansberroFernando Sansberro

Actualizado: 25 dic 2021

En este momento tenemos todo pronto para comenzar a programar el movimiento de los objetos. ¿Has visto la manera en que se mueven los objetos en un videojuego y te has preguntado cómo es posible lograrlo? Esto es lo que veremos en este capítulo.


Algunos objetos se mueven en forma constante, otros aceleran como un cohete o como un auto de carreras, otros caen por el efecto de la gravedad como ocurre al patear una pelota o disparar un proyectil, otros se deslizan sobre el hielo o se frenan con fricción en la tierra, etc. Todo lo que se nos pueda ocurrir al diseñar nuestro juego lo podremos implementar utilizando los conceptos fundamentales de la física.


Es inevitable escuchar la palabra física y tener algo de miedo, sentir que está lleno de fórmulas complicadas que tenemos que memorizarlas, pero nada más alejado de la realidad cuando se trata de videojuegos. Los conceptos de física, al verse aplicados en un videojuego, se convierten en conceptos intuitivos, fáciles de entender y, lo que es más importante, aplicables a nuestro videojuego para mover objetos de maneras sorprendentes.


En este capítulo vamos a ver los conceptos de velocidad y aceleración, y cómo modelamos el código para que lo que programemos lo podamos usar en todos nuestros juegos. Más adelante veremos los conceptos de gravedad y fricción que están relacionados a la velocidad y a la aceleración.



Introducción a la Física


La física es la ciencia que estudia las propiedades y el comportamiento de los objetos en la naturaleza (energía, materia, tiempo, espacio y las interacciones de estos cuatro conceptos entre sí).


Entre otras cosas, la física describe un objeto cayendo por la gravedad (como la manzana de Newton), las órbitas de los planetas, la electricidad, etc. Existen muchas áreas de especialización dentro de la física, pero la rama de la física que utilizaremos para mover objetos en los videojuegos es la mecánica clásica.


La mecánica clásica describe el movimiento de los objetos. Por ejemplo, si tenemos una manzana en la mano y la soltamos, es obvio que se va a caer por efecto de la gravedad. En muchas situaciones sabremos qué es lo que va a pasar, solamente utilizando el sentido común, pero en otras situaciones no será simple saber qué es lo que va a ocurrir.



Física Real vs. Física Simulada


Siempre deberemos hacer una distinción entre las fórmulas físicas reales y las fórmulas que simulan la física, o dicho de otra manera, son fórmulas suficientemente buenas como para ser utilizadas en los juegos. A veces, implementar en un juego algunas fórmulas o ecuaciones físicas es muy pesado como para usarse en lenguaje Python. Por eso es que a veces utilizaremos fórmulas que son sencillas de programar, y que son lo suficientemente buenas como para que el resultado sea muy similar a la realidad, ejecutando la menor cantidad posible de cálculos.


Además, como hemos visto en muchos videojuegos, la física de un videojuego se escribe para que el juego sea divertido de jugar, no para que las cosas se comporten como en la vida real (por ejemplo, basta ver qué tan alto salta un personaje en un juego de plataformas para entender que no se utiliza física simulando el mundo real).



Vectores


Antes de entrar con el tema de velocidad, necesitamos saber lo que es un vector. Si bien por ahora no los utilizaremos para programar nuestro primer juego, sí debemos saber que son. Más adelante, siempre programaremos juegos utilizando vectores, pues es algo imprescindible.


Un vector es un objeto matemático que contiene dos informaciones: una magnitud (es un número) y una dirección (generalmente se representa por el componente horizontal y el componente vertical). En la figura 6-1 se muestra gráficamente un vector.


Figura 6-1: Un vector y sus coordenadas.



En este caso, el vector v tiene su punto de apoyo (origen) en las coordenadas (0, 0), y termina en las coordenadas (x, y). Los vectores se pueden descomponer siempre en dos vectores, según la componente x (horizontal) y la componente y (vertical), que es lo que usamos cuando, por ejemplo, a un objeto lo movemos cierta cantidad de unidades en la horizontal y cierta cantidad de unidades en la vertical.


Más adelante vamos a reescribir todo el código de movimiento para que use vectores, escribiendo para esto una clase CVector (lo haremos en el siguiente libro de la serie).

Por ahora, utilizaremos en forma separada las componentes horizontal y vertical para mover objetos. Esto significa que tendremos dos variables para la posición (las coordenadas x e y), y también dos variables para definir una velocidad (sus componentes x e y).



Velocidad y Rapidez


Supongamos que un auto se mueve por una carretera. Cuando el auto se mueve, tiene una cierta velocidad. La velocidad es un vector (indicando dirección), y la rapidez es la magnitud (también denominado largo o módulo) del vector velocidad. Por ejemplo, si decimos que el auto viaja a 60 km/h en dirección este, entonces tenemos:


Velocidad: Es el vector 60 km/h hacia el este. Es un vector.

Rapidez: 60 km/h (es la magnitud del vector velocidad). Es un número.


La velocidad es el vector que resulta de combinar la rapidez (el número) con la dirección (la flecha).


Nota: En inglés, la velocidad se dice “velocity” y se refiere al vector velocidad. Rapidez se dice “speed” y se refiere a la magnitud del vector velocidad.



Rapidez


La rapidez indica qué tanta distancia recorre un objeto en cierto tiempo. Matemáticamente esto corresponde a la ecuación:



Rapidez = Distancia / Tiempo



Por ejemplo, si un auto se mueve a 80 km/h, esto quiere decir que recorre una distancia de 80 kilómetros en una hora. Si el auto hiciera 80 km en 2 horas, esto significa que el auto recorre 40 kilómetros en una hora: 80 km / 2 hs = 40 km/h.


Figura 6-2: El auto moviéndose a 40 km/h.



Como la rapidez es la distancia realizada en cierto tiempo, podemos calcular qué distancia recorrerá el auto, por ejemplo, en cuatro horas, despejando de la ecuación:



Distancia = Rapidez * Tiempo



Como la velocidad del auto es 80 km/h, entonces tenemos: 80 km/h * 4 hs = 320 kms. Con esta ecuación podemos calcular la distancia que se mueve un objeto si sabemos su rapidez y el tiempo que transcurre.


Del mismo modo, si sabemos la distancia que ha recorrido un auto y a que rapidez iba, podemos saber el tiempo que le tomó:



Tiempo = Distancia / Rapidez



Si el auto recorrió 320 kms y llevaba una rapidez de 80 km/h, entonces 320 kms / 80 km/h = 4 hs. Al auto le toma 4 horas recorrer 320 kms a una rapidez de 80 km/h.


Tiempo y Frames


Cuando hablamos de la rapidez de un objeto, decimos que se mueve una determinada distancia en cierto tiempo. La distancia generalmente se mide en metros, o kilómetros cuando se trata de un auto, pero cuando se trata de un juego, y movemos un objeto, nos queda más natural dar la velocidad en píxeles. Cuando en el ejemplo de los cuadrados incrementamos en 2 la coordenada x para que se mueva, lo que estamos haciendo es moviéndolo 2 píxeles en cada frame. La rapidez entonces en ese caso es de 2 px / frame.


El tiempo que transcurre entre dos llamadas a update() es un frame. Si por ejemplo, el juego corre a 30 FPS (30 frames por segundo), el tiempo de un frame será equivalente a 1 / 30 segundos (0,03333... segundos).


Si usamos frame como unidad de tiempo, podemos reemplazar en las ecuaciones al tiempo por frames. Entonces, como Distancia = Rapidez * Tiempo, la ecuación queda:



Distancia = Rapidez * Frames



De esta forma, podemos decir que una nave en un juego, por ejemplo, se mueve a una velocidad de diez píxeles por frame. La distancia recorrida por un objeto sigue siendo una medida de distancia, excepto que en un juego generalmente es la cantidad de píxeles que se mueve en un frame. Entonces, en nuestros juegos, la distancia también la manejaremos en píxeles.


Nota: En los primeros juegos que hagamos, el mundo será la pantalla, por lo cual nuestras coordenadas y distancias estarán relacionadas a la pantalla. En futuros juegos, usaremos coordenadas y distancias del “mundo” (expresadas en nuestro mundo virtual), independientemente de cómo se ve el juego o de la resolución de la pantalla.


Nota: En algunos entornos o motores (como por ejemplo en Unity en 3D) es mejor trabajar en unidades virtuales, ya que la pantalla o el mundo se puede escalar y la distancia entre dos objetos no corresponde a la distancia en píxeles. Todo esto lo veremos a futuro. Por ahora nos concentramos en aprender a crear nuestro primer videojuego.



Movimiento a Velocidad Constante


Vamos a mover a tres naves espaciales en nuestro juego, cada una con una velocidad diferente. Pondremos las tres naves a la izquierda de la pantalla y éstas se moverán hacia la derecha. La nave de más arriba se moverá a 4 píxeles por frame, la nave del medio lo hará a 2 píxeles por frame y la nave de más abajo se moverá a 1 píxel por frame.


Abre el ejemplo de la carpeta: capitulo_06\001_velocidad. En este ejemplo los objetos tienen velocidad.


Para mover un objeto, en cada frame le sumamos a sus coordenadas la distancia que se mueve. Tendremos en CGameObject la velocidad del objeto en las variables mVelX y mVelY, siendo mVelX la velocidad en la componente horizontal (x) y mVelY la velocidad en la componente vertical (y). Las siguientes sentencias mueven el objeto de lugar.


# Mover el objeto.
def update(self):

   # Mover el objeto.
   self.mX += self.mVelX
   self.mY += self.mVelY

   ...

Si miramos en la función update() de CGameObject, hemos puesto estas dos líneas que mueven el objeto según estas dos variables de velocidad. Si cambiamos cualquiera de estos valores de velocidad, el objeto se moverá. La idea es decirle a un objeto, que tiene una velocidad determinada, y que al hacer eso el objeto se mueva solo (se moverá solo porque en su función update() tiene estas líneas de código que le agregamos).


Nota que también hemos quitado los parámetros de incremento de posición que antes recibía la función update() como parámetros. Ahora el objeto se moverá automáticamente cuando cambiemos su velocidad.


A partir de ahora vamos a crear muchas funciones en la clase CGameObject, de forma tal que nos sirvan para todos los objetos del juego.

En la clase principal (main.py), creamos las tres naves como siempre, las colocamos en la posición inicial y luego le damos la velocidad utilizando funciones que vamos a escribir en CGameObject:


# Crear las naves: se le pasa como parámetro la imagen de la nave.
n1 = CNave(CNave.TYPE_PLATINUM)
n2 = CNave(CNave.TYPE_GOLD)
n3 = CNave(CNave.TYPE_RED)

# Colocar las naves en su posición inicial.
n1.setXY(0, 100)
n2.setXY(0, 150)
n3.setXY(0, 200)

# Establecer la velocidad de las naves.
n1.setVelX(4)
n1.setVelY(0)
n2.setVelX(2)
n2.setVelY(0)
n3.setVelX(1)
n3.setVelY(0)

Como puedes ver, hemos agregado una nueva nave enemiga (una roja). Para eso en la clase CNave agregamos una constante más (CNave.TYPE_RED), y hemos puesto una imagen de la nave roja en la carpeta de imágenes dentro de la carpeta de assets (cópiala a tu propio proyecto y agrega la carga de la imágen según el tipo).


Lo que hace el código principal es simplemente asignar una velocidad a cada nave. Establecemos la velocidad asignando los componentes x e y de la velocidad. Por ejemplo, la primera nave se mueve de a 4 píxeles por frame en la horizontal, y no se mueve en la vertical.

La idea detrás de esto es que una vez que le damos velocidad a un objeto, el objeto continuará moviéndose hasta que le digamos que se detenga o le digamos de hacer otra cosa.


En el game loop, se llama a la función update() de las naves. Hemos sacado los parámetros de incremento (aIncX, aIncY) que había en el ejemplo anterior. Ahora update() no lleva parámetros y simplemente suma la velocidad del objeto a su posición, como vimos antes. En el game loop, estas líneas mueven las naves:


# Lógica de las naves.
n1.update()
n2.update()
n3.update()

En la clase CGameObject, agregamos dos atributos para llevar la velocidad del objeto: mVelX será la velocidad en la horizontal, y mVelY será la velocidad en la vertical:


class CGameObject(object):

   def __init__(self):

       # Coordenadas del objeto.
       self.mX = 0
       self.mY = 0

       # Velocidad.
       self.mVelX = 0
       self.mVelY = 0

	 ...

La función update(), ahora suma la velocidad del objeto a su posición, como vimos antes:


# Update mueve el objecto según su velocidad.
def update(self):

   # Mover el objeto.
   self.mX += self.mVelX
   self.mY += self.mVelY

   ...

Como hemos modificado la función update() de CGameObject, sacándole los parámetros que tenía antes, debemos de modificar también las funciones update() de las clases CNave y CCuadrado. Las funciones quedan así:


# Mover el objeto.
def update(self):

   # Invocar a update() de la clase base para el movimiento.
   CGameObject.update(self)

Por último, en CGameObject agregamos las funciones que utilizamos en main.py para establecer los componentes x e y de la velocidad:


# Establece la velocidad X del objeto.
def setVelX(self, aVelX):

   self.mVelX = aVelX

# Establece la velocidad Y del objeto.
def setVelY(self, aVelY):

   self.mVelY = aVelY

En general, para cada variable que agreguemos en una clase, haremos sus funciones correspondientes para establecer los valores y recuperarlos. A estas funciones se les llama setters (para establecer valores) y getters (para obtener valores). Casi todas las clases tendrán estas funciones. Recordemos que en inglés “set significa establecer y “get significa obtener.


Al correr el ejemplo vemos que la nave de arriba (la primera) se mueve a 4 píxeles por frame, la segunda a 2 píxeles por frame y la de abajo (la tercera) a 1 pixel por frame.



Figura 6-3: Las tres naves moviéndose a diferente velocidad..



¿Qué Velocidad debe tener un Objeto en el Juego?


Al decidir qué velocidad tiene que tener un objeto, en lugar de ponerle una velocidad a los objetos y estar probando a ver si queda bien mediante prueba y error, lo mejor es calcular realmente la velocidad según lo que se desea hacer.


Como pusimos el game loop a 60 frames por segundo y la ventana tiene 640 píxeles de ancho, la nave que va a dos píxeles por frame recorrerá la pantalla entera en 640 px / 2 px/frame = 320 frames. Como en un segundo se ejecutan 60 ciclos del game loop, 320/60 nos da 5,33 segundos. La nave que va a dos píxeles por segundo demora 5,33 segundos en atravesar la pantalla. Esto se da si realmente el game loop corre a 60 frames por segundo, porque si la computadora es lenta puede ser que en un segundo no se lleguen a hacer 60 frames.


Nota: En los primeros juegos que hagamos, las velocidades de los objetos dependen del framerate. En futuros juegos, haremos que la física de los objetos corra siempre igual, independiente del framerate.


Al establecer la velocidad de una nave, por ejemplo, si quisiéramos que la nave recorra la pantalla en 10 segundos, ¿qué velocidad debería tener?


Los datos que tenemos dados son:

Frame rate del juego: 60 frames por segundo

Ancho de la pantalla (distancia a recorrer): 640 píxeles


Entonces tenemos que:

velocidad (píxeles / frame) = distancia (píxeles) / tiempo (frames)

velocidad = 640 pixels / 6 segundos

1 segundo = 60 frames


Entonces:

velocidad = 640 píxeles / 60 * 6 frames = 640 píxeles / 360 frames = 1,77 píxeles / frame

De esta forma, si a un objeto le ponemos 1,77 píxeles por frame de velocidad en la x, recorrerá la pantalla (640 píxeles) en 6 segundos (360 frames).


Movimiento en Diagonal


Si queremos mover un objeto en diagonal, lo que debemos hacer es moverlo en la horizontal y también en la vertical.

Abre el ejemplo de la carpeta: capitulo_06\002_movimiento_diagonal. En este ejemplo, en main.py, modificamos la velocidad de las naves para que se muevan en diagonal:


# Establecer la velocidad de las naves.
n1.setVelX(2)
n1.setVelY(2)
n2.setVelX(4)
n2.setVelY(-2)
n3.setVelX(1)
n3.setVelY(1)

La primera nave, por ejemplo, tendrá una velocidad horizontal de dos píxeles por frame y una velocidad vertical de dos píxeles por frame. La figura 6-4 muestra este movimiento.



Figura 6-4: Movimiento de la nave en diagonal.



En este caso, la nave no se mueve con rapidez 2, sino que la rapidez es mayor (usando Pitágoras podemos calcular que la rapidez, en este caso, es la raíz cuadrada de 8, o sea aproximadamente 2,83). Esto implica que esta nave se mueve más rápido en la diagonal que en la horizontal, lo cual no es bueno para algunos juegos, por ejemplo, en los juegos RPG en los cuales movemos un personaje con las teclas de dirección, donde no queremos que el personaje camine más rápido en diagonal. Hay varios juegos donde esto pasa.


Más adelante cuando trabajemos con vectores, veremos como solucionar este problema que es común a muchos juegos cuando éstos son desarrollados por principiantes.



Aceleración


La aceleración produce un cambio de velocidad. Cuando un objeto cambia su velocidad es porque ha acelerado. Al igual que la velocidad, la aceleración es un vector, y como tal tiene una dirección y una magnitud.

La aceleración es un cambio de la velocidad en un cierto tiempo. Mira la figura 6-5.



Figura 6-5: El auto acelerando en un tiempo determinado.



En la figura 6-5 se muestra un auto acelerando. Un objeto tiene una velocidad inicial en un tiempo inicial y una velocidad final en un tiempo final. La aceleración es la diferencia del cambio de la velocidad en el tiempo transcurrido. La unidad de aceleración es distancia/tiempo².



Aceleración = (velocidad final - velocidad inicial) /

(tiempo final - tiempo inicial)



¿Cómo saber dónde va a estar un objeto en el futuro?


En algunos juegos podemos querer saber dónde va a estar un objeto en el futuro (por ejemplo, dentro de tres segundos) para poder disparar una bala a ese punto y pegarle justo cuando el objeto llegue ahí.

Si sabemos la posición actual del objeto y su velocidad, podemos calcular su posición en un tiempo en el futuro (por ejemplo en esos tres segundos). Supongamos también que el objeto está acelerando. Calculamos la posición futura del objeto con las siguiente fórmula:



Pos. futura = Pos. actual + Velocidad actual * tiempo + ½ * (Aceleración) * tiempo²



Como la velocidad y la aceleración son vectores, tenemos que resolver los vectores en sus componentes x e y para poder utilizarlos. Tendremos las variables mVelX y mVelY para representar el vector de velocidad, y las variables mAccelX y mAccelY para representar el vector de aceleración. Más adelante veremos en profundidad el manejo de vectores, haremos la clase CVector correspondiente y la utilizaremos, en lugar de tener dos variables para la velocidad y dos variables para la aceleración.

Entonces, por ahora, como aún no tenemos una clase para manejar un vector, usaremos los componentes x e y del vector, por separado:



x futura = x actual + velX * tiempo + ½ * (accelX) * tiempo²

y futura = y actual + velY * tiempo + ½ * (accelY) * tiempo²



Nota que cuando la aceleración es cero. Toda la multiplicación por cero, da cero, dejando a la ecuación como es cuando la velocidad es constante y no hay aceleración:



Pos. futura = Pos. actual + Velocidad * tiempo

(cuando no hay aceleración)



Simplificando las Ecuaciones en los Juegos


La ecuación que vimos anteriormente suma ½ * (accelX) * tiempo². Como en los juegos estamos corriendo la función update() en cada frame, el tiempo que transcurre entre un frame y otro es muy chico. Por ejemplo, si tenemos el juego a 60 frames por segundo, y un segundo son 1000 milisegundos, entonces el tiempo entre frame y frame es de 1000 / 60 = 16,66… milisegundos. Expresado en segundos, este tiempo es de 0,0166… segundos (1 segundo / 60 = 0,0166… segundos).


Si vemos la ecuación, estamos dividiendo por dos el tiempo al cuadrado, lo cual da 0.0013. Este valor es muy chico como para que la aceleración influya en el cambio de posición del objeto. Como estamos haciendo un videojuego 2D y no necesitamos una física realista, podemos ignorar completamente esta parte de la ecuación, lo cual hará las fórmulas mucho más sencillas y correrán más rápido para el juego.


Nota: Hay diferentes formas de escribir estas ecuaciones de movimiento (se denominan integraciones, y tenemos integración de Euler, Verlet, RK4, etc.). La que utilizaremos es la más sencilla (Euler) que sirve para la mayoría de los juegos 2D. Si necesitáramos una física más precisa, como por ejemplo para el cálculo de proyectiles, o para tener mayor precisión porque el juego lo pide, debemos usar otra.



¿Cómo saber qué velocidad tendrá un objeto en el futuro?


Si sabemos la aceleración del objeto y su actual velocidad, podemos proyectar la velocidad en esos segundos en el futuro (el tiempo).

La ecuación entonces, es:



Velocidad futura = Velocidad actual + Aceleración * tiempo



Por ahora usaremos los componentes x e y por separado:



velX futura = velX actual + accelX * tiempo

velY futura = velY actual + accelY * tiempo



Como el tiempo que transcurre entre una llamada a update() y otra es de un frame cuando estamos trabajando a una framerate fijo, la ecuación queda simplificada (tiempo = 1):



velX = velX + accelX

velY = velY + accelY



Este es el código que cambia la velocidad según la aceleración, y es el código que pondremos en la función update() de CGameObject para que todos los objetos del juego soporten aceleración.



Implementando la Aceleración


Veamos el ejemplo que se encuentra en la carpeta: capitulo_06\003_aceleracion.


Al inicio de la clase CGameObject, agregamos las dos variables para llevar control de la aceleración del objeto en la horizontal y en la vertical. Ambas variables son los componentes del vector aceleración.


class CGameObject(object):

   def __init__(self):

       # Coordenadas del objeto.
       self.mX = 0
       self.mY = 0

       # Velocidad.
       self.mVelX = 0
       self.mVelY = 0

       # Aceleración.
       self.mAccelX = 0
       self.mAccelY = 0

	  ...

Luego, escribimos dos funciones para asignar la aceleración del objeto como hacemos siempre cada vez que agregamos atributos en la clase (escribimos las funciones setters y getters).


# Establece la aceleración x del objeto.
def setAccelX(self, aAccelX):

   self.mAccelX = aAccelX

# Establece la aceleración y del objeto.
def setAccelY(self, aAccelY):

   self.mAccelY = aAccelY

Por último, en la función update() de CGameObject agregamos el código que cambia la velocidad del objeto según su aceleración.


# Update mueve el objecto según su velocidad.
def update(self):

   # Modificar la velocidad según la aceleración.
   self.mVelX += self.mAccelX
   self.mVelY += self.mAccelY
  
   # Mover el objeto.
   self.mX += self.mVelX
   self.mY += self.mVelY

   ...

Con esto, tenemos todo pronto para que un objeto en el juego tenga aceleración. En el programa principal (main.py), en la función init(), ponemos en cero las velocidades iniciales de las tres naves y le damos una aceleración diferente a cada una: 0.1 a la primera, 0.2 a la segunda y 0.3 a la tercera.


# Las naves comienzan detenidas.
n1.setVelX(0)
n1.setVelY(0)
n2.setVelX(0)
n2.setVelY(0)
n3.setVelX(0)
n3.setVelY(0)

# Acelerar las naves.
n1.setAccelX(0.1)
n1.setAccelY(0)
n2.setAccelX(0.2)
n2.setAccelY(0)
n3.setAccelX(0.3)
n3.setAccelY(0)

Al correr el ejemplo vemos que las naves aceleran cada vez más y luego llega un momento en que no se ven correctamente en la pantalla. Más adelante deberemos colocarle una velocidad máxima a los objetos para evitar problemas de este tipo.


Con esto ya tenemos la base para mover los objetos del juego. En el próximo capítulo veremos cómo agregar la nave del jugador y moverla con el teclado.



 
 
 

Comments


bottom of page