[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? 😛

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *