En la mayoría de videojuegos en tercera persona, la dirección hacia la que se mueve el personaje depende de la orientación de la cámara. A pesar de que Sion Tower es un tower defense, controlamos a un personaje en tercera persona y, por tanto, debía incorporar este sistema. Estos días he estado exprimiendo los sesos para implementar dicho movimiento. ¡Por fin! Tras decenas de diagramas con vectores, ángulos y sistemas de coordenadas, lo he conseguido. En este artículo explico un sencillo algoritmo para conseguirlo.
¿Qué queremos conseguir?
El siguiente diagrama muestra el sistema de coordenadas de Ogre. Imaginad que nuestro personaje se encuentra en el origen p = (0, 0, 0) y que la cámara está en el c = (0, 0, 50) mirando hacia el origen. Nuestras teclas de movimiento son las clásicas: WASD. Al pulsar W el personaje debe moverse en la dirección negativa del eje Z, en cambio, si pulsamos D deberá hacerlo en la dirección positiva del eje X.
En cambio, si la cámara se situase en c = (50, 0, 0) mirando a (0, 0, 0) las teclas no tendrían el mismo efecto en términos absolutos, aunque se mantiene idéntico si tomamos como punto de referencia a la cámara. Si pulsamos W el personaje se desplazará en la dirección negativa del eje X. En el caso de que pulsemos D, el personaje se dirigirá en la dirección negativa del eje Z.
Por supuesto, el personaje deberá girarse para mirar hacia la dirección del desplazamiento. A continuación, detallamos la manera de implementar este sistema de movimiento.
Algoritmo: objetos implicados
Este pequeño algoritmo está orientado al motor de renderizado Ogre3D y la biblioteca de dispositivos de entrada OIS aunque su filosofía es válida para otras herramientas. Lo ideal sería utilizar clases para el personaje y otros elementos aunque, en esta ocasión, he preferido no hacerlo en favor de la simplicidad del ejemplo.
Partimos de los siguientes objetos:
Ogre::SceneNode* nodoPersonaje; // Nodo que representa al personaje
Ogre::Vector3 direccionObjetivo; // Relativa a la cámara
Ogre::Vector3 direccionActual; // Relativa a la cámara
Ogre::Vector3 velocidad; // Velocidad del personaje
bool andando; // true si el personaje se está desplazando
OIS::Keyboard* teclado; // Teclado para controlar al personaje
Algoritmo: desplazamiento del personaje
En cada frame debemos tomar la dirección de la cámara y quedarnos con su componente x y z ya que nos movemos en el plano. Esa será nuestra dirección "hacia delante". Es muy sencillo obtener la dirección "hacia la derecha", simplemente intercambiamos las componentes x y z cambiándole el signo a la z de "hacia delante".
La dirección objetivo de este frame se puede calcular consultando las pulsaciones de las teclas y combinando los dos vectores anteriores utilizando sumas o restas. Nos podremos mover en 8 direcciones distintas. Por ejemplo, si deseamos dirigirnos hacia atrás a la izquierda tendremos: direccionObjetivo = -delante – derecha. Finalmente, la dirección objetivo debe ser normalizada, es decir, que el módulo del vector sea igual a 1. Esto es muy importante si no queremos que la velocidad del personaje se vea alterada. Finalmente, aplicamos el desplazamiento teniendo en cuenta la velocidad, la dirección y el tiempo en milisegundos desde el último frame. Esta técnica se conoce como LERP (Linear Interpolation).
Algoritmo: orientación del personaje
Sólo nos queda corregir la orientación del personaje para que mire hacia la dirección en la que se desplaza. Ogre pone a nuestra disposición el método Vector3::getRotationTo que, dado un vector nos devuelve el cuaternio a aplicar de forma que tras la rotación quede alineado con el segundo. Los cuaternios se utilizan para representar rotaciones en el espacio y están compuestos por un ángulo y un eje (en este caso el y). Si multiplicamos la rotación necesaria por la que ya posee el nodo del personaje, éste mirará hacia donde deseamos. Es importante el orden de los operadores ya que el producto de cuaternios no es conmutativo.
Algoritmo: código completo
He aquí el código completo:
using namespace Ogre;
void EstadoJuego::actualizarPersonaje(Ogre::Real deltaT) {
// Tomamos la dirección de la cámara
Vector3 delante = camara->getDirection();
delante.y = 0;
// Vector relativo a la cámara, ortogonal hacia su derecha
Vector3 derecha(-delante.z, 0, delante.x);
// Calculamos la dirección objetivo en función de la pulsación de las
// teclas.
if (teclado->isKeyDown(OIS::KC_W) && teclado->isKeyDown(OIS::KC_D)) {
direccionObjetivo = delante + derecha;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_W) && teclado->isKeyDown(OIS::KC_A)) {
direccionObjetivo = delante - derecha;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_S) && teclado->isKeyDown(OIS::KC_D)) {
direccionObjetivo = -delante + derecha;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_S) && teclado->isKeyDown(OIS::KC_A)) {
direccionObjetivo = -delante - derecha;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_W)) {
direccionObjetivo = delante;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_S)) {
direccionObjetivo = -delante;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_D)) {
direccionObjetivo = derecha;
andando = true;
}
else if (teclado->isKeyDown(OIS::KC_A)){
direccionObjetivo = -derecha;
andando = true;
}
else
andando = false;
// Normalizamos el vector direccion
direccionObjetivo.normalise();
// Si debemos desplazarnos, aplicamos la traslación y calculamos la
// rotación a aplicar
if (andando) {
nodoPersonaje->translate(velocidad * direccionObjetivo * deltaT,
Node::TS_WORLD);
if (direccionObjetivo != direccionActual) {
Quaternion rotacion = direccionActual.getRotationTo(direccionObjetivo,
Vector3(0, 1, 0));
Quaternion rotacionActual = nodoPersonaje->getOrientation()
nodoPersonaje->setOrientation(orientacion * rotacionActual);
direccionActual = direccionObjetivo;
}
}
}
Posibles mejoras
Este algoritmo no está pensado para incluirse en un juego completo puesto que debe ser refinado. Sólo tenemos en cuenta la animación externa del personaje (desplazamiento y orientación). El algoritmo se olvida completamente de la animación interna. Si quieres un mejor resultado deberías incluirla (andar, detenerse, etc).
La rotación es brusca, lo ideal sería aplicarla poco a poco de forma que el personaje se girase suavemente. Para hacerlo deberíamos dividir la rotación en partes y aplicar una en cada frame.
Referencias
Seguramente quieras consultar fuentes mucho más fiables que yo:
- Game Engine Architecture (Jason Gregory): libro que cubre todos los aspectos a la hora de desarrollar un motor de juego. Contiene una sección de matemáticas muy bien explicada.
- LERP (Wikipedia)
- Quaternios (Confuted): si esto de los cuaternios te ha sonado a chino te recomiendo que consultes este artículo en el que se hace una pequeña introducción.
- Documentación oficial de Ogre: para obtener más detalles sobre las clases y los métodos empleados.