Funciones esenciales para crear un juego.

2
13632

Funciones esenciales para crear un juego.

En tutoriales anteriores se ha mostrado como podíamos crear un
juego a partir de uno existente, en este tutorial intentaremos
facilitaros las cosas, para ello os enseñaremos algunas funciones de
utilidad (crear escenarios propios, cargar personaje propio y como dotarle
de movimiento, etc.).

Empezaremos por crear el espacio donde se producirán todas las
acciones del juego, para ello nos podemos crear una clase que sea derivada
de Skybox, en ella tendremos el constructor que lo usaremos para montar
las caras de nuestro Skybox y otra función que nos cárgara las
texturas.

public class MiSkyBox extends Skybox {

    public MiSkyBox() {
        super("universo", 100, 100, 100);
        Texture north =     cargaTextura("north.jpg");
        Texture south =     cargaTextura("south.jpg");
        Texture east =      cargaTextura("east.jpg");
        Texture west =      cargaTextura("west.jpg");
        Texture up =        cargaTextura("top.jpg");
        Texture down =      cargaTextura("bottom.jpg");

        setTexture(Skybox.Face.North, north);
        setTexture(Skybox.Face.West, west);
        setTexture(Skybox.Face.South, south);
        setTexture(Skybox.Face.East, east);
        setTexture(Skybox.Face.Up, up);
        setTexture(Skybox.Face.Down, down);
        Callable preload = new Callable() {

            public Object call() throws Exception {
                preloadTextures();
                return null;
            }
        };
        GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).enqueue(preload);
        updateRenderState();
    }

    Texture cargaTextura(String nombre)
    {
        return TextureManager.loadTexture(
		                mostermosca.class.getClassLoader().getResource(
		                nombre),
		                Texture.MinificationFilter.BilinearNoMipMaps,
                                Texture.MagnificationFilter.Bilinear);

    }

}


Con esta clase siempre podremos crear cualquier Skybox a nuestro gusto
y de este modo tendremos nuestro mundo creado.

En el update() del juego tendremos que añadir este código.

       skybox.setLocalTranslation(cam.getLocation());
        skybox.updateGeometricState(0, true);

Con esto haremos que el Skybox este siempre donde la cámara de
ese modo nunca nos saldremos de él.

Ahora os mostrare como creamos el suelo por el que queremos que se
mueva nuestro jugador.

private void buildTerrain() {


        MidPointHeightMap heightMap = new MidPointHeightMap(64, 1f);
        Vector3f terrainScale = new Vector3f(4, 0.0575f, 4);
        tb = new TerrainBlock("Terrain", heightMap.getSize(), terrainScale,
                heightMap.getHeightMap(), new Vector3f(0, 0, 0));

        tb.setModelBound(new BoundingBox());
        tb.updateModelBound();
        ProceduralTextureGenerator pt = new ProceduralTextureGenerator(
                heightMap);
        pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader().getResource("adictosaltrabajo/texturas/grassb.png")), -128, 0, 128);
        pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader().getResource("adictosaltrabajo/texturas/dirt.jpg")), 0, 128, 255);
        pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader().getResource("adictosaltrabajo/texturas/highest.jpg")), 128, 255,
                384);
        pt.createTexture(32);

        TextureState ts = display.getRenderer().createTextureState();
        Texture t1 = TextureManager.loadTexture(pt.getImageIcon().getImage(),
                Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear, true);
        ts.setTexture(t1, 0);

        Texture t2 = TextureManager.loadTexture(
                TestTerrain.class.getClassLoader().getResource(
                "adictosaltrabajo/texturas/Detail.jpg"),
                Texture.MinificationFilter.Trilinear,
                Texture.MagnificationFilter.Bilinear);

        ts.setTexture(t2, 1);
        t2.setWrap(Texture.WrapMode.Repeat);

        t1.setApply(Texture.ApplyMode.Combine);
        t1.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
        t1.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
        t1.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
        t1.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
        t1.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);

        t2.setApply(Texture.ApplyMode.Combine);
        t2.setCombineFuncRGB(Texture.CombinerFunctionRGB.AddSigned);
        t2.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
        t2.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
        t2.setCombineSrc1RGB(Texture.CombinerSource.Previous);
        t2.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);

        tb.setRenderState(ts);
        tb.setDetailTexture(1, 16);
        tb.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

        scene.attachChild(tb);


    }

Este método lo podemos añadir en el fichero java donde
estemos creando la escena del juego.

Si no queremos que la cámara se sitúe por debajo del
terreno debemos añadir el siguiente código en el
método update() del juego.

       if (cam.getLocation().y < (tb.getHeight(cam.getLocation()) + 2)) {
             cam.getLocation().y = tb.getHeight(cam.getLocation()) + 2;
             cam.update();
        }

Entonces por el momento hemos montado el cielo, es decir la caja donde
transcurrirá el juego y el suelo del juego donde se
moverá nuestro jugador.

Ahora crearemos nuestro jugador.

Para hacer esto primero tenemos que hacer es cargar el modelo, para
ello nos creamos una clase llamada CargadorModelos y le ponemos este
código que sera el que nos permita cargar un xml para nuestro
muñeco.

class CargadorModelos {

    private final Logger logger = Logger.getLogger(CargadorModelos.class.getName());
    private URL urlmodelo = null;
    protected XMLImporter xmlImporter = XMLImporter.getInstance();

    CargadorModelos(URL path) {
        urlmodelo = path;
    }

    protected Spatial CargaModelo() {
        // May also be called during update() loop to add to scene.
        Spatial espacioCargado = null;

        try {
            espacioCargado = (Spatial) xmlImporter.load(urlmodelo);
        } catch (Exception e) {
            logger.log(Level.INFO, "Error al cargar fichero: " + urlmodelo, e);
            throw new IllegalArgumentException("Error al cargar el modelo: " + urlmodelo, e);
        }

        espacioCargado.setModelBound(new BoundingBox());
        //espacioCargado.setLocalScale(0.1f);

        espacioCargado.updateModelBound();

        // The default update loop will update world bounding volumes.

        return espacioCargado;
    }
}

Después no crearemos una clase para poder definir lo que
queremos que haga el jugador en nuestro caso la hemos llamado Mosca.

public class Mosca extends Node {

    Spatial mosca;
    private Quaternion q1 = new Quaternion();
    private Quaternion q2 = new Quaternion();
    private Quaternion q3 = new Quaternion();
    private Vector3f leanAxis = new Vector3f(0, 1, 0);
    private Vector3f vaux = new Vector3f();
    private Vector3f vmov = new Vector3f();
    private Vector3f aux = new Vector3f(0f, 0f, 1f);
    private float Velocidad = 3f;
    private boolean primeravez = true;
    float xini, zini, xfin, xac, zac, zfin, incx, incz;

    public Mosca(String id, Spatial pmosca) {

        super(id);
        this.detachChild(this.mosca);
        mosca = pmosca;

        this.attachChild(this.mosca);
    }

    public void update(float time) {

        MoverAdelante(vmov);
    }

    public void Stop() {
        this.localTranslation.addLocal(0, 0, 0);
    }

    public void GoTo(Vector3f destino) {


        mosca.worldToLocal(destino, vaux);

        if (destino != mosca.getLocalTranslation()) {


            if (primeravez == true) {
                vmov = vaux.divide(10);
                primeravez = false;

                RotarMosca(vaux);


            }
           MoverAdelante(vmov);

        }
    }

    public void MoverAdelante(Vector3f donde) {
        this.localTranslation.addLocal(donde);


    }

    public void MoverAtras() {
        this.localTranslation.addLocal(0.2f, 0.0f, -0.2f);
    }

    public void DesplazarMosca() {
        int Direccion;

        Random rnd = new Random();

        Direccion = (int) (rnd.nextDouble() * 2);

        if (Direccion == 1) {
            this.getLocalTranslation().addLocal(1, 0, 0);
        } else {
            this.getLocalTranslation().addLocal(-1, 0, 0);
        }


    }

    public void mueveMosca(int accion) {


        if (accion == MoverMoscaKeyInputAction.DERECHA) {
            this.getLocalTranslation().addLocal(-1, 0, 0);

        }
        if (accion == MoverMoscaKeyInputAction.IZQUIERDA) {
            this.getLocalTranslation().addLocal(1, 0, 0);
        }
        if (accion == MoverMoscaKeyInputAction.ABAJO) {
            this.getLocalTranslation().addLocal(0, 0, -1);
        }
        if (accion == MoverMoscaKeyInputAction.ARRIBA) {
            this.getLocalTranslation().addLocal(0, 0, 1);
        }


    }

    public void RotarMosca(Vector3f donde) {

        this.getLocalRotation().fromAngleAxis(aux.angleBetween(donde.normalize()), this.leanAxis);
    }
}

Como se puede comprobar es una clase derivada de Node. En esta clase
esta implementado algunos métodos esenciales como el método
update() que se ejecuta cada vez que el juego realiza su update(),
métodos interesantes de este código sería el GoTo ya que
haya la dirección a la que queremos que nuestro jugador se
mueva, el método RotarMosca() ya que en el se muestra como se
hace para rotar un personaje sobre su eje y, se puede observar que el vector
aux esta inicializado en la dirección que mira el jugador (este
debería haber sido pintado mirando hacia un eje especifico) en
nuestro caso el eje Z, entonces normalizamos el vector dirección
para que exista una intersección entre los dos vectores. Y por
último el método mueveMosca que implementa movimientos que la
mosca realizara cuando se pulsen las teclas correspondientes.

Si queremos que el jugador siempre que se mueva por el mapa esté siempre sobre la superficie debemos añadir este
código.

        float characterMinHeight = tb.getHeight(player.getLocalTranslation()) + agl;

        if (!Float.isInfinite(characterMinHeight) && !Float.isNaN(characterMinHeight)) {

            player.getLocalTranslation().y = characterMinHeight;
        }

        tb.getSurfaceNormal(player.getLocalTranslation(), normal);
        if (normal != null) {
            player.rotateUpTo(normal);
        }

 La variable agl que es de tipo float contiene el valor de la
altura del BoundingBox del jugador, entonces al sumárselo a la
altura del mapa, conseguimos el punto donde tenemos que situar al
personaje.

Para crear las etiquetas encima de nuestros personajes, encontré
por internet una clase que se llama TextLabel2D y me solucionó este
problema.

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.Arrays;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.FastMath;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.BlendState.TestFunction;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

public class TextLabel2D {
    private String text;
    private float blurIntensity = 0.1f;
    private int kernelSize = 5;
    private ConvolveOp blur;
    private Color foreground = new Color(1f, 1f, 1f);
    private Color background = new Color(0f, 0f, 0f);
    private float fontResolution = 40f;
    private int shadowOffsetX = 2;
    private int shadowOffsetY = 2;
    private Font font;

    public TextLabel2D(String text) {
        this.text = text;
        updateKernel();
        setFont(Font.decode("Sans PLAIN 40"));
    }

    public void setText(String text)
    {
        this.text=text;
    }

    public String getText()
    {
       return this.text;

    }
    public void setFont(Font font){
        this.font = font;
    }

    public void setShadowOffsetX(int offsetPixelX){
        shadowOffsetX = offsetPixelX;
    }
    public void setShadowOffsetY(int offsetPixelY){
        shadowOffsetY = offsetPixelY;
    }
    public void setBlurSize(int kernelSize){
        this.kernelSize = kernelSize;
        updateKernel();
    }
    public void setBlurStrength(float strength){
        this.blurIntensity = strength;
        updateKernel();
    }
    public void setFontResolution(float fontResolution){
        this.fontResolution = fontResolution;
    }

    private void updateKernel() {
        float[] kernel = new float[kernelSize*kernelSize];
        Arrays.fill(kernel, blurIntensity);
        blur = new ConvolveOp(new Kernel(kernelSize, kernelSize, kernel));
    }

    /**
     *
     * @param scaleFactors is set to the factors needed to adjust texture coords
     * to the next-power-of-two- sized resulting image
     */
    private BufferedImage getImage(Vector2f scaleFactors){
        BufferedImage tmp0 = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = (Graphics2D) tmp0.getGraphics();
        Font drawFont = font.deriveFont(fontResolution);
        g2d.setFont(drawFont);
        Rectangle2D b = g2d.getFontMetrics().getStringBounds(text, g2d);

        int actualX = (int)b.getWidth()+kernelSize+1+shadowOffsetX;
        int actualY = (int)b.getHeight()+kernelSize+1+shadowOffsetY;

        int desiredX = FastMath.nearestPowerOfTwo(actualX);
        int desiredY = FastMath.nearestPowerOfTwo(actualY);

        if(scaleFactors != null){
            scaleFactors.x = (float)actualX/desiredX;
            scaleFactors.y = (float)actualY/desiredY;
        }

        tmp0 = new BufferedImage(desiredX, desiredY, BufferedImage.TYPE_INT_ARGB);

        g2d = (Graphics2D) tmp0.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        int textX = kernelSize/2;
        int textY = g2d.getFontMetrics().getMaxAscent() - kernelSize/2;

        g2d.setColor(background);
        g2d.drawString(text, textX + shadowOffsetX, textY + shadowOffsetY);

        BufferedImage ret = blur.filter(tmp0, null);

        g2d = (Graphics2D) ret.getGraphics();
        g2d.setFont(drawFont);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        g2d.setColor(foreground);
        g2d.drawString(text, textX, textY);

        return ret;
    }

    public Quad getQuad(float height){
        Vector2f scales = new Vector2f();
        BufferedImage img = getImage(scales);
        float w = img.getWidth() * scales.x;
        float h = img.getHeight() * scales.y;
        float factor = height / h;
        Quad ret = new Quad("textLabel2d", w * factor , h * factor);
        TextureState ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
        Texture tex = TextureManager.loadTexture(img, MinificationFilter.BilinearNoMipMaps, MagnificationFilter.Bilinear, true);



        ts.setTexture(tex);
        ts.setEnabled(true);
        ret.setRenderState(ts);

        ret.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);

        BlendState as = DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
        as.setBlendEnabled(true);
        as.setTestEnabled(true);
        as.setTestFunction(TestFunction.GreaterThan);
        as.setEnabled(true);
        ret.setRenderState(as);

        ret.setLightCombineMode(LightCombineMode.Off);
        ret.updateRenderState();
        return ret;
    }

    public BillboardNode getBillboard(float height){
        BillboardNode bb = new BillboardNode("bb");
        Quad q = getQuad(height);
        bb.attachChild(q);
        return bb;
    }



    public void setForeground(Color foreground) {
        this.foreground = foreground;
    }

    public void setBackground(Color background) {
        this.background = background;
    }
}

Y la verdad es muy útil, con que se cree y se añada como
hijo al elemento que queramos etiquetar ya esta.

El código fuente de las clases implementadas las puede descargar de: fuentes.zip

Espero que les haya sido útil este tutorial, seguiremos
haciendo más tutoriales sobre esta tecnología
analizando más ejemplos algo más complicados,
todo el que quiera hacer una aportación será bien
recibida. Para comunicarme cualquier problema o sugerencia de mejora
podéis utilizar la zona de comentarios, de este modo todo el
mundo se podrá aprovechar de las respuestas.

Saludos.

2 COMENTARIOS

  1. La verdad es que es muy interesante JME , ojala sigan haciendo tutoriales sobre el ya que no hay muchos ejemplos tan bien explicados por internet.

    Saludos.

  2. Hola, tengo un 3ds, despues que lo levanto con blender que le tengo que agregar, para luego convertirlo a xml y que lo reconosca el jmonkey, que coordenadas va a necesitar el jmonkey ????

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad