[XNA] Comportamientos de navegación para agentes autónomos (steering behaviors)

Los comportamientos de navegación, aplicados a la programación de juegos, se refieren a la capacidad de los agentes de desplazarse de forma autónoma, es decir, que el comportamiento de los mismos no está previamente calculado, sino que es generado según las condiciones del entorno en cada momento, de forma espontánea. El código de estos ejemplos es relativamente sencillo, pero dado que no he encontrado ninguna implementación en XNA de este tipo de algoritmos, me ha parecido interesante compartirlos.

Pueden existir muchos comportamientos de este tipo (de hecho, podemos inventarnos los que queramos), pero hay algunos que pueden llegar a considerarse como más «estandarizados». Cuando digo estandarizados, me refiero a los comportamientos, no a su implementación: no existe una fórmula universal para codificarlos. Los comportamientos que veremos en esta primera parte son:

  • Búsqueda
  • Huida
  • Errar

Lo realmente interesante es que el código es muy sencillo, y se pueden conseguir resultados muy interesantes introduciendo variaciones del mismo en nuestras producciones.


La implementación

La codificación la he planteado de forma que existe una posible lógica de comportamientos autónomos, a la cual se le envía una estructura de tipo Agente, que contiene cierta información de referencia del mismo. Esta estructura es la siguiente:

public struct Agente
{
   public Vector2 posicion;
   public Vector2 velocidad;
   public float velocidadMaxima;
   public float angulo;
   public float rotacionMaxima;
   public Estado estado;
   public float distanciaMinima;
}

Conocer esta estructura va a ser útil a continuación, para entender el código que implementa cada uno de los comportamientos.


Búsqueda y huida

La búsqueda es el reverso de la huida. Así pues el código será prácticamente idéntico. Vamos a verlo:

        public void Seek(GameTime gameTime)
        {
            if (Vector2.Distance(agente.posicion, this.destino) > 10.0f)
            {
                agente.velocidad = destino – agente.posicion;
                agente.velocidad.Normalize();
                agente.velocidad *= agente.velocidadMaxima;

                agente.angulo = (float)System.Math.Atan2(agente.posicion.Y – this.destino.Y, agente.posicion.X – this.destino.X);

                Vector2 heading = new Vector2(
                    (float)Math.Abs(Math.Cos(agente.angulo)), (float)Math.Abs(Math.Sin(agente.angulo)));

                agente.posicion += heading * agente.velocidad;
            }           
        }

Este método Seek corresponde a la implementación de la búsqueda. Para implementar la huida, sólo sería necesario cambiar la forma de establecer la velocidad a:  agente.velocidad = destino – agente.posicion;
No tiene demasiado secreto, establecemos la velocidad a partir de la posición actual del agente y su destinación, y después calculamos el ángulo y la dirección sobre la cual debe rotar el agente (mirará siempre hacia el vector posición «destino»). 


Movimientos erráticos

Este método es un poco más complicado, y ha sido inspirado en el algorismo de Craig Reynolds, aunque quiero recalcar que no es una implementación explícita de su propuesta.

public void Wander(GameTime gameTime)
        {
            if (tiempo == 0.0f || ultimoUpdate >= tiempo)
            {
                tiempo = Randomizer.obtenirAleatori(500, 2000);
                int radioCirculo = 25;
                float anguloTMP = Randomizer.obtenirAleatori(-360, 360);

                float posicionXCirculo = agente.posicion.X + 50.0f + ((float)Math.Cos(anguloTMP) * radioCirculo);
                float posicionYCirculo = agente.posicion.Y + 50.0f + ((float)Math.Sin(anguloTMP) * radioCirculo);

                // Si el ángulo es negativo, invertimos la dirección
                if (anguloTMP < 0)
                {
                    posicionXCirculo *= -1;
                    posicionYCirculo *= -1;
                }

                this.destino = new Vector2(posicionXCirculo, posicionYCirculo);
                ultimoUpdate = 0.0f;
            }

            if (Vector2.Distance(agente.posicion, this.destino) > 10.0f)
            {
                agente.velocidad = destino – agente.posicion;
                agente.velocidad.Normalize();
                agente.velocidad *= agente.velocidadMaxima;

                agente.angulo = (float)System.Math.Atan2(agente.posicion.Y – this.destino.Y, agente.posicion.X – this.destino.X);

                Vector2 heading = new Vector2(
                    (float)Math.Abs(Math.Cos(agente.angulo)), (float)Math.Abs(Math.Sin(agente.angulo)));

                agente.posicion += heading * agente.velocidad;
            }

            ultimoUpdate += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
        }

Una de las cosas interesantes que pueden verse en el algoritmo es que este se parece mucho al de búsqueda, pero con un añadido: el establecimiento de un destino aleatorio. La dirección destino se calcula aleatoriamente en un círculo «imaginario», obteniendo uno de sus puntos por azar, lo cual se hace específicamente con estas líneas de código:

float posicionXCirculo = agente.posicion.X + 50.0f + ((float)Math.Cos(anguloTMP) * radioCirculo);
float posicionYCirculo = agente.posicion.Y + 50.0f + ((float)Math.Sin(anguloTMP) * radioCirculo);

Otra cosa «curiosa», es que sólo recalculamos la dirección cada cierto tiempo, para no generar cambios en la dirección demasiado bruscos o ireales dentro de un juego.

Resultado final

Este es el resultado final de la implementación del algoritmo:

[View:http://www.youtube.com/watch?v=GMhxUllEaLY:550:0]

El código está disponible, como siempre, y puede ser descargado en este enlace.

Algunas ideas

¿Os imagináis utilizar estos algoritmos combinados con los de flocking del artículo anterior? Con la unión ambos algoritmos puede resultar fácil hacer una patrulla (flocking) que siga a su comandante, que se mueve de forma errática (wandering).

 

[webcast] Noviembre de webcasts de XNA

En el UOC DotNetClub hemos organizado varios webcast para el noviembre que se avecina. Los webcast son de «dificultad ascendente», y partiendo de cero, de forma que son ideanles para  quien quiera aprender XNA. 

Regístrate en: http://uoc.dotnetclubs.com/eventos

Estos son por ahora los webcast confirmados:

  • Desarrollo de videojuegos para Windows y XBOX 360

 

 

[XNA] Flocking o movimientos colectivos

¿Quién no ha jugado alguna vez al Age of Empires? En ese juego, puedes seleccionar un grupo de unidades, y hacer que estas se muevan de forma colectiva hacia el destino que les indiques. Eso es lo que conocemos como técnicas de flocking, cuyo orígen viene de la naturaleza con los animales: bandas de pájaros, manadas, etc. La implementación de la técnica siempre será igual o muy parecida a la proporcionada en este ejemplo, por lo que casi podemos afirmar que estamos hablando de un «algoritmo de flocking». Esto podría considerarse que está dentro del ámbito de la Inteligencia Artificial en el mundo de los videojuegos (muy básica, eso sí).

En este artículo veremos una implementación básica de flocking. Implementacones más avanzadas podrían incluir, además de los movimientos colectivos, algoritmos de pathfinding, o programación comportamientos cuando un miembro del grupo se queda desperdigado (porque queda atascado en algún obstáculo por ejemplo).

La siguiente figura simplifica la idea del flocking:

Eso sí, como las cosas en la vida nunca son tan sencillas, deberemos tener en cuenta una serie de factores cuando implementemos algoritmos flocking. Son los siguientes:

  • Separación: Los miembros no pueden estar excesivamente cerca (no queremos que se amontonen).
  • Cohesión: Queremos que los miembros vayan lo suficientemente juntos y no se «pierda» ninguno por el camino.
  • Alineamiento: Todos los miembros del grupo deben ir alineados en la misma dirección.

Implementación

La implementación se basa en el desarrollo de dos clases: Escuadron y Soldado. Son bastante autodescriptivas. Un soldado es un miembro del grupo, con una dirección, posición, velocidad, ángulo de giro y dirección deseada. Además tiene un método «Caminar», que es el que hace que este se desplaze. La clase escuadrón es la que tiene más lógica. La veremos con mayor detalle.

La clase Escuadron contiene el destino final al que se dirige este, y distintos factores que se utilizan para calcular el desplazamiento (son los factores que he enumerado anteriormente). Además la clase contiene una lista de soldados, que se irán cargando con el método AddSoldado.


Update de la clase Escuadron

Primero estableceremos la dirección de los soldados, y posteriormente les aplicaremos los factores del algoritmo de flocking para que el movimiento sea acorde al de un grupo (y no se acaben superponiendo a medida que se acercan al destino).

// Establecemos la dirección de los soldados
foreach (Soldado soldado in soldados)
{
    Vector2 direccionDeseada = this.destinoFinal – soldado.posicion;

    direccionDeseada.Normalize();
    soldado.SetDireccion = direccionDeseada;
}

// Factores flocking
this.UpdateSeparation();
this.UpdateCohesion();
this.UpdateAlignment();

Después de aplicar los factores, los desplazaremos. Además (y sólo para hacerlo bonito), guardaremos el valor del ángulo existente entre su posición y el destino final (para que «caminen mirando» al objetivo).

foreach (Soldado soldado in soldados)
{
    // Vida sana hay que tener…
    soldado.Caminar();

    // Orientamos al miembro del grupo de forma que mire hacia el destino
    soldado.angulo = (float)System.Math.Atan2(soldado.posicion.Y – this.destinoFinal.Y, soldado.posicion.X – this.destinoFinal.X);
}

Aplicación de los factores: Separación

private void UpdateSeparation()
{
    foreach (Soldado soldado in soldados)
    {
          Vector2 direccionDeseada = soldado.direccionDeseada;
          Vector2 nuevaDireccion = Vector2.Zero;

          // Separamos cada uno de los soldados de los demás
          foreach (Soldado vecino in soldados)
          {
               if (vecino != soldado)
               {
                    Vector2 distancia = vecino.posicion – soldado.posicion;
                    float longitud = distancia.Length();
                    float factor = longitud * longitud;

                    if (factor > 0.0f)
                    {
                         nuevaDireccion -= distancia / factor;
                    }
                }
          }

          // Si hay que ajustar la dirección del soldado…
          if (nuevaDireccion.Length() > 0.0f)
          {
                nuevaDireccion.Normalize();

                direccionDeseada += nuevaDireccion * this.factorSeparacion;
                direccionDeseada.Normalize();
                soldado.SetDireccion = direccionDeseada;
          }
    }
}

Aplicación de los factores: Cohesión

        private void UpdateCohesion()
        {
            if (soldados.Count > 0)
            {
                // Localizamos el centro del grupo
                Vector2 centro = Vector2.Zero;

                foreach (Soldado soldado in soldados)
                {
                    centro += soldado.posicion;
                }

                centro = centro / soldados.Count;

                // Miramos la distancia hasta el centro y se suma un vector proporcional
                // a la dirección deseada
                foreach (Soldado soldado in soldados)
                {
                    Vector2 nuevaDireccion = centro – soldado.posicion;

                    if (nuevaDireccion.Length() > 0.0f)
                    {
                        nuevaDireccion.Normalize();
                        nuevaDireccion = nuevaDireccion * this.factorCohesion;
                        nuevaDireccion += soldado.direccionDeseada;
                        nuevaDireccion.Normalize();

                        soldado.SetDireccion = nuevaDireccion;
                    }
                }
            }       
        }

Aplicación de los factores: Alineamiento

        private void UpdateAlignment()
        {
            if (soldados.Count > 0)
            {
                // Calculamos la media aritmética de la dirección de los miembros
                Vector2 mediaDireccion = Vector2.Zero;

                foreach (Soldado soldado in soldados)
                {
                    mediaDireccion += soldado.direccionDeseada;
                }

                mediaDireccion = mediaDireccion / soldados.Count;

                // Añadimos a la dirección deseada el factor del promedio
                foreach (Soldado soldado in soldados)
                {
                    Vector2 nuevaDireccion = mediaDireccion – soldado.direccionDeseada;

                    if (nuevaDireccion.Length() > 0.5f)
                    {
                        nuevaDireccion.Normalize();
                        nuevaDireccion = nuevaDireccion * this.factorAlineamiento;
                        nuevaDireccion += soldado.direccionDeseada;
                        nuevaDireccion.Normalize();

                        soldado.SetDireccion = nuevaDireccion;
                    }
                }
            }
        }


Utilizando el escuadrón y los soldados

Primero inicializaremos un objeto Escuadron en la clase Game:

escuadron = new Escuadron(new Vector2(400, 400), 0.35f, 0.15f, 0.30f);

A continuación añadimos soldados al escuadrón:

escuadron.AddSoldado(new Soldado(new Vector2(10,50), new Vector2 (0.55f, 0.55f)));
escuadron.AddSoldado(new Soldado(new Vector2(50, 50), new Vector2(0.55f, 0.55f)));
escuadron.AddSoldado(new Soldado(new Vector2(25, 65), new Vector2(0.55f, 0.55f)));
escuadron.AddSoldado(new Soldado(new Vector2(5, 15), new Vector2(0.55f, 0.55f)));
escuadron.AddSoldado(new Soldado(new Vector2(55, 10), new Vector2(0.55f, 0.55f)));

Además, añadiremos un nuevo soldado al escuadrón cada 2 segundos para darle más vidilla al asunto:

if (tiempoDesdeUltimoUpdate >= 2000.0f)
{
     escuadron.AddSoldado(new Soldado(new Vector2(55, 10), new Vector2(0.55f, 0.55f)));
     tiempoDesdeUltimoUpdate = 0.0f;
}

tiempoDesdeUltimoUpdate += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

El resultado final

En el vídeo se puede ver el resultado. El algoritmo es mejorable, con la introducción por ejemplo de pathfinding, o hacer que los miembros del grupo sólo avancen si no están girando, hacer que los miembros del grupo no se amontonen en el último momento (cuando alcanzan el objetivo), etc.

[View:http://www.youtube.com/watch?v=OGz7jCYVv3E:550:0]

El código puede ser descargado en este enlace.

Otras implementaciones

Se me ha ocurrido buscar en youtube otras implementaciones de Flocking y… he visto un par de interesantes:

[View:http://www.youtube.com/watch?v=KyunEojjts8:550:0]

[View:http://www.youtube.com/watch?v=oTFx0uZc6Eo:550:0]

[XNA] Frustum culling en XNA

Frustum culling es una técnica que en programación gráfica en 3D consiste en «pintar» por pantalla sólo los objetos que están siendo enfocados por la cámara, ahorrando tiempo de procesador, y por lo tanto consiguiendo como resultado framerates más altos.Hacer que los juegos corran rápido es crítico, y más en una plataforma de código manejado como es XNA, donde ahí tiene alguna desventaja respecto otros frameworks de desarrollo gráficos basados en C++, así pues, la aplicación de técnicas como esta, nos pueden sacar de más de un apuro. Además, como va siendo habitual, comentar que esta técnica no es específica de XNA, sinó que se usa en todos los frameworks de desarrollo gráfico en 3D.

Lo que está en la cámara tiene forma de pirámide (en realidad, de «trozo de pirámide», ya que tenremos que considerar que el nearplane normalmente será mayor que 0). Si no aplicamos la técnica de frustum culling, el procesador intentará «pintar» todos los modelos de nuestro juego, desperdiciando inútilmente el procesador. ¿Cómo se aplica la técnica de frustum culling en XNA? La verdad es que es mucho más fácil de lo que puede parecer!

Para hacer esta implementación tengo preparado un código que carga un montón de modelos en pantalla y los mueve de un lado para otro aleatóriamente. Además, en el ejemplo veremos por pantalla el número de FPS, y podremos activar / desactivar el Frustum Culling (pulsando la tecla Q se activa, y volviéndola a pulsar se desactiva). Así podremos ver en tiempo real la diferencia entre un modo y el otro. En mi sistema, consigo hasta 30fps más activando el frustum culling… Esto variará mucho de la máquina de cada uno. Si tienes un maquinón con 4 procesadores, quizá necesites añadir más modelos a la escena para percibir la diferencia. Sólo para que os hagáis una idea:


Frustum = true => 62 fps


Frustum = false => 39 fps!!!

Como podéis ver… la diferencia es abismal. Así pues, demostrada la importancia de esta técnica, vamos a ver su más que sencilla implementación.

La idea es simple, XNA tiene una clase llamada BoundingFrustum, que nos permite detectar si hay colisión entre el frustum de la cámara y un boundingsphere. Así pues necesitaremos asignar un boundingsphere a cada uno de los modelos que queremos que se vean afectados por esta técnica. Esto no es un problema, ya que el boundingsphere es un objeto ligero y rápido como el rayo 🙂 

Una forma de crear automáticamente un boundingsphere para vuestros modelos es este código:

foreach (ModelMesh mesh in defaultModel.Meshes)
{
    BoundingSphere meshBoundingSphere = mesh.BoundingSphere;

    BoundingSphere.CreateMerged(ref defaultModelBoundingSphere,
                                ref meshBoundingSphere,
                                out defaultModelBoundingSphere);
}

Este código habría que llamarlo después de cargar el modelo, y guardar en algún lugar el BoundingSphere de este.

Ahora toca modificar la típica clase «cámara» que todos tenemos en nuestros proyectos 3D de XNA (no pegaré todo el código aquí, podéis descargarlo de este artículo si queréis). Considerando que tenemos una clase Camera, añadiremos el atributo siguiente:

 public BoundingFrustum boundingFrustum;

Otra cosa en la clase cámara… cada vez que modifiquemos su matriz View (eso será cada vez que movamos la cámara), habrá que llamar al código siguiente:

boundingFrustum = new BoundingFrustum(view * projection);

Y con eso hemos acabado con la clase cámara… ahora vamos a ver qué hacemos con este BoundingFrustum.

La forma «típica» de pintar modelos 3D consiste en recorrer los Meshes del modelo, y por cada uno de ellos, sus BasicEffect, ir estableciendo las matrices de World, Vista y Proyección, para finalmente pintar el mesh. Para aplicar el Frustum Culling pondremos un poco de código extra, que es este:

            foreach (ModelMesh mesh in model.Meshes)
            {
                if(this.bounding != null && camera.frustumEnabled)
                {
                    ContainmentType currentContainmentType = ContainmentType.Disjoint;

                    currentContainmentType = camera.boundingFrustum.Contains(this.bounding);

                    if (currentContainmentType == ContainmentType.Disjoint)
                        continue;
                }

                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World =
                        Matrix.CreateFromYawPitchRoll(
                        this.rotation.Y,
                        this.rotation.X,
                        this.rotation.Z) *
                        Matrix.CreateScale(this.scale) *
                        Matrix.CreateTranslation(this.position);

                    effect.Projection = camera.projection;
                    effect.View = camera.view;
                }
                mesh.Draw();
            }

Lo que hemos hecho es simple, básicamente nos aseguramos que el boundingsphere del mesh a pintar está dentro del frustum, en caso contrario, simplemente no lo pintamos, y pasamos al siguiente mesh. Finito!! Con una técnica así de fácil se pueden conseguir grandes resultados.

El código puede ser descargado en este enlace.

Finalmente, quiero agradecer a Tyler Gasve, por inspirarme y motivarme a escribir este artículo 🙂

 

[evento] Congreso de Desarrolladores de Videojuegos 2009

Noviembre emocionante! Ya está aquí el CDV 2009 de Valencia! El registro es gratuito, y las plazas son limitadas, así que no dudéis en registraros!

El CDV parte con la intención de aglutinar en un único espacio a todos los desarrolladores profesionales de videojuegos españoles, amateurs y academia con el fin de conocerse y compartir opiniones, presentar la situación del panorama español e internacional y promover el asociacionismo de la industria.

El evento tendrá lugar los días 5 al 7 de noviembre.

Más información en la web oficial del evento

[XNA] Simulación de un semáforo de tráfico

En este sencillo ejemplo se puede ver un tema interesante, y a mi parecer poco «documentado», y es a acceder a las distintas partes de un Modelo 3D desde XNA. La lógica del semáforo no es complicada, y ha sido pensada para que sea fácil añadir muchos semáforos en un mundo 3D.

La clase más importante, de cara a la lógica, es Semaforo, que es la que contiene toda la información relativa al estado actual «y futuro» de cada uno de los semáforos. Entremos en un poco más de detalle:


Campos, métodos y propiedades más importantes

  • ID: Relaciona el semáforo con el modelo 3D. En este caso lo he diseñado con 3DMax 2010.
  • Tempo: Contiene una lista de tempos del semáforo. Describe los estados (colores) y el tiempo que debe permanecer cada uno de ellos. Nota: El concepto tempo me lo acabo de inventar…
  • ultimoUpdate: Se utiliza en el método Update, para comprobar si ha transcurrido el tiempo necesario para pasar al siguiente estado del semáforo (lo que significaría un «cambio de color»).
  • currentIndex: Utilizado sólo para acceder de manera indexada a la lista de tempos

  •  Current: indica el tempo actual (lo que viene a ser el color, una vez renderizado).

El método AddTempo (sobrecargado), permite añadir tempos a la clase. Me resulta práctica esta sobrecarga, en la que le pasamos todos los tempos directamente.

public void AddTempo(TempoSemaforo[] _tempo)
{
     foreach (TempoSemaforo tempoActual in _tempo)
          Tempo.Add(tempoActual);
}

En este código podemos ver como se usaría este método, por ejemplo desde la clase Game:

TempoSemaforo[] temposSemaforo1 = { new TempoSemaforo(TempoSemaforo.Estado.Verde, TIEMPOVERDE), new TempoSemaforo(TempoSemaforo.Estado.Ambar, TIEMPOAMBAR),
                                                             new TempoSemaforo(TempoSemaforo.Estado.Rojo, TIEMPOROJO) };

TempoSemaforo[] temposSemaforo2 = { new TempoSemaforo(TempoSemaforo.Estado.Rojo, TIEMPOROJO), new TempoSemaforo(TempoSemaforo.Estado.Verde, TIEMPOVERDE),
                                                             new TempoSemaforo(TempoSemaforo.Estado.Ambar, TIEMPOAMBAR) };

this.grupoSemaforos[0].AddTempo(temposSemaforo1);
this.grupoSemaforos[1].AddTempo(temposSemaforo2);

Lo que estamos haciendo aquí es definir los distintos estados que tendrá el semáforo, y el tiempo que durará cada uno de ellos. Cuando el semáforo llega al último estado, vuelve a comenzar en un bucle «Infinito». Esto lo he dejado así para darle un poco de flexibilidad, ya que hay un montón de tipos de semáforos distintos aunque no lo parezca…

Como tema importante, queda destacar que en el método draw deberemos ejecutar este código por cada uno de los semáforos de la lista:

if (mesh.Name.Contains(semaforo.ID))
{
     effect.DiffuseColor = black;

     if (mesh.Name == semaforo.ID + «verd»&& semaforo.Current.estadoSemaforo == TempoSemaforo.Estado.Verde)
     {
          // Verde
          effect.DiffuseColor = new Vector3(0, 1, 0);
     }

     if (mesh.Name == semaforo.ID + «ambar» && semaforo.Current.estadoSemaforo == TempoSemaforo.Estado.Ambar)
     {
          // Verde
          effect.DiffuseColor = new Vector3(1, 0.5f, 0);
     }

     if (mesh.Name == semaforo.ID + «roig» && semaforo.Current.estadoSemaforo == TempoSemaforo.Estado.Rojo)
     {
          // Rojo
         effect.DiffuseColor = new Vector3(1, 0, 0);
     }
}

Resultado final

[View:http://www.youtube.com/watch?v=kJbf-4p-iuk:550:0]

El código se puede descargar en este enlace.

[Evento] ¿Cómo crear una empresa de videojuegos?

El ciclo de conferencias OPEN TALENT presenta el próximo jueves, 15
de octubre, una conferencia en la sala de actos que tendrá como
protagonista a Jordi Torras Noguera, director de Marketing y
Comunicación de la empresa U-Play Studios.

La conferencia, cuyo título es «Com crear una empresa de videojocs»?
(“¿Cómo crear una empresa de videojuegos?”), se realiza en el marco del
máster en Diseño y Creación de Videojuegos
y tiene como objetivos tanto proporcionar conocimientos a los alumnos
del máster para que puedan crear su propia empresa en el ámbito de los
videojuegos como ofrecerles las claves necesarias para que la gestionen
de manera eficiente.

El acto está abierto a todo el mundo que esté interesado en la temática.

Inscríbete en este enlace.

[XNA] Experimento: Simular las ruedas de un vehículo

Bah! Simular la rueda de un coche… eso está tirao… Pues eso mismo pensaba yo… hasta que me puse a escribir el código. Una rueda se ve afectada por la aceleración, cambio de velocidades, cambios de dirección… (y no nos metemos en la amortiguación o las colisiones con el terreno!).

En este artículo no seré muy tiquismiquis con los temas de física, porque no es el objetivo. Simplemente quiero emular el movimiento de una rueda, pero no quiero hacer un simulador de coches al estilo simax, más real que la propia realidad, pero sin gastar gasolina… (que no se si está hecho en XNA o DirectX por cierto). Eso es realmente complicado, y queda fuera del alcance del rato que he dedicado al ejemplo 🙂  Aun así, para los valientes, os dejo enlaces a algunos artículos que os pueden encaminar en el tema:

También diré que existen engines de física ya hechos para XNA, y que incluso uno de ellos, JigLibX, ya contiene el modelado completo de un coche. No obstante si usara ese código no aprendería nada por el camino…


DECLARACIÓN DE CONSTANTES

Utilizaremos algunas constantes para definir valores «fijos» del entorno:

float[] MAXSPEED = new float[5] {0.05f, 0.10f, 0.25f, 0.40f, 0.50f};
const float MAXROTATION = 1.0f;
const float MAXACCELERATION = 0.05f;
float[] ACCELERATIONPOWER = new float[5] { 0.0005f, 0.0010f, 0.0020f, 0.0030f, 0.0040f };
const float BREAKPOWER = 0.0050f;
const sbyte GEARS = 5;
const float FRICTION = 0.00015f;

Como curiosidad vemos aquí que la velocidad máxima y la aceleración son arrays… eso es así porque cada una de las cinco marchas de la caja de cambios tendrá valores distintos. Ahora definiremos algunas propiedades de la rueda (lo pongo todo en la clase Game, así que no hagáis esto «en casa»):

CarState carState = CarState.stopped;
float speed = 0.0f;
float acceleration = 0.0f;
sbyte gear = 0;

Por cierto, el enumerado CarState, como se puede imaginar, define el estado del «coche» en cada momento -ojo, que nosotros solo programamos una rueda xD-

public enum CarState
{
    stopped = 0,
    accelerating = 1,
    braking = 2,
    deadpoint = 3
}

Después cargaremos un modelo 3D, que va a ser nuestra rueda:

El código no va a ser demasiado complicado, así que detallaré a partir de ahora sólo un método, llamado a su vez por el método Update(). A este método le he llamado KeyBoardInput. Como ya imaginaréis, lo que hace es «actuar» en función de lo que el usuario introduzca por teclado. Lo que el usuario puede hacer es:

  • Acelerar
  • Frenar
  • Girar hacia la derecha o a la izquierda

Vamos, ¿que no es nada del otro mundo verdad? (por cierto, el cambio de marchas es «automático»).  Lo primero será establecer por defecto el estado «punto muerto» al vehículo, y controlar el giro a la derecha y a la izquierda. Observad que la dirección está limitada por la constante MAXROTATION (todavía no he visto un coche al que le puedas girar las ruedas 360 grados).

KeyboardState kbState = Keyboard.GetState();

this.carState = CarState.deadpoint;

if (kbState.IsKeyDown(Keys.Right) && this.ruedaDelantera1.rotation.Y >= -MAXROTATION)
    this.ruedaDelantera1.rotation.Y -= 0.05f;

if (kbState.IsKeyDown(Keys.Left) && this.ruedaDelantera1.rotation.Y <= MAXROTATION)
    this.ruedaDelantera1.rotation.Y += 0.05f;

Continuamos dentro del mismo método… y pasamos a controlar el acelerador:

// Acelerador
if (kbState.IsKeyDown(Keys.Up))
{
    if (this.gear == 0)
        this.gear = 1;

    this.carState = CarState.accelerating;

    if (this.speed <= MAXSPEED[gear-1])
    {
        if (this.acceleration + ACCELERATIONPOWER[gear – 1] <= MAXACCELERATION)
        {
             this.acceleration += ACCELERATIONPOWER[gear – 1];
        }

        this.speed += this.acceleration / ((float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f);
    }
    else
    {
        if (this.gear <= GEARS-1)
        {
             this.gear++;
             this.acceleration = 0.0f;
        }
    }     

En este método estamos insultando a Newton… pero esperemos que no le sepa mal. Si la velocidad no es la máxima que puede alcanzar la marcha, incrementaremos la velocidad en función de la aceleración (uniforme). En caso contrario, si no hemos alcanzado la marcha máxima (en este caso tenemos 5), la incrementamos, y ponemos la aceleración a 0 (si, al cambiar la marcha hay que soltar el acelerador, es lo que tiene).

Ahora le toca al freno

// Freno
if (kbState.IsKeyDown(Keys.Down) && this.speed > 0)
{
    if (this.carState == CarState.accelerating)
    {
        this.acceleration = 0.0f;
    }
    this.carState = CarState.braking;

    this.acceleration += BREAKPOWER;
    this.speed -= this.acceleration / ((float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f);

    if (this.speed <= 0.00005f)
    {
        this.speed = 0.0f;
        this.carState = CarState.stopped;
    }  

El método es parecido. Si estamos frenando, ponemos el estado actual del coche como tal, y frenamos con la fuerza definida en BREAKPOWER, y recalculamos la velocidad, restándole la aceleración dividida por el tiempo. Además´, cuando la velocidad es muy cercana a cero, la establecemos a 0 por aproximación, y establecemos el estado «detenido». A continuación vamos reduciendo las marchas progresivamente, si la velocidad disminuye por debajo del mínimo de cada una de las marchas del cambio (por eso decía que el cambio es «automático»).

if (gear > 0 && this.carState != CarState.accelerating)
    if (this.speed <= MAXSPEED[gear – 1])
    {
        if (this.gear > 0)
        {
             this.gear–;
             this.acceleration = 0.0f;
        }
    }

Queremos que si dejamos de acelerar la velocidad vaya disminuyendo, ¿no? Pues la reducimos lentamente a causa de la fricción:

if (this.speed – FRICTION >= 0)
    this.speed -= FRICTION;

Finalmente, la rueda girará en función de la velocidad:

 this.ruedaDelantera1.rotation.Z += this.speed;

Finalmente, ejecutamos, y este es el resultado:

El código del experimento puede ser descargado aquí.

¿Álguien se anima con un coche completo? 😛