Anuncios

Bienvenidos sean a este post, hoy daremos el inicio de algo mas complejo.

Anuncios

Hasta este post vimos como mostrar imagenes y como desplazarlas por nuestra ventana pero a partir de hoy veremos como poder manejarlas.

Anuncios

Ante todo debemos entender un par de conceptos antes de comenzar, el primero es que los movimientos se realizan en 2D por medio de los ejes cartesianos X e Y. Para hacer una equivalencia imaginemos que cada pixel en nuestra ventana representa un punto de dicho eje cartesiano y dependiendo de los valores de X e Y sera el pixel que usaremos ya sea para indicar desde donde comienza a ubicarse nuestro sprite (dibujo) o hasta donde se desplazara, para hacer esto debemos usar un vector de 2D pero que es un vector?

Un vector puede ser descripto como una entidad con una direccion y una magnitud

Shaun Mitchell
Anuncios

Esto lo usaremos para representar aspectos de los objetos de nuestro juego, como la velocidad y aceleracion, y permitiendo la creacion del movimiento. Vamos a tomar como ejemplo a velocidad, y para poder representar de manera apropiada la velocidad de nuestro objetos necesitaremos la direccion hacia donde se dirige y tambien la cantidad (o magnitud), por la cual esta apuntando en esa direccion pero antes vamos a definir algunos puntos sobre vectores.

Anuncios

El primer tema que vamos a definir es la representacion de un vector:

v(x, y)
Anuncios

Con esto establecido podemos representar la longitud de un vector de la siguiente forma:

longitud_de_vector(x,y)=√(x² + y²)
Anuncios

Vamos a tomar como ejemplo un vector de la siguiente forma:

v1(3,-2)
Anuncios

Si lo aplicamos a la formula de calculo de longitud, nos quedara de la siguiente manera:

longitud_de_vector(3,-2)=√(3² + (-2)²)
Anuncios

Con los valores de los ejes X e Y del vector podemos ubicar a nuestro objeto en un entorno de 2D. Con esto podemos usar operaciones comunes de vector para mover a nuestros objetos pero antes de sumergirnos en todo esto vamos a crear una clase de vector dentro de nuestro proyecto Juego. Sino lo poseen les dejo un link para descargarlo:

Proyecto Juego

Anuncios

Descarguen el archivo y extraigan el contenido en el PC. Lo unico que deben hacer es revincular a SDL y SDL_Image. Para ello, les recomiendo este post donde comento como hacerlo para SDL y este otro post donde hago lo mismo para SDL_Image. Una vez que este todo funcionando correctamente, podemos continuar. Nuestro primer paso sera agregar una nueva clase y la llamaremos Vector2D, una vez creada agregaremos el siguiente codigo dentro del archivo de encabezado:

Vector2D.h

#pragma once
#ifndef __Vector2D__
#define __Vector2D__
#include <math.h>

class Vector2D
{
public:
	Vector2D(float, float);
	float getX();
	float getY();
	void setX(float);
	void setY(float);
	float longitud();

private:
	float m_x;
	float m_y;
};

#endif // !__Vector2D__
Anuncios

Como usamos y usaremos en todas nuestras clases, aplicaremos al inicio la condicion para evitar duplicaciones innecesarias, incluimos una libreria para realizar operaciones matematicas (math.h). Esta clase tendra las dos variables miembro para el eje X y el eje Y (m_x y m_y), luego tendremos úna serie de prototipos. Estos son un constructor para iniciarlos con dos valores que podemos recibir, y tenemos las funciones getter and setter para obtener y establecer los valores de m_x y m_y respectivamente, asi como tambien una para calcular la longitud. Nuestro siguiente paso sera ir a Vector2D.cpp y agregaremos el siguiente codigo:

Vector2D.cpp

#include "Vector2D.h"

Vector2D::Vector2D(float x, float y): m_x(x), m_y(y){}
float Vector2D::getX() { return m_x; }
float Vector2D::getY() { return m_y; }
void Vector2D::setX(float x) { m_x = x; }
void Vector2D::setY(float y) { m_y = y; }

float Vector2D::longitud() {
	return (float)sqrt((m_x * m_x) + (m_y * m_y));
}
Anuncios
Anuncios

Tal como dijimos anteriormente, el constructor sera para recibir dos valores y asignarlos a las variables de la parte privada. Asi como las otras funciones sirven tanto para obtener los valores como para asignar nuevos a las variables respectivamente. La siguiente puede ser mas compleja porque esta formula es para lo que comentamos al inicio. Debemos aclarar que el valor a devolver es de tipo float porque puede que el lenguaje entienda que devolvemos un valor de tipo double y no float. Lo siguiente es la funcion sqrt, la cual es la encargada de calcular la raiz cuadrada, y luego multiplicamos dos veces el valor de m_x y m_y para realizar la suma de los cuadrados de cada uno de estos valores, C++ y casi ningun lenguaje posee una funcion para esto. Permitiendo obtener el valor de la longitud del vector informado. Hasta ahora tenemos una clase base con una funcion que calcula la longitud del vector pero para nuestro siguiente paso haremos sobrecarga en los operadores para poder hacer calculos con nuestros vectores. Para ello, iremos a nuestro archivo de encabezado y lo modificaremos de la siguiente manera:

Vector2D.h

#pragma once
#ifndef __Vector2D__
#define __Vector2D__
#include <math.h>
#include <iostream>

class Vector2D
{
public:
	Vector2D(float, float);
	float getX();
	float getY();
	void setX(float);
	void setY(float);
	float longitud();
	Vector2D operator+ (const Vector2D&) const;
	friend Vector2D& operator+= (Vector2D&, const Vector2D&);
	Vector2D operator- (const Vector2D&) const;
	friend Vector2D& operator-= (Vector2D&, const Vector2D&);
	Vector2D operator* (float);
	Vector2D& operator*= (float);
	Vector2D operator/ (float);
	Vector2D& operator/= (float);


private:
	float m_x;
	float m_y;
};

#endif // !__Vector2D__
Anuncios

Nuestro cambio es basicamente agregar los prototipos de sobrecarga para distintos tipos de operadores. Estas sobrecargas van a ser para las operaciones arimeticas basicas, asi como tambien las que tiene con el igual para ese tipo de operacion. Como dijimos, son prototipos de estas. Pasemos a Vector2D.cpp y pasemos a definirlas. Para ello, agregaremos al codigo existente el siguiente segmento de codigo:

Vector2D Vector2D::operator+ (const Vector2D& v2) const {
	return Vector2D(m_x + v2.m_x, m_y + v2.m_y);
}
Vector2D& operator+= (Vector2D& v1, const Vector2D& v2) {
	v1.m_x += v2.m_x;
	v1.m_y += v2.m_y;
	return v1;
}
Vector2D Vector2D::operator- (const Vector2D& v2) const {
	return Vector2D(m_x - v2.m_x, m_y - v2.m_y);
}
Vector2D& operator-= (Vector2D& v1, const Vector2D& v2) {
	v1.m_x -= v2.m_x;
	v1.m_y -= v2.m_y;
	return v1;
}
Vector2D Vector2D::operator* (float escala) {
	return Vector2D(m_x * escala, m_y * escala);
}
Vector2D& Vector2D::operator*= (float escala) {
	m_x *= escala;
	m_y *= escala;
	return *this;
}
Vector2D Vector2D::operator/ (float escala) {
	return Vector2D(m_x / escala, m_y / escala);
}
Vector2D& Vector2D::operator/= (float escala) {
	m_x /= escala;
	m_y /= escala;
	return *this;
}
Anuncios

Como pueden ver algunas son muy parecidas, pero sobre esto ya hablaremos con mas detalle. Analicemos las sobrecargas del operador de adicion:

Vector2D Vector2D::operator+ (const Vector2D& v2) const {
	return Vector2D(m_x + v2.m_x, m_y + v2.m_y);
}
Vector2D& operator+= (Vector2D& v1, const Vector2D& v2) {
	v1.m_x += v2.m_x;
	v1.m_y += v2.m_y;
	return v1;
}
Anuncios
Anuncios

La primer sobrecarga del signo de mas (+), se encarga de recibir un valor de tipo Vector2D, y en el bloque devolveremos un nuevo objeto de tipo Vector2D donde a los valores actuales de m_x y m_y le sumaremos los valores de m_x y m_y del objeto informado (v2). La siguiente funcion hace la sobrecarga del operador += pero como usamos a friend, no es necesario informar de cual clase es y esto es para permitirnos el acceso a las partes privadas y protegidas de otra clase. Volvemos a usar el mismo tipo, a operator pero esta vez recibiremos dos valores, v1 y v2, y en el bloque le diremos que los datos de v1 sean incrementados con los de v2 y por ultimo devolvemos al objeto v1, con estas dos funciones podremos hacer las siguientes operaciones:

Vector2D v1(10,11);
Vector2D v2(35,25);
v1 += v2;
Vector2D v3 = v1 + v2;
Anuncios

En este ejemplo simple tenemos dos vectores creados (v1 y v2), a los cuales en la primera operacion incrementaremos a v1 mediante v2, utilizando la sobrecarga de +=, y para la segunda operacion creamos otro vector llamado v3 al cual le asignaremos el valor resultante de la suma de v1 y v2. Pasemos a la siguiente sobrecarga:

Vector2D Vector2D::operator- (const Vector2D& v2) const {
	return Vector2D(m_x - v2.m_x, m_y - v2.m_y);
}
Vector2D& operator-= (Vector2D& v1, const Vector2D& v2) {
	v1.m_x -= v2.m_x;
	v1.m_y -= v2.m_y;
	return v1;
}
Anuncios

Es tecnicamente igual al caso anterior pero esta vez en lugar de realizar la suma realiza la resta en ambos tipos de operadores pero la conducta es exactamente la misma, pasemos al siguiente operador que es de multiplicacion:

Vector2D Vector2D::operator* (float escala) {
	return Vector2D(m_x * escala, m_y * escala);
}
Vector2D& Vector2D::operator*= (float escala) {
	m_x *= escala;
	m_y *= escala;
	return *this;
}
Anuncios

En este caso volvemos a sobrecargar los dos operadores, en ambos casos no recibiremos un valor de tipo Vector2D sino uno tipo float para saber por cuanto debemos multiplicarlo (escala). En el primer caso, devolvemos un nuevo objeto de tipo Vector2D a los cuales le multiplicaremos los valores de m_x y m_y por el valor informado en escala. El siguiente operador multiplica al valor asignado a m_x y m_y del objeto por escala y devuelve el apuntador this de dicho objeto. Con esto comentado, pasamos a la ultima modificacion de operadores como es la division:

Vector2D Vector2D::operator/ (float escala) {
	return Vector2D(m_x / escala, m_y / escala);
}
Vector2D& Vector2D::operator/= (float escala) {
	m_x /= escala;
	m_y /= escala;
	return *this;
}
Anuncios

De vuelta tecnicamente es lo mismo que hicimos en la sobrecarga anterior pero esta vez con division. Mantenemos a escala para saber por cual dividimos y seguimos devolviendo el apuntador this en la segunda sobrecarga. Con esto ya tenemos la posibilidad de sumar entre objetos de tipo Vector2D, de restar entre ellos, de multiplicar un Vector2D por un numero y tambien de dividirlo por un numero. Solo nos resta agregar la normalizacion de un vector.

Anuncios

Esta es la ultima operacion que veremos y una de las importantes para trabajar con vectores, pero a que nos referimos cuando hablamos de normalizar? Normalizar un vector es cuando hacemos su longitud igual a 1, los vectores con una longitud (magnitud) de 1 son conocidos como unidades de vector y son muy utiles para representar una direccion, tal como la direccion de un objeto. Para ello, volveremos a Vector2D.h y en la parte publica agregaremos la siguiente declaracion:

void normalizar();
Anuncios

Luego iremos a Vector2D.cpp y vamos a agregar la siguiente definicion para el prototipo anterior:

void Vector2D::normalizar() {
	float l = longitud();
	if (l > 0) (*this) *= 1 / l;
}
Anuncios

Primero creamos una variable de tipo float que guardara el valor almacenado en longitud, recuerden que nos devuelve la operacion vista al inicio. Luego chequearemos que l sea mayor a 0, ni se les ocurra hacer una division por cero puede desestabilizar el universo tal y como lo conocemos 😁, y en todo caso si se cumple la condicion tomaremos al apuntador this y al valor lo multiplicaremos por el inverso de l (longitud). Recuerden que el inverso se calcula dividiendo 1 por el valor informado, con esto ya tenemos todas las operaciones basicas para comenzar a trabajar con vectores.

Anuncios

Para ello, debemos abrir SDLObjetoJuego.h para hacer nuestras modificaciones. La primera modificacion sera incluir a la nueva libreria:

#include "Vector2D.h"
Anuncios

El siguiente paso sera comentar o eliminar de la parte privada de la clase SDLObjetoJuego las variables m_x y m_y para reemplazarlas por la siguiente:

Vector2D m_posicion;
Anuncios

En este caso, tendremos un objeto que podra contener a los dos ejes que teniamos antes. La siguiente modificacion sera en el constructor que hay en SDLObjetoJuego.cpp, veamos como es el constructor actual:

SDLObjetoJuego::SDLObjetoJuego(const CargadorParams* pParams): 
	ObjetoJuego(pParams) {
	m_x = pParams->getX();
	m_y = pParams->getY();
	m_ancho = pParams->getAncho();
	m_alto = pParams->getAlto();
	m_idTextura = pParams->getIdTextura();
	m_frameActual = 1;
	m_filaActual = 1;
}
Anuncios

Lo reemplazaremos por el siguiente constructor:

SDLObjetoJuego::SDLObjetoJuego(const CargadorParams* pParams): 
	ObjetoJuego(pParams), 
	m_posicion((float)pParams->getX(), (float)pParams->getY()) {
	m_ancho = pParams->getAncho();
	m_alto = pParams->getAlto();
	m_idTextura = pParams->getIdTextura();
	m_frameActual = 1;
	m_filaActual = 1;
}
Anuncios

Lo nuevo que agregamos es la definicion o inicio de m_posicion donde le pasamos los valores de X e Y del objeto de tipo ObjetoJuego llamado pParams. Para esta ocasion debimos «castear», porque el valor obtenido (sera de tipo int) lo convierta a float que es el verdadero tipo que espera recibir m_posicion en sus parametros. Lo siguiente que hicimos fue eliminar las dos definiciones de m_x y m_y porque ahora no son necesarias, primero porque no existen y segundo porque lo hicimos al iniciar m_posicion. Nuestra siguiente modificacion sera en el metodo dibujar de SDLObjetoJuego, veamos como es actualmente:

void SDLObjetoJuego::dibujar() {
	ManejarTexturas::instanciar()->dibujar_frame(m_idTextura,
		m_x, m_y, m_ancho, m_alto, m_filaActual, m_frameActual,
		Eljuego::instanciar()->getRenderer());
}
Anuncios

La reemplazaremos por la siguiente funcion:

void SDLObjetoJuego::dibujar() {
	ManejarTexturas::instanciar()->dibujar_frame(m_idTextura,
		m_posicion.getX(), m_posicion.getY(), m_ancho, m_alto, 
		m_filaActual, m_frameActual, Eljuego::instanciar()->getRenderer());
}
Anuncios

Solamente reemplazamos a m_x y m_y por los llamados de getX y getY de m_posicion respectivamente, el resto de la funcion trabaja de la misma manera como la explicamos anteriormente. Por ultimo, debemos modificar la funcion actualizar de los objetos. Primero veamos el caso de Enemigo, para ello debemos ir a la funcion actualizar en Enemigo.cpp:

void Enemigo::actualizar() {
	m_x += 1;
	m_frameActual = int(((SDL_GetTicks() / 100) % 6));
}
Anuncios

Y ahora la modificaremos con el siguiente codigo:

void Enemigo::actualizar() {
	m_posicion.setX(m_posicion.getX() + 1);
	m_posicion.setY(m_posicion.getY() + 1);
}
Anuncios

Esta funcion sera la encargada de cambiar la posicion de nuestro ejes X e Y del vector m_posicion, de manera temporal haremos que se incremente el valor de ambos ejes en 1 primero obteniendo el valor de cada eje por medio de su funcion getter correspondiente. Ahora debemos hacer lo mismo para el jugador. Para ello modificaremos a la funcion actualizar en Jugador.cpp de la siguiente manera:

void Jugador::actualizar() {
	m_posicion.setX(m_posicion.getX() - 1);
}
Anuncios

En este caso, decrementaremos la posicion del eje X en m_posicion en 1 de la misma forma que hicimos con la otra funcion actualizar. Con todo esto realizado, podemos dar por terminada las modificaciones basicas para que nuestro proyecto comience a trabajar con vectores. Probemos de compilarlo y ver que sucede mediante el siguiente video

Anuncios

Como se puede ver en el video ahora tenemos una clase que nos permite manipular los objetos de una manera mas simple, y esto es solo el comienzo.

Anuncios

En resumen, hoy hemos visto que es un vector, como es nuestro vector, tambien hemos visto el primer calculo que haremos sobre este, creamos la clase que se encargara de calcularlos, incluimos una nueva libreria, tambien hemos hecho la base de esta clase con sus respectivos metodos getter and setter, asi como tambien hemos hecho todo lo necesario para poder manipular nuestros objetos mediante estas nueva implementaciones. Espero les haya sido de utilidad y les dejo un link a GitHub donde estan los codigos creados hoy:

Movimientos y vectores / GitHub

Les dejo algunas de mis redes sociales para seguirme o recibir una notificacion cada vez que subo un nuevo post:

Anuncios

Donación

Es para mantenimento del sitio, gracias!

$1.50