Comunidad orientada al desarrollo de videojuegos

Físicas en Wave Engine

OBJETIVOS

Vamos a ver una introducción a los conceptos básicos de física en Wave Engine. Para ello trabajaremos creando un suelo y una pared para interactuar con ella mediante física.

CREAR UN NUEVO PROYECTO

Vamos a crear un nuevo proyecto como siempre, con la plantilla de Wave Engine y lo llamamos PhysicWall:

screenwalk

CREAR LA CÁMARA

Lo primero que haremos será crear la cámara que necesitamos para ver el mundo. Para ello vamos a la clase MyScene.cs y añadimos los siguientes usings:

using WaveEngine.Common.Math;
using WaveEngine.Components.Cameras;

Creamos una cámara como esta:

FreeCamera camera = new FreeCamera("MainCamera", new Vector3(0, 10, 20), new Vector3(0, 5, 0));
EntityManager.Add(camera);

Estamos creando una cámara, añadiéndola al EntityManager, ya que la cámara es una entidad.

El behavior por defecto de FreeCamera nos permite movernos por el mundo usando las teclas a, w, s, d y si presionamos el botón izquierdo del ratón podremos mirar alrededor.

CREAR EL SUELO

Vamos a crear el suelo para nuestro mundo. Consiste en un cubo, con un Transform3D, en el ponemos una altura de 1. Pero primero necesitamos añadir estos usings en MyScene.cs:

using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Physics3D;
using WaveEngine.Components.Graphics3D;
using WaveEngine.Materials;

Ahora copiamos el siguiente código para crear el suelo al final del método CreateSecene():

Entity ground = new Entity("Ground")
     .AddComponent(new Transform3D() { Position = new Vector3(0, -1, 0), Scale = new Vector3(100, 1, 100) })
     .AddComponent(new BoxCollider())
     .AddComponent(Model.CreateCube())
     .AddComponent(new RigidBody3D() { IsKinematic = true })
     .AddComponent(new MaterialsMap(new BasicMaterial(Color.White)))
     .AddComponent(new ModelRenderer());
 
EntityManager.Add(ground);

El componente Transform3D indica que el suelo esta posicionado en el origen del mundo, en los ejes X y Z, pero en el punto –1 del eje Y. Y el componente Scale nos da el tamaño del suelo con 100 de ancho, 1 de alto y 100 de largo. Si no modificamos estas propiedades el componente Model.CreateCube() crea un cubo por defecto de un tamaño 1x1x1.

El componente BoxCollider() crea un algoritmo básico de colisión que se aplica a nuestro suelo, y RigidBody3D() proporciona las propiedades por defecto para el cálculo de la física, pero estamos modificando la propiedad por defecto IsKinematic a true.

El componente MaterialsMap pone la textura o lista de texturas que el suelo tenga. Por ahora, solo ponemos un color plano, el gris, con Color.Gray.

Ahora si iniciamos tendremos lo siguiente:

03ground

Ahora podemos movernos por el mundo con el componente por defecto de FreeCamera.

CREAR UN BLOQUE

Repetiremos el mismo proceso para crear un bloque como hicimos con el suelo, para ello usamos el siguiente código, lo ponemos al final del método CreateScene():

Entity brick = new Entity("brick")
    .AddComponent(new Transform3D() { Position = new Vector3(  0.5f,5f ,0), Scale = new Vector3(1f, 0.5f, 0.5f)})
    .AddComponent(new MaterialsMap(new BasicMaterial(Color.Red)))
    .AddComponent(Model.CreateCube())
    .AddComponent(new BoxCollider())
    .AddComponent(new RigidBody3D() { Mass = 100 })
    .AddComponent(new ModelRenderer());
 
EntityManager.Add(brick);

Si ejecutamos veremos caer el bloque al suelo:

04brick_thumb

CREAR LA PARED

Ahora vamos a crear la pared con el bloque, encapsulamos el código de la creación del bloque con esta nueva función:

private Entity CreateBox(string name, Vector3 position, Vector3 scale, float mass)
{
     Entity primitive = new Entity(name)
         .AddComponent(new Transform3D() { Position = position, Scale = scale })
         .AddComponent(new MaterialsMap(new BasicMaterial(Color.Red)))
         .AddComponent(Model.CreateCube())
         .AddComponent(new BoxCollider())
         .AddComponent(new RigidBody3D() { Mass = mass })
         .AddComponent(new ModelRenderer());
 
     return primitive;
}

Ahora reemplazamos el código del primer bloque que creamos por por dos bucles for que crearan la pared, ponemos el código donde estaba el bloque anterior:

int width = 10;
int height = 10;
float blockWidth = 2f;
float blockHeight = 1f;
float blockLength = 1f;
 
int n = 0;
for (int i = 0; i < width; i++)
{
     for (int j = 0; j < height; j++)
     {
         n++;
         var toAdd = CreateBox("box" + n, new Vector3(i * blockWidth + .5f * blockWidth * (j % 2) - width * blockWidth * .5f,
                                                      blockHeight * .5f + j * (blockHeight),
                                                      0),
                                                      new Vector3(blockWidth, blockHeight, blockLength), 10);
         EntityManager.Add(toAdd);
     }
}

Para no perdernos, el código de MyScene.cs sería el siguiente:

using System;
using System.Collections.Generic;
using WaveEngine.Common.Graphics;
using WaveEngine.Common.Math;
using WaveEngine.Components;
using WaveEngine.Components.Graphics3D;
using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;
using WaveEngine.Framework.Managers;
using WaveEngine.Framework.Physics3D;
using WaveEngine.Framework.Services;
using WaveEngine.Materials;
 
namespace PhysicWallProject
{
    public class MyScene : Scene
    {
        protected override void CreateScene()
        {
            RenderManager.BackgroundColor = Color.CornflowerBlue;
 
            //Adds the camera
            FreeCamera camera = new FreeCamera("MainCamera", new Vector3(0, 10, 20), new Vector3(0, 5, 0));
            EntityManager.Add(camera);
            RenderManager.SetActiveCamera(camera.Entity);
 
            //Adds the world ground
            Entity ground = new Entity("Ground")
                .AddComponent(new Transform3D() { Position = new Vector3(0, -1, 0), Scale = new Vector3(100, 1, 100) })
                .AddComponent(new BoxCollider())
                .AddComponent(Model.CreateCube())
                .AddComponent(new RigidBody3D() { IsKinematic = true })
                .AddComponent(new MaterialsMap(new BasicMaterial(Color.White)))
                .AddComponent(new ModelRenderer());
 
            EntityManager.Add(ground);
 
            //Creates the wall
            int width = 10;
            int height = 10;
            float blockWidth = 2f;
            float blockHeight = 1f;
            float blockLength = 1f;
 
            int n = 0;
            for (int i = 0; i < width; i++)
            {
                for (int j = 0; j < height; j++)
                {
                    n++;
                    var toAdd = CreateBox("box" + n, new Vector3(i * blockWidth + .5f * blockWidth * (j % 2) - width * blockWidth * .5f,
                                                                 blockHeight * .5f + j * (blockHeight),
                                                                 0),
                                                                 new Vector3(blockWidth, blockHeight, blockLength), 100);
                    EntityManager.Add(toAdd);
                }
            }
        }
 
        private Entity CreateBox(string name, Vector3 position, Vector3 scale, float mass)
        {
            Entity primitive = new Entity(name)
                .AddComponent(new Transform3D() { Position = position, Scale = scale })
                .AddComponent(new MaterialsMap(new BasicMaterial(Color.Red)))
                .AddComponent(Model.CreateCube())
                .AddComponent(new BoxCollider())
                .AddComponent(new RigidBody3D() { Mass = mass })
                .AddComponent(new ModelRenderer());
 
            return primitive;
        }
    }
}

Si ejecutamos veremos lo siguiente:

05wall_thumb

Para hacerlo más divertido y vistoso, vamos a crear un nuevo método que nos devuelva un color aleatorio:

private Color GetRandomColor()
{
     var random = WaveServices.Random;
     return new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), 1f);
}

Y en el método CreateBox() cambiamos la línea del material:

.AddComponent(new MaterialsMap(new BasicMaterial(Color.Red)))

Por esta otra:

.AddComponent(new MaterialsMap(new BasicMaterial(GetRandomColor())))

Y si ejecutamos ahora, se verá una pared más llamativa y divertida:

06funnywall_thumb

CREAR LA BOLA

Vamos a crear una bola para que cada vez que pulsemos el botón del ratón o toquemos la pantalla salga la bola disparada y podamos interactuar con la pared.

Necesitamos crear un componente nuevo, un Behavior. Un Behavior es un componente que tiene un método update. Un componente puede ser añadido a una entidad, y por lo tanto proporcionar un método Update para modificar la entidad en cada frame. Para ello necesitamos crear una nueva clase en nuestro proyecto y la llamaremos FireBehavior().

Añadimos los usings necesarios:

using WaveEngine.Framework;
using WaveEngine.Framework.Graphics;

Hacemos que FireBehavior herede de Behavior:

public class FireBehavior:Behavior

Ahora añadimos estas variables de clase:

private bool pressed;
Entity sphere;
 
[RequiredComponent]
public Camera Camera;

La variable pressed será utilizada para comprobar si el botón izquierdo del ratón esta pulsado o la pantalla es tocada. La entidad sphere es la bola que será lanzada. Camera es el componente asociado a este behavior y con [RequiredComponent] indicamos que es es un componente necesario. Si no hay una cámara asociada a este behavior, saltará una excepción.

Este es el constructos por defecto:

public FireBehavior()
     : base("FireBehavior")
{
     Camera = null;
}

El constructor por defecto solo llama al constructor base con un string que representa el nombre del behavior, e inicializa la variable Camera.

Necesitamos añadir los siguientes usings:

using WaveEngine.Framework.Services;
using WaveEngine.Framework.Graphics;
using WaveEngine.Common.Math;
using WaveEngine.Materials;
using WaveEngine.Common.Graphics3D;
using WaveEngine.Common.Graphics;
using WaveEngine.Framework.Physics3D;

Vamos a añadir la lógica del método Update sobrescribiéndolo de esta manera:

protected override void Update(TimeSpan gameTime)
{
     var touches = WaveServices.Input.TouchPanelState;
 
     if (touches.Count > 0)
     {
         if (!pressed)
         {
             pressed = true;
             if (sphere == null)
             {
                 sphere = new Entity("ball")
                    .AddComponent(new Transform3D() { Scale = new Vector3(2) })
                    .AddComponent(new MaterialsMap(new BasicMaterial(Color.Gray)))
                    .AddComponent(Model.CreateSphere())
                    .AddComponent(new SphereCollider())
                    .AddComponent(new RigidBody3D() { Mass = 3, EnableContinuousContact = true })
                    .AddComponent(new ModelRenderer());
                 EntityManager.Add(sphere);
             }
 
             RigidBody3D rigidBody = sphere.FindComponent<RigidBody3D>();
             rigidBody.ResetPosition(Camera.Position);
             var direction = Camera.LookAt - Camera.Position;
             rigidBody.ApplyLinearImpulse(500 * direction);
         }
     }
     else
     {
         pressed = false;
     }
}

Con este método Update estamos modificando la entidad asociada. Primero necesitamos tener acceso al TouchPanelState que representa el estado del panel táctil del dispositivo. WaveServices.Input da acceso a todos los inputs del dispositivo, como la brújula, el acelerómetro, etc. En este caso necesitamos acceder al estado del panel táctil.

Controlamos los taps de pantalla y los clicks del ratón, si son mayor que 0, se ha producido un click o tap en la pantalla. Con esto no iniciamos y lanzamos la bola cada vez que se ejecute el Update, si no que si la bola es nula creamos una de color gris, y tamaño 2. Usamos un SphereCollider y ponemos los parámetros del RigidBody3D para calcular la colisión.

Una vez la bola ha sido inicializada y el método Update detecta que es el momento de lanzar la bola, se posiciona la bola en la posición de la cámara y se aplica un impulso con ApplyLinearImpulse en el componente RigidBody de la esfera.

Con este código, tenemos  un behavior que lanza una bola cada vez que hacemos tap en la pantalla o hacemos click con el botón del ratón.

La última cosa que tenemos que hacer es asociar este behavior a nuestra cámara, vamos a MyScene.cs, localizamos la inicialización de la cámara y modificamos esta con el siguiente código:

//Adds the camera
FreeCamera camera = new FreeCamera("MainCamera", new Vector3(0, 10, 20), new Vector3(0, 5, 0));
Camera.Entity.AddComponent(new FireBehavior());
EntityManager.Add(camera);
RenderManager.SetActiveCamera(camera);

Ejecutamos y ya podemos interactuar con la pared lanzando la bola en su dirección:

07interactwall_thumb

AÑADIENDO TEXTURAS

Es hora de aplicar texturas a nuestros objetos. Puedes obtenerlas en la web, crearte las tuyas propias o usar las que proporcionamos en el proyecto.

Aprenderemos a usar Wave Editor, para transformar archivos .jpg y .X en .wpk:

08waveeditorjpg_thumb

Vamos a File/New en el menú del proyecto, se abrirá un diálogo para seleccionar la carpeta dónde los assets serán reemplazados y exportados.

Una vez creado el proyecto, hacemos click derecho en la carpeta Assets y seleccionamos Import Asset:

09importasset_thumb

abrimos los archivos que queramos exportar:

10importasset

Vamos al menú Project/Export para exportar los archivos a .wpk(Wave Package). Una vez el editor de Wave haga el trabajo, podemos encontrar los archivos exportados en la carpeta Export/Default dentro de la carpeta de tu proyecto de Wave Editor.

Ahora, añadimos los archivos a nuestro proyecto PhysicWall dentro de la carpeta Content:

12addedwpk_thumb

Necesitamos acceder a las propiedades de ambos archivos para cambiar la primera opción y seleccionar Contenido y en la opción de directorio de salida elegimos copiar si es posterior.

Para aplicar la textura y el modelo a nuestra escena, tenemos que modificar BasicMaterial del bloque en el método CreateBox():

.AddComponent(new MaterialsMap(new BasicMaterial(GetRandomColor())))

Obteniendo la textura brickTexture.wpk:

.AddComponent(new MaterialsMap(new BasicMaterial("Content/brickTexture.wpk")))

Finalmente modificamos esta línea:

.AddComponent(Model.CreateCube())

Obteniendo el modelo brick.wpk:

.AddComponent(new Model("Content/brick.wpk"))

El resultado final sería el siguiente:

13finalresult_thumb

DESCARGAR EL PROYECTO

Puedes descargar el proyecto completo aquí.

FUENTES

Tutorial original en inglés: http://blog.waveengine.net

Para descargar Wave Engine: http://www.waveengine.net

Traducido por Carlos Sánchez López

, , , , ,

One thought on “Físicas en Wave Engine

  • Renton says:

    Hola,

    Creé una entidad con un modelo y un boxcollider, pero la base de mi modelo no corresponde con la base del boxcollider.

    Como puedo arreglar esto?

    si mi modelo es un muñeco de nieve cual es el mejor collider para mi modelo?

Leave a Reply