Tutoriais Jmonkeyengine

Estes tutoriais não são de minha autoria, apenas os estou traduzaindo. O lugar da onde os estou retirando é http://jmonkeyengine.org/wiki/doku.php/jme3:beginner.

Table of Contents

jMonkeyEngine 3 Tutorial (1) - Hello SimpleApplication

Pré-requisitos: Este tutorial assume que você tenha baixado o SDK da JMonkeyEngine.

Nesta série de tutoriais, nós assumimos que você usa o SDK da jMonkeyEngine. Como um desenvolvedor Java intermediário ou avançado, você rapidamente verá que, em geral, você pode desenvolver código da jMonkeyEngine em qualquer ambiente de desenvolvimento integrado (NetBeans IDE, Eclipse, IntelliJ) ou mesmo da linha de comando.

OK, Vamos nos aprontar para criar nossa primeira aplicação jMonkeyEngine3.

Crie um Projeto

Na SDK da jMonkeyEngine:

  1. Escolha Arquivo (File) → Novo Projeto (New Project)… do menu principal.
  2. No assistente de Novo Projeto, selecione o modelo JME3 → Jogo Básico (Basic Game). Clique em prosseguir (Next).
    1. Especifique um nome de projeto, e.g. "HelloWorldTutorial"
    2. Especifique um caminho para armazenar seu novo projeto, e.g. um diretório projetosjMonkey no seu diretório de usuário
  3. Clique em terminar (Finish).

Se você tem perguntas, leia mais sobre Criação de Projeto aqui.

Nós recomendamos atravessar os passos você mesmo, como descrito nos tutoriais. Alternativamente, você pode criar um projeto baseado no modelo JmeTests no SDK da jMonkeyEngine. Isto criará um projeto que já contém as amostras jme3test.helloworld (e muitas outras). Por exemplo, você pode usar o projeto JmeTests para verificar se você tem a solução certa.

Escreva uma aplicação de amostra

Para este tutorial, você deseja criar um pacote jme3test.helloworld no seu projeto, e criar um arquivo HelloJME3.java nele.

No SDK da jMonkeyEngine:

  1. Dê um clique com o botão direito no nó pacotes de código-fonte (Source Packages) de seu projeto.
  2. Escolha Novo (New)… → Classe Java (Java Class) para criar um novo arquivo.
  3. Digite o nome da classe: HelloJME3
  4. Digite o nome do pacote: jme3test.helloworld.
  5. Clique em Finalizar (Finish).

O SDK cria o arquivo HelloJME3.java para você.

Código de Amostra

Substitua os conteúdos do arquivo HelloJME3.java com o seguinte código:

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.math.ColorRGBA;

/** Sample 1 - how to get started with the most simple JME 3 application.
 * Display a blue 3D cube and view from all sides by
 * moving the mouse and pressing the WASD keys. */
public class HelloJME3 extends SimpleApplication {

    public static void main(String[] args){
        HelloJME3 app = new HelloJME3();
        app.start(); // start the game
    }

    @Override
    public void simpleInitApp() {
        Box b = new Box(Vector3f.ZERO, 1, 1, 1); // create cube shape at the origin
        Geometry geom = new Geometry("Box", b);  // create cube geometry from the shape
        Material mat = new Material(assetManager,
          "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
        mat.setColor("Color", ColorRGBA.Blue);   // set color of material to blue
        geom.setMaterial(mat);                   // set the cube's material
        rootNode.attachChild(geom);              // make the cube appear in the scene
    }
}

Dê um clique com o botão direito na classe HelloJME3 class e escolha Executar (Run). Se um diálogo de configurações da jME3 aparecer, confirme as configurações padrão.

Você deveria ver uma janela simples exibindo um cubo 3D.
Pressione as teclas WASD keys e mova para navegar ao redor.
Olhe no texto do FPS e na informação de contagem de objeto na esquerda-fundo. Você usará esta informação durante o desenvolvimento, e você removerá ela para a liberação. (Para ler os números corretamente, considere que as 14 linhas de texto contam como 14 objetos com 914 vértices.)
Pressione Escape (Esc) para fechar a aplicação.

Parabéns! Agora camos decobrir como isso funciona!

Compreendendo o código

O código acima tem inicializado a cena, e iniciado a aplicação.

Inicie a SimpleApplication

Olhe na primeira linha. A classe HelloJME3.java estende com.jme3.app.SimpleApplication.

public class HelloJME3 extends SimpleApplication {
  // seu código...
}

[[/code]]

Todo jogo JME3 é uma instância de com.jme3.app.SimpleApplication. A classe SimpleApplication gerencia seu grafo de cena 3D e automaticamente desenha ele para a tela – isto é, em breve, o que uma engine de jogo faz para você!

Você inicia todo jogo JME3 do método main(), como toda aplicação Java padrão:

  1. Instancie sua classe baseada em SimpleApplication
  2. Chame o método start() da aplicação para iniciar a engine de jogo.
    public static void main(String[] args){
        HelloJME3 app = new HelloJME3(); // instantiate the game
        app.start();                     // start the game!
    }

Este código abre sua janela de aplicação. Vamos aprender a por alguma coisa para a janela em seguinda.

Entendendo a Terminologia

O que você quer fazer Como você diz isso na terminologia JME3
Você quer criar um cubo. Eu crio uma geometria (Geometry) com uma forma de caixa (Box) 1x1x1
Você quer usar uma cor azul. Eu crio um Material com uma propriedade cor (Color) azul
Você quer colorir o cubo azul. Eu coloco o Material da geometria caixa (Box Geometry)
Você quer adicionar o cubo para a cena. Eu anexo a geometria caixa (Box Geometry) para o nó raíz (rootNode)
Você quer que o cubo apareça no centro. Eu crio a caixa (Box) na origem = em Vector3f.ZERO.

Se você não esta familiar com o vocabulário, leia mais sobre o Grafo de Cena aqui.

Inicialize a Cena

Olhe no resto da amostra de código. O método simpleInitApp() é automaticamente chamado uma vez no início quando a aplicação inicia. Todo jogo JME3 deve ter este método. No mpetodo simpleInitApp(), você carrega objetos do jogo antes que o jogo inicie.

    public void simpleInitApp() {
       // seu código de inicialização...
    }

O código de inicialização de um cubo azul parece como se segue:

    public void simpleInitApp() {
        Box b = new Box(Vector3f.ZERO, 1, 1, 1); // create a 2x2x2 box shape at the origin
        Geometry geom = new Geometry("Box", b);  // create a cube geometry from the box shape
        Material mat = new Material(assetManager,
          "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
        mat.setColor("Color", ColorRGBA.Blue);   // set color of material to blue
        geom.setMaterial(mat);                   // set the cube geometry 's material
        rootNode.attachChild(geom);              // make the cube geometry appear in the scene
    }

Um jogo JME típico tem o seguinte processo de inicialização:

  1. Você inicializa os objetos do jogo:
    1. Você cria ou carrega objetos e posiciona eles.
    2. Você faz objetos aparecerem na cena por anexálos ao nó raiz (rootNode).
    3. Exemplos: Carregar o jogador, terreno, céu, inimigos, obstáculos, …, e colocá-los nas suas posições de início.
  2. Você inicializa variáveis
    1. Você cria variáveis para rastrear o estado de jogo.
    2. Você configura as variáveis para os valores de início delas.
    3. Exemplos: Coloque a pontuação para 0, coloque a saúde para 100%, …
  3. Você inicializa as teclas e ações do mouse.
    1. As seguintes ligações de entrada já estão pré-configuradas:
    2. W,A,S,D keys – Mova ao redir da cena
    3. Movimento do mouse e teclas seta - Vire a câmera
    4. Escape (Esc) - Sai do jogo
    5. Defina suas próprias teclas adicionais e ações de clique do mouse
    6. Exemplos: CLique para aturar, pressione a Barra de Espaço para pular, …

Conclusão

Você têm aprendido que uma SimpleApplication é um bom ponto de início porque ela fornece você com:

  • Um método simpleInitApp() onde você cria objetos.
  • Um nó raiz (rootNode) onde você anexa objetos para fazê-los aparecer na cena.
  • Configurações de entrada padrão úteis que você pode usar para navegação na cena.

Quando desenvolvendo uma aplicação de jogo, você irá querer:

  • Inicializar a cena de jogo
  • Dispara ações de jogo
  • Responder a entrada do usuário.

Agora os próximos tutoriais lhe ensinarão a como realizar estas tarefas com a jMonkeyEngine 3.

Continue com o tutorial Hello Node, onde você aprende mais detalhes sobre como inicializar o mundo do jogo, também conhecido como o grafo de cena.

Veja também:

Instalar a JMoneyEngine
SimpleApplication da Linha de comando
Criar um projeto JME3.

jMonkeyEngine 3 Tutorial (2) - Hello Node

Anterior: Hello SimpleApplication, Próximo: Hello Assets.

Neste tutorial nós daremos uma olhada na criação de uma cena 3D.

  • Este tutorial assume que você save o que o grafo de cena é.
  • Para uma introdução visual, cheque o Grafo de Cena para Novatos.

Quando criando um jogo 3D

  • Você cria alguns objetos como jogadores, edifícios, etc.
  • Você adiciona os objetos para a cena.
  • Você move, redimensiona, rotaciona, colore, e anima eles.

Você aprendera que o grafo de cena representa o mundo 3D, e porque o nó raiz (rootNode) é importante. Você aprenderá como criar objetos simples, como deixá-los transportar dados customizados (como, por exemplo, pontos de saúde), e como "transformá-los" por mover, escalonar e rotacionar. Você compreenderá a diferença entre os dois tipos de "Espaciais" no grafo de cena: nós (Nodes) e geometrias (Geometries).

Amostra de código

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node;

/** Sample 2 - How to use nodes as handles to manipulate objects in the scene.
 * You can rotate, translate, and scale objects by manipulating their parent nodes.
 * The Root Node is special: Only what is attached to the Root Node appears in the scene. */

public class HelloNode extends SimpleApplication {

    public static void main(String[] args){
        HelloNode app = new HelloNode();
        app.start();
    }

    @Override
    public void simpleInitApp() {

        /** create a blue box at coordinates (1,-1,1) */
        Box box1 = new Box( Vector3f.ZERO, 1,1,1);
        Geometry blue = new Geometry("Box", box1);
        Material mat1 = new Material(assetManager, 
                "Common/MatDefs/Misc/Unshaded.j3md");
        mat1.setColor("Color", ColorRGBA.Blue);
        blue.setMaterial(mat1);
        blue.move(1,-1,1);

        /** create a red box straight above the blue one at (1,3,1) */
        Box box2 = new Box( Vector3f.ZERO, 1,1,1);
        Geometry red = new Geometry("Box", box2);
        Material mat2 = new Material(assetManager, 
                "Common/MatDefs/Misc/Unshaded.j3md");
        mat2.setColor("Color", ColorRGBA.Red);
        red.setMaterial(mat2);
        red.move(1,3,1);

        /** Create a pivot node at (0,0,0) and attach it to the root node */
        Node pivot = new Node("pivot");
        rootNode.attachChild(pivot); // put this node in the scene

        /** Attach the two boxes to the *pivot* node. */
        pivot.attachChild(blue);
        pivot.attachChild(red);
        /** Rotate the pivot node: Note that both boxes have rotated! */
        pivot.rotate(.4f,.4f,0f);
    }
}

Construa e execute a amostra de código. Você deveria ver duas caixas coloridas inclinadas no mesmo ângulo.

Entendendo a Terminologia

Neste tutorial você aprenderá alguns novos termos:

O que você quer fazer Como você diz isso na terminologia JME3
Colocar a disposição da cena 3D Popular o grafo de cena
Criar objetos da cena Criar espaciais (Spatials) (e.g. criar geometrias (Geometries) )
Fazer um objeto aparecer na cena Anexar um espacial (Spatial) para o nó raiz rootNode
Fazer um objeto desaparecer da cena Retirar um espacial (Spatial) do nó raiz rootNode
Posicionar/mover, virar, ou redimensionar um objeto Transladar, ou rotacionar, ou escalar um objeto = transformar um objeto

Toda aplicação JME3 tem um nó raiz (rootNode): Seu jogo automaticamente herda o objeto rootNode de SimpleApplication. Tudo anexado ao nó raiz (rootNode) é parte do grafo de cena. Os elementos do grafo de cena são os espaciais (Spatials).

  • Um Spatial contém a localização, rotação e escala de um objeto.
  • Um Spatial pode ser carregado, transformado, e salvo.
  • Há dois tipos de Spatials: nós (Nodes) e geometrias (Geometries).
Geometria (Geometry) Nó (Node)
Visibilidade: Uma geometria (Geometry) é um objeto de cena visível Um nó (Node) é uma "alavanca" invisível para objetos da cena.
Propósito: Uma geometria (Geometry) armazena a aparência de um objeto. Um nó (Node) agrupa geometrias (Geometries) e outros nós (Nodes) juntos.
Exemplos: Uma caixa, uma esfera, um jogador, um edifício, um pedaço de terreno, um veículo, mísseis, NPCs, etc… O nó raiz (rootNode), um nó de chão agrupando vários terrenos, um nó veículo-com-passageiros customizado, um nó jogador-com-arma, um nó de aúdio, etc…

Entendendo o Código

O que acontece no trecho de código? Você usa o método simpleInitApp() que foi introduzido no primeiro tutorial para inicializar a cena.

  1. Você cria a primeira geometria caixa
    1. Crie uma forma caixa (Box) com extensões de (1,1,1), isto faz a caixa 2x2x2 unidades do mundo grande.
    2. Posicione a caixa em (1,-1,1) usando o método move() method. (Não mude o Vector3f.ZERO a menos que você queira mudar o centro de rotação)
    3. Envolva a forma caixa (Box) em uma geometria (Geometry).
    4. Crie um material azul
    5. Aplique o material azul para a geometria da caixa (Box Geometry).

[[code]]

Box box1 = new Box( Vector3f.ZERO, 1,1,1);
Geometry blue = new Geometry("Box", box1);
Material mat1 = new Material(assetManager,
"Common/MatDefs/Misc/Unshaded.j3md");
mat1.setColor("Color", ColorRGBA.Blue);
blue.setMaterial(mat1);
blue.move(1,-1,1);
[[/code]]

  1. Você cria uma segunda geometria (Geometry) de caixa.
    1. Crie uma segunda forma caixa (Box) com o mesmo tamanho.
    2. Posicione a segunda caixa em (1,3,1). Isto é imediatamente acima da primeira caixa, com uma lacuna de 2 unidades do mundo entre elas.
    3. Envolva a forma caixa (Box) em uma geometria (Geometry).
    4. Crie um material vermelho
    5. Aplique o material vermelho para a geometria caixa (Box Geometry).

[[code]]
Box box2 = new Box( Vector3f.ZERO, 1,1,1);
Geometry red = new Geometry("Box", box2);
Material mat2 = new Material(assetManager,
"Common/MatDefs/Misc/Unshaded.j3md");
mat2.setColor("Color", ColorRGBA.Red);
red.setMaterial(mat2);
red.move(1,3,1);
[[/code]]

  1. Você cria um nó (Node) pivô
    1. Nomeie o nó "pivot".
    2. Por padrão o nó (Node) é posicionado em (0,0,0).
    3. Anexe o nó (Node) ao nó raiz (rootNode).
    4. O nó (Node) não tem aparência visível na cena.

[[code]]
Node pivot = new Node("pivot");
rootNode.attachChild(pivot);
[[/code]]

Se você executar a aplicação somente com o código dado até aqui, a cena parece vazia. Isto é porque o nó (Node) está invisível, e você não tem ainda anexado quaisquer geometrias (Geometries) visíveis para o nó raiz (rootNode).
Anexe as duas caixas para o nó pivô.

            pivot.attachChild(blue);
            pivot.attachChild(red);

Se você executar o aplicativo com somente o código dado até aqui, você vê dois cubos: Um vermelho imediatamente acima de um azul.
Rotacione o nó pivô.

            pivot.rotate( 0.4f , 0.4f , 0.0f );

Se você executar o aplicativo agora, você verá duas caixas uma no topo da outra - ambas inclinadas no mesmo ângulo.

O que é um nó pivô (Pivot Node)?

Você pode transformar (e.g. rotacionar) geometrias (Geometries) ao redor do próprio centro delas, ou ao redor de um ponto central definido pelo usuário. Um ponto central definido pelo usuário para um ou mais geometrias (Geometries) é chamado pivô.

Neste exemplo, você agrupou duas geometrias (Geometries) por anexá-las para um nó pivô (Node). Você vê o nó (Node) pivô como um instrumento para rotacionar as duas geometrias (Geometries) ao mesmo tempo ao redor de um centro em comum. Rotacionar o nó (Node) pivô rotaciona todas as geometrias (Geometries) anexadas, de uma única vez. O nó pivô é o centro da rotação. Antes de anexar as outras geometrias (Geometries), tenha certeza que o nó pivô está em (0,0,0). Transformar um nó (Node) pai para transformar todas as crianças espaciais (Spatials) anexadas é uma tarefa comum. Você usará este método muito em seus jogos quando você mover espaciais (Spatials).
Exemplos: Um veículo e seu motorista movem juntos; um planeta com sua lua orbitam o sol.
Contraste este caso com a outra opção: Se você não criar um nó pivô extra e transformar uma geometria (Geometry), então toda transformação é feita relativa a origem da geometria (Geometry) (tipicamente o centro dela).
Exemplos: Se você rotacionar cada cubo diretamente (usando red.rotate(0.1f , 0.2f , 0.3f); e blue.rotate(0.5f , 0.0f , 0.25f);), então cada cubo é rotacionado individualmente ao redor do seu centro. Isto é similar a um planeta rotacionando ao redor de seu próprio centro.

Como eu Populo o Grafo de Cena?

Tarefa…? Solução!
Crie um espacial (Spatial) Crie uma forma malha (Mesh), envolva ela em uma geometria (Geometry), e dê a ela um Material. Por exemplo:
Box mesh = new Box(Vector3f.ZERO, 1, 1, 1); // a cuboid default mesh
Geometry thing = new Geometry("thing", mesh); 
Material mat = new Material(assetManager,
   "Common/MatDefs/Misc/ShowNormals.j3md");
thing.setMaterial(mat);
Faça um objeto aparecer na cena Anexe o espacial (Spatial) para o nó raiz (rootNode), ou para qualquer no que esteja anexado para o nó raiz (rootNode).
rootNode.attachChild(thing);
Remova objetos da cena Retire o nó espacial (Spatial) do nó raiz (rootNode), e de qualquer nó que esteja vinculado ao nó raiz (rootNode).
rootNode.detachChild(thing);
rootNode.detachAllChildren();
Ache um nó espacial na cena pelo nome do objeto, ou ID, ou por sua posição na hierarquia pai-criança. Olhe na criança ou pai do nó:
Spatial thing = rootNode.getChild("thing");
Spatial twentyThird = rootNode.getChild(22);
Spatial parent = myNode.getParent();
Especifique o que deveria ser carregado no início Tudo que você inicializa e anexa ao nó raiz (rootNode) no método simpleInitApp() é parte da cena no início do jogo.

Como eu transformo espaciais (Spatials)?

Há três tipos de transformação 3D: Translação, Escalonamento, e Rotação.

[[row]]
[[/table]]
Translação move espaciais (Spatials ) eixo X eixo Y eixo Z
Especifique a nova localização em três dimensões: Quão distante ela está da origem indo direita para cima?
Para mover um espacial (Spatial) para coordenadas específicas, tais como (0,40.2f,-2), use:
thing.setLocalTranslation( new Vector3f( 0.0f, 40.2f, -2.0f ) );

Para mover um espacial (Spatial) por uma certa quantia, e.g. mais acima (y=40.2f) e mais atrás (z=-2.0f):

thing.move( 0.0f, 40.2f, -2.0f );
+right -left +up -down +forward -backward
Escalonamento redimensiona espaciais (Spatials) X-axis Y-axis Z-axis
Especifique o fator de escalonamento em cada dimensão: tamanho, altura, comprimento.
um valor entre 0.0f e 1.0f diminue o espacial (Spatial); maior que 1.0f estica ele; 1.0f mantém ele o mesmo.
Usando o mesmo valor para cada dimensão escalona proporcionalmente, valor diferentes esticam ele.
Para escalonar um espacial (Spatial) 10 vezes mais longo, um décimo da altura, e manter o mesmo comprimento:
thing.scale( 10.0f, 0.1f, 1.0f );
length height width
Rotação gira espaciais (Spatials) X-axis Y-axis Z-axis
Rotação 3-D é um pouco complicado (aprenda os detalhes aqui). em breve: Você pode rotacionar ao redor de três eixos: Pitch (X), yaw (Y), e roll (Z). Você pode especificar ângulos em graus por multiplicar o valor de graus com FastMath.DEG_TO_RAD.
Para rolar um objeto 180° ao redor do z axis:
thing.rotate( 0f , 0f , 180*FastMath.DEG_TO_RAD );

Dica: Se sua idéia de jogo pede uma quantidade séria de rotações, é merecedor dar uma olhada em quaternions, uma estrutura de dado que pode combinar e armazenar rotações eficientemente.

thing.setLocalRotation( 
  new Quaternion().fromAngleAxis(180*FastMath.DEG_TO_RAD, new Vector3f(1,0,0)));
pitch = fazer um sinal de sim com sua cabeça yaw = agitar sua cabeça roll = inclinar sua cabeça

Como eu Resolvo Problemas com espaciais (Spatials)?

Se você obtém resultados inesperados, cheque se você fez os seguintes enganos frequentes:

[[table]]

Problema? Solução!
Geometria (Geometry) criada não aparece na cena. Você anexou ela a (um nó que está vinculado a) o nó raiz (rootNode)?
Ela tem um Material?
Qual é sua translação (posição)? Ela está atrás da câmera ou coberta por uma outra geometria (Geometry)?
Ela é tão minúscula ou tão gigante para ver?
Ela está tão distante da câmera? (Tente cam.setFrustumFar(111111f); para ver mais distante ||
Um espacial (Spatial) rotaciona em maneiras inesperadas. Você usou os valores em radianos, e não em graus? (Se você usou graus, multiplique eles com FastMath.DEG_TO_RAD para convertê-los para radianos)
Você criou o espacial (Spatial) na origem (Vector.ZERO) antes de movê-lo?
Você rotacionou ao redor do nó pivô ou ao redor de algo mais?
Você rotacionou ao redor do eixo certo? ||
Uma geometria (Geometry) tem cor (Color) ou Material inepserado. Você reusou um Material de uma outra geometria (Geometry) e tem inadvertidamente mudado suas propriedades? (Se sim, considere cloná-lo: mat2 = mat.clone(); )

Como eu Adiciono um Dado Customizado para espaciais (Spatials)?

Muitos espaciais (Spatials) representam personagens ou outras entidades que o jogador pode interagir. O código acima que rotaciona as duas caixas ao redor de um centro em comum (pivô) poderia ser usado para uma espaçonave estacionada em uma estação espacial orbital, por exemplo.

Dependendo do seu jogo, entidades de jogo não somente mudam a posição delas, rotação ou escala (as transformações que você aprendeu). Entidades de jogo também têm propriedades personalizadas, como saúde, inventário carregado, equipamento usado para um personagem, ou força do casco e combustível restante para uma aeronave. Em Java, você representa dados de entidade como variáveis de classe, e.g. floats, Strings, ou Arrays.

Você pode adicionar dados personalizados diretamente para qualquer nó (Node) ou geometria (Geometry). Você não precisa estender a classe nó (Node) para incluir variáveis! Por exemplo, para adicionar um número de id customizado para um nó, você usaria:

pivot.setUserData( "pivot id", 42 );

Para ler o id do nó (Node) em outro lugar, você usaria:

int id = pivot.getUserData( "pivot id" );

Por usar diferentes chaves de Strings (aqui a chave é o id do pivô), você pode recuperar e configurar vários valores para quaisquer dados que o espacial (Spatial) precisa carregar. Quando você iniciar a escrever seu jogo, você talvez adicione um valor de combustível para um nó carro, valor de velocidade para um nó avião, ou número de moedas douradas para um nó jogador, e muito mais. Entretanto, deve-se notar que somente objetos customizados que implementam Savable podem ser passados.

Conclusão

Você aprenderu que sua cena 3D é um grafo de cena composto de espaciais (Spatials): Geometrias (Geometries) visíveis e nós (Nodes) invisíveis. Você pode transformar espaciais (Spatials), ou anexá-los a nós e transformar os nós. Você sabe a maneira mais fácil de como adicionar propriedades de entidade customizadas (tais como a saúde do jogador ou a velocidade do veículo) para espaciais (Spatials).

Desde que formas padrões como esferas e caixas ficam velhas rápido, continue com o próximo capítulo onde você aprenderá a carregar ativos, como por exemplo, modelos 3-D.

jMonkeyEngine 3 Tutorial (3) - Hello Assets

Anterior: Hello Node, Próximo: Hello Update Loop

Neste tutorial nós aprenderemos a carregar modelos 3D e colcoar texto no grafo de cena, usando o Gerenciador de Ativo (Asset Manager) da JME. Você também aprenderá como determinar os caminhos corretos, e quais formatos de arquivo usar.

Problema no achar os arquivos para executar a amostra? Para conseguir os ativos (modelos 3D), adicione o arquivo jme3-test-data.jar incluso para seu classpath. no projeto criado com o SDK da jMonkeyEngine (recomendado), simplesmente dê um clique com o botão direito em seu projeto, escolha "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adiciona a biblioteca pré-configurada "jme3-test-data" library.

Amostra de código

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapText;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

/** Sample 3 - how to load an OBJ model, and OgreXML model, 
 * a material/texture, or text. */
public class HelloAssets extends SimpleApplication {

    public static void main(String[] args) {
        HelloAssets app = new HelloAssets();
        app.start();
    }

    @Override
    public void simpleInitApp() {

        Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");
        Material mat_default = new Material( 
            assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
        teapot.setMaterial(mat_default);
        rootNode.attachChild(teapot);

        // Create a wall with a simple texture from test_data
        Box box = new Box(Vector3f.ZERO, 2.5f,2.5f,1.0f);
        Spatial wall = new Geometry("Box", box );
        Material mat_brick = new Material( 
            assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat_brick.setTexture("ColorMap", 
            assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg"));
        wall.setMaterial(mat_brick);
        wall.setLocalTranslation(2.0f,-2.5f,0.0f);
        rootNode.attachChild(wall);

        // Display a line of text with a default font
        guiNode.detachAllChildren();
        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
        BitmapText helloText = new BitmapText(guiFont, false);
        helloText.setSize(guiFont.getCharSet().getRenderedSize());
        helloText.setText("Hello World");
        helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);
        guiNode.attachChild(helloText);

        // Load a model from test_data (OgreXML + material + texture)
        Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
        ninja.scale(0.05f, 0.05f, 0.05f);
        ninja.rotate(0.0f, -3.0f, 0.0f);
        ninja.setLocalTranslation(0.0f, -5.0f, -2.0f);
        rootNode.attachChild(ninja);
        // You must add a light to make the model visible
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f));
        rootNode.addLight(sun);

    }
}

Compile e execute a amostra de código. Você deveria ver um ninja verde com um bule colorido permanecendo atrás de uma parede. O texto na tela deveria dizer "Hello World".

O gerenciador de ativo

Por ativos de jogo nós queremos dizer todos os arquivos multimídia, tais como modelos, materiais e texturas, cenas inteiras, shaders customizados, música e arquivos de som, e fontes customizadas. JME3 vem com um objeto AssetManager prático que ajuda você a acessar seus ativos. O AssetManager pode carregar arquivos de:

  • O classpath atual (o nível do topo de seu diretório de projeto),
  • O diretório de ativos de seu projeto, e
  • opcionalmente, caminhos persoanlizados que você registrar.

O seguinte é a estrutura de diretório recomendada em seu diretório de projeto:

MyGame/assets/Interface/
MyGame/assets/MatDefs/
MyGame/assets/Materials/
MyGame/assets/Models/
MyGame/assets/Scenes/
MyGame/assets/Shaders/
MyGame/assets/Sounds/
MyGame/assets/Textures/
MyGame/build.xml <— script de construção Ant
MyGame/src/… <— fontes Java vão aqui
MyGame/…

Isto é apenas um melhor prática sugerida, e é o que você consegue por padrão quando criando um novo projeto Java no SDK da jMokeyEngine. Você pode criar um diretório de ativos e tecnicamente nomear os subdiretórios da maneira que você gostar.

Carregando Texturas

Coloque suas texturas em um subdiretório de assets/Textures/. Carregue a textura em um material antes que você configure o Material. A seguinte amostra de código é do método simpleInitApp() e carrega um modelo de parede simples:

// Crie uma parede com uma textura simples de test_data
Box box = new Box(Vector3f.ZERO, 2.5f,2.5f,1.0f);
Spatial wall = new Geometry("Box", box );
Material mat_brick = new Material( 
    assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat_brick.setTexture("ColorMap", 
    assetManager.loadTexture("Textures/Terrain/BrickWall/BrickWall.jpg"));
wall.setMaterial(mat_brick);
wall.setLocalTranslation(2.0f,-2.5f,0.0f);
rootNode.attachChild(wall);

Neste caso, você cria seu próprio Material e aplica ele para a geometria (Geometry). Você baseia Materiais nas descrições de material padrão (por exemplo, "Unshaded.j3md"), como mostrado neste exemplo.

Carregando Texto e Fontes

Este exemplo exibe o texto "Hello World" na fonte padrão na aresta do fundo da janela. Você anexa texto para o nó da GUI (guiNode) – isto é um nó especial para elementos de exibição plana (ortogonal). Você exibe texto para mostrar a pontuação do jogo, a saúde do jogador, etc. A seguinte amostra de código vai no método simpleInitApp().

// Exibe uma linha de texto com uma fonte padrão
guiNode.detachAllChildren();
guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
BitmapText helloText = new BitmapText(guiFont, false);
helloText.setSize(guiFont.getCharSet().getRenderedSize());
helloText.setText("Hello World");
helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);
guiNode.attachChild(helloText);

Dica: Limpe o texto existente no nó da GUI (guiNode) por retirar todas as suas crianças.

Carregando um modelo

Exporte seu modelo 3D no formato OgreXML (.mesh.xml, .scene, .material, .skeleton.xml) e coloque ele em um subdiretório de assets/Models/. A seguinte amostra de código vai no método simpleInitApp().

// Carrega um modelo de test_data (OgreXML + material + textura)
Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
ninja.scale(0.05f, 0.05f, 0.05f);
ninja.rotate(0.0f, -3.0f, 0.0f);
ninja.setLocalTranslation(0.0f, -5.0f, -2.0f);
rootNode.attachChild(ninja);
// Você deve adicionar uma luz direcional para fazer o modelo visível!
DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
rootNode.addLight(sun);

Note que você precisa criar um Material se você exportou o modelo com um material. Lembre-se de adicionar uma fonte de luz, como mostrado, de outra maneira o material (e o modelo inteiro) não estará visível!

Carregando Ativos de Caminhos Personalizados

E seu jogo dependen de arquivos de modelo fornecidos pelo usuário, que não estão inclusos na distribuição? Se um arquivo não é localizado no local padrão (e.g. diretório de ativos), você pode registrar um localizador (Locator) customizado e carregá-lo de qualquer caminho.

Aqui está um exemplo de uso de um ZipLocator que está registrado para um arquivo town.zip no nível topo de seu diretório de projeto:

    assetManager.registerLocator("town.zip", ZipLocator.class);
    Spatial scene = assetManager.loadModel("main.scene");
    rootNode.attachChild(scene);

Aque está um HttpZipLocator que pode baixar modelos zipados e carregá-los:
    assetManager.registerLocator(
      "http://jmonkeyengine.googlecode.com/files/wildhouse.zip", 
      HttpZipLocator.class);
    Spatial scene = assetManager.loadModel("main.scene");
    rootNode.attachChild(scene);

JME3 oferece ClasspathLocator, ZipLocator, FileLocator, HttpZipLocator, e UrlLocator (see com.jme3.asset.plugins).

Criando Modelos e Cenas

Para criar modelos 3D e cenas, você precisa de um editor de malha 3D (3D Mesh Editor) com um plugin exportador (Exporter) OgreXML. Por exemplo, você pode criar modelos completamente texturizados com Blender. Você pode usar o SDK para carregar modelos, converter modelos e criar cenas deles.

Se você usar Blender, exporte seus modelos como malhas Ogre XML com materiais como se segue:

  1. Abra o menu Arquivo (File) > Exportar (Export) > Exportador OgreXML (OgreXML Exporter) para abrir o diálogo do exportador.
  2. No campo Exportar Materiais (Export Materials): Dê ao material o mesmo nome que o modelo. Por exemplo, o modelo something.mesh.xml acompanha something.material, mais (opcionalmente) something.skeleton.xml e alguns arquivos de textura JPG.
  3. No campo Exportar Malhas (Export Meshes): Selecione um subdiretório de seu diretório assets/Models/ directory. E.g. assets/Models/something/.
  4. Ayive as seguintes configurações do exportador
    • Copiar Texturas (Copy Textures): YES
    • Renderizar materiais (Rendering Materials): YES
    • Virar Eixos (Flip Axis): YES
    • Requer Materiais (Require Materials): YES
    • Nome do Esqueleto segue o da malha (Skeleton name follows mesh): YES
  5. Clique em exportar.

Formatos de Arquivo de Modelo

JME3 pode carregar modelos Ogre XML + materials, Ogre DotScenes, bem como modelos Wavefront OBJ+MTL models. O código loadModel() trabalha com estes arquivos quando você executa o código diretamente do SDK da jMonkeyEngine SDK.

Se você construir os executáveis usando o scrit de construção padrão, então os arquivos de modelo originais (XML, OBJ, etc) não são inclusos. Quando você executar o executável, você obetrá uma mensagem de erro se você tentar carregar quaisquer modelos diretamente:

com.jme3.asset.DesktopAssetManager loadAsset
WARNING: Cannot locate resource: Models/Ninja/Ninja.mesh.xml
com.jme3.app.Application handleError
SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main]
java.lang.NullPointerException

Carregando os arquivos XML/OBJ diretamente é somente aceitável durante a fase de desenvolvimento. Se seus projetista gráfico coloca arquivos atualizados para o diretório de ativos, você pode rapidamente revisar a versão mais recente em seu ambiente de desenvolvimento.

Para teste e para a construção de liberação final, voc~e usa arquivos .j3o exclusivamente. J3o é um formato binário otimizado para aplicações jME3, e arquivos .j3o são automaticamente inclusos no arquivo JAR distribuível pelo script de construção. Quando você faz construções de teste de QA (Quality and Assurance - Averiguação da Qualidade) ou está pronto para liberar, use o SDK para converter todos os arquivos .obj/.scene/.xml/.blend para .j3o, e somente carregue as versões .j3o.

  1. Abra seu Projeto JME3 no SDK da jMonkeyEngine.
    1. Dê um clique com o botão direito em um arquivo .Blend, .OBJ, ou .mesh.xml file na janela Projetos (Projects), e escolha "converter para binário JME3" ("convert to JME3 binary").
    2. O arquivo .j3o aparece próximo ao arquivo .mesh.xml file e tem o mesmo nome.
    3. Mude todas as linhas do seu loadModel() de acordo. Por exemplo:
    Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.j3o");

Se seu executável dá uma exceção em tempo de execução, tenha certeza de que você converteu todos os modelos para .j3o!

Carregando Modelos e a Cena

Tarefa? Solução
Carregar um modelo com materiais
Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml");
rootNode.attachChild(elephant);

Spatial elephant = assetManager.loadModel("Models/Elephant/Elephant.j3o");
rootNode.attachChild(elephant);

carregar um modelo com materiais

[code]
Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.j3o");
Material mat = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); // default material
teapot.setMaterial(mat);
rootNode.attachChild(teapot);
[/code]

Carregar uma cena
Spatial scene = assetManager.loadModel("Scenes/town/main.scene");
rootNode.attachChild(scene);

Spatial scene = assetManager.loadModel("Scenes/town/main.j3o");
rootNode.attachChild(scene);

Exercício - Como Carregar Ativos

Como um exercício, vamos tentar diferentes maneiras de carregar uma cena. Você aprenderá a como carregar a cena diretamente, ou de um arquivo zip.

  1. baixe a cena de amostra town.zip.
  2. (Opcional:) Dezipe o arquivo town.zip para ver a estrutura da Ogre dotScene contida: Você terá um diretório chamado town. Ele contém arquivos XML e textura, e o arquivo chamado main.scene. (Isto é apenas para sua informação, você não precisa fazer nada com ele.)
  3. Coloque o arquivo town.zip no diretório topo de nível de seu projeto JME3, assim:

> jMonkeyProjects/MyGameProject/assets/
> MonkeyProjects/MyGameProject/build.xml
> jMonkeyProjects/MyGameProject/src/
> jMonkeyProjects/MyGameProject/town.zip
> …

  1. Use o seguinte método para carregar modelos de um arquivo zip:
    1. Verifique se town.zip está no diretório do projeto.
    2. Registre um localizador de arquivo zip para o diretório do projeto: Adicione o seguinte código sobre simpleInitApp() {

[code]
assetManager.registerLocator("town.zip", ZipLocator.class);
Spatial gameLevel = assetManager.loadModel("main.scene");
gameLevel.setLocalTranslation(0, -5.2f, 0);
gameLevel.setLocalScale(2);
rootNode.attachChild(gameLevel);
[/code]
# O método loadModel() agora pesquisa pelo arquivo zip diretamente para carregar os arquivos (isto significa, não escreva loadModel(town.zip/main.scene) ou similar!)

  1. Limpe, construa e execute o projeto.
  2. Você deveria agora ver o Ninja+parede+bule permanecendo em uma cidade.

Dica: se você registrar novos localizadores, tenha certeza de que você não tenha quaisquer conflitos de nome: Não nomeie todas as cenas main.scene mas dê a cada cena um nome único.

Anteriormente neste tutorial, você carregou cenas e modelos do diretório de ativo. Isto é a maneira mais comum que você estará carregando cenas e modelos. Aqui está o procedimento típico:

  1. Remova o código que você adicionou para o exercício anterior.
  2. Mova o diretório dezipado town/ no diretório assets/Scenes/ de seu projeto.
  3. Adicione o seguinte código sobre simpleInitApp() {

[code]
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.scene");
gameLevel.setLocalTranslation(0, -5.2f, 0);
gameLevel.setLocalScale(2);
rootNode.attachChild(gameLevel);
[/code]

  1. Note que o caminho é relativo ao diretório assets/…
  2. Limpe, construa e execute o projeto. De novo, você deveria ver o Ninja+parede+bule em uma cidade.

Aqui está um terceiro método que você deve conhecer, carregando uma cena/modelo de um arquivo .j3o:

  1. Remova o código do exercício anterior.
  2. Se você j´pa não fez, abra o SDK e abra o projeto que contém a classe HelloAsset.
  3. Na janela de projetos, navegue para o diretório assets/Scenes/town.
  4. Dê um clique com o botão direito em main.scene e converta a cena para binário: A jMonkeyPlatform gera um arquivo main.j3o.
  5. Adicione o seguinte código em simpleInitApp() {

[code]
Spatial gameLevel = assetManager.loadModel("Scenes/town/main.j3o");
gameLevel.setLocalTranslation(0, -5.2f, 0);
gameLevel.setLocalScale(2);
rootNode.attachChild(gameLevel);
[/code]

  1. Novamente, note que o caminho é relativo ao diretório assets/…
  2. Limpe, construa e execute o projeto.
  3. De novo, você deveria ver o Ninja+parede+bule em uma cidade.

Conclusão

Agora você sabe como popular o grafo de cena com modelos e formas estáticas, e como construir cenas. Você aprendeu como carregar ativos usando o gerenciador de ativos (assetManager) e você viu que os caminhos iniciam relativos ao seu diretório de projeto. Uma outra coisa importante que você aprendeu é converter modelos para o formato .j3o para os JARs executáveis etc.

Vamos adicionar alguma ação para a cena e continuar com o Loop de Atualização!

Veja também:

  • O tutorial de importação do Blender definitivo
  • instantâneos de um modelo grande carregado
  • Tutoriais de vídeo para conseguir OgreXML do 3DS Max usando OgreMax
  • Se você quiser aprender a carregar sons, veja Hello Audio
  • Se você quer aprender mais sobre carregar texturas e materiais, veja Hello Material

jMonkeyEngine 3 Tutorial (4) - Hello Update Loop

Anterior: Hello Assets, Próximo: Hello Input System

Agora que você sabe como carregar ativos, como modelos 3D, você quer implementar alguma jogabilidade que usa estes ativos. Neste tutorial nós olhamos no loop de atualização. O loop de atualização de seu jogo é onde a ação acontece.

Amostra de Código

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;

/** Sample 4 - how to trigger repeating actions from the main update loop.
 * In this example, we make the player character rotate. */
public class HelloLoop extends SimpleApplication {

    public static void main(String[] args){
        HelloLoop app = new HelloLoop();
        app.start();
    }

    protected Geometry player;

    @Override
    public void simpleInitApp() {

        Box b = new Box(Vector3f.ZERO, 1, 1, 1);
        player = new Geometry("blue cube", b);
        Material mat = new Material(assetManager,
          "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        player.setMaterial(mat);
        rootNode.attachChild(player);
    }

    /* This is the update loop */
    @Override
    public void simpleUpdate(float tpf) {
        // make the player rotate
        player.rotate(0, 2*tpf, 0); 
    }
}

Compile e execute o arquivo: Você vê um cubo rotacionando constantemente.

Entendendo o Código

Comparado a nossos exemplos de código anteriores você nota que a geometria (Geometry) do jogador é agora um campo (atributo) de classe. Isto é porque nós queremos que o loop de atualização seja capaz de acessar e transformar esta geometria (Geometry). Como notmal, nós inicializamos o objeto jogador no método simpleInitApp().

Agora dê uma olhada mais de perto no método simpleUpdate() – este é o loop de atualização.

  • A linha player.rotate(0, 2*tpf, 0); muda a rotação do objeto jogador.
  • Nós usamos a variável tpf (tempo por quadro) ("time per frame") para temporizar esta ação dependendo da taxa atual de quadros por segundo. Isto simplesmente significa que o cubo rotaciona com a mesma velocidade em máquinas rápidas e lentas, e o jogo permanece jogável.
  • Quando o jogo executa, o código rotate() é executado repetidamente.

Usando o Loop de Atualização

Um objeto rotacionando é apenas um exemplo simples. No loop de atualização, você tipicamente tem muitos testes e dispara várias ações de jogo. Isto é onde você atualiza a pontuação e pontos de vida, checa por colisões, faz os inimigos calcularem o próximo movimento deles, rola os dados se uma armadilha foi colocada, toca sons ambiente aleatórios, e muito mais.

  • O método simpleUpdate() inicia sua execução após o método simpleInitApp() ter inicializado o grafo de cena e as variáveis de estado.
  • JME3 executa tudo no método simpleUpdate() repetidamente, tão rápido quanto possível.
  • Use o loop para consultar o estado do jogo e então inciar ações.
  • Use o loop para disparar reações e atualizar o estado do jogo.
  • Use o loop com sabedoria, por que ter chamadas demais no loop também faz o jogo mais executar mais lento.

Inicializar - Atualizar - Renderizar

Note o contraste:

  • O método simpleInitApp() é executado somente uma vez, imediatamente no início;
  • O método simpleUpdate() executa repetidamente, durante o jogo.
  • Depois de toda atualização a jMonkeyEngine automaticamente redesenha (renderiza) a tela para você!

Desde que rendering é automático e, inicialização e a atualiação são os dois conceitos mais importantes em uma SimpleApplication para você imediatamente. Estes métodos são onde você carrega e cria dados do jogo (uma vez), e (repetidamente) muda as propriedades deles para atualizar o estado do jogo:

  • simpleInitApp() é a "primeira respiração" da aplicação.
  • simpleUpdate() é a batida de coração da aplicação.
  • A unidade de tempo de atualização é chamada tiques (ticks).

Tudo em um jogo acontece ou durante a inicialização ou durante o loop de atualização. Isto significa que estes dois métodos crescem muito com o tempo. Há duas estratégias como desenvolvedores experientes espalham o código de atualização e inicialização em várias classes Java:

  1. Mova os blocos de código do método simpleInitApp() para AppStates.
  2. Mova os blocos de código do método simpleUpdate() para Custom Controls.

mantenha isto em mente pata depois quando sua aplicação crescer.

Exercícios

Aqui estão algumas coisas divertidas para experimentar:

  • O que acontece se você dar ao método rotate() números negativos?
  • Você pode criar duas geometrias (Geometries) uma próxima a outra, e fazer uma rotacionar duas vezes mais rápido que a outra? (use a varuável tpf)
  • Você pode fazer um cubo que pulsa (cresce e diminui)
  • Você pode fazer um cubo que muda de cor? (mude e configure o Material)
  • Você pode fazer um cubo que role (rotacione ao redor do eixo x, e translade ao longo do eixo z)

Olhe de volta no tutorial Hello Node se você não lembra os métodos de transformação para escalonar, transladar, e rotacionar.

Link para soluções propostas pelos usuário: http://jmonkeyengine.org/wiki/doku.php/jm3:solutions Esteja certo de tentar resolvê-las por si só primeiro!

Conclusão

Agora você está ouvindo ao loop de atualização, "a batida do coração" do jogo, e você pode adicionar todos os tipos de ação a ele.

A próxima coisa que o jogo precisa é alguma interação! Continue aprendendo a como responder a entrada do usuário.

Veja também:

Desenvolvedores jME3 avançados usam Estados da Aplicação (Application States) e Controles Personalizados (Custom Controls) para implementar mecânicas de jogo no loop de atualização deles. Você topará nestes tópicos de novo mais tarde quando você proceder para documentação mais avançada.

jMonkeyEngine 3 Tutorial (5) - Hello Input System

Anteior: Hello Update Loop, Próximo: Hello Material

por padrão, SimpleApplication configura um controle de câmera que permite a você dirigir a camâmera com as teclas WASD, as teclas de seta, e o mouse. Você pode usá-lo cini uma câmera voadora de primeira pessoa imediatamente. Mas e se você precisa de uma câmera de terceira pessoa, ou você deseja que teclas disparem ações de jogo especiais?

Todo jogo tem suas ligações de tecla personalizadas, e este tutorial explica como definí-las. Nós primeiro definimos os pressionamentos de tecla e eventos de mouse, e então nós definimos as ações que elas deveriam disparar.

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.math.ColorRGBA;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;

/** Sample 5 - how to map keys and mousebuttons to actions */
public class HelloInput extends SimpleApplication {

  public static void main(String[] args) {
    HelloInput app = new HelloInput();
    app.start();
  }
  protected Geometry player;
  Boolean isRunning=true;

  @Override
  public void simpleInitApp() {
    Box b = new Box(Vector3f.ZERO, 1, 1, 1);
    player = new Geometry("Player", b);
    Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setColor("Color", ColorRGBA.Blue);
    player.setMaterial(mat);
    rootNode.attachChild(player);
    initKeys(); // load my custom keybinding
  }

  /** Custom Keybinding: Map named actions to inputs. */
  private void initKeys() {
    // You can map one or several inputs to one named action
    inputManager.addMapping("Pause",  new KeyTrigger(KeyInput.KEY_P));
    inputManager.addMapping("Left",   new KeyTrigger(KeyInput.KEY_J));
    inputManager.addMapping("Right",  new KeyTrigger(KeyInput.KEY_K));
    inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE),
                                      new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    // Add the names to the action listener.
    inputManager.addListener(actionListener, new String[]{"Pause"});
    inputManager.addListener(analogListener, new String[]{"Left", "Right", "Rotate"});

  }

  private ActionListener actionListener = new ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Pause") && !keyPressed) {
        isRunning = !isRunning;
      }
    }
  };

  private AnalogListener analogListener = new AnalogListener() {
    public void onAnalog(String name, float value, float tpf) {
      if (isRunning) {
        if (name.equals("Rotate")) {
          player.rotate(0, value*speed, 0);
        }
        if (name.equals("Right")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x + value*speed, v.y, v.z);
        }
        if (name.equals("Left")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x - value*speed, v.y, v.z);
        }
      } else {
        System.out.println("Press P to unpause.");
      }
    }
  };
}

Construa e execute a amostra.

  • Pressiona a barra de espaço ou clique para rotacionar o cubo
  • Pressione as teclas J e K para mover o cubo.
  • Pressione P para pausar e despausar o jogo. Enquanto pausado, o jogo não deveria responder a qualquer entrada, diferente de P.

Definindo Mapeamentos e Gatilhos

Primeiro você registra cada nome de mapeamento com seu(s) gatilho(s). Lembre-se do seguinte:

  • Um gatilho de ação pode ser um ação de mouse ou um pressionamento de tecla.
  • Por exemplo, um movimento de mouse, um clique do mouse, ou pressionando a letra "P".
  • O nome do mapeamento é uma string que você pode escolher.
  • O nome deveria descrever a ação (e.g. "Rotacionar"), e não o gatilho. Porque o gatilho pode mudar.
  • Um mapeamento nomeado pode ter vários gatilhos.
  • Por exemplo, a ação "Rotacionar" ("Rotate") pode ser disparada por um clique e por pressionar a barra de espaço.

Dê uma olhada no código

  • Você registra o mapeamento nomeado "Rotacionar" ("Rotate") para a tecla de gatilho barra de espaço.
    new KeyTrigger(KeyInput.KEY_SPACE)).
  • Na mesma linha, você também registra "Rotacionar" ("Rotate") para um gatilho alternativo de clique de mouse.
    new MouseButtonTrigger(MouseInput.BUTTON_LEFT)
  • Voce atribui os mapeamentos Pause, Esquerda (Left), Direita (Right) para as teclas P, J, K, respectivamente.
    // You can map one or several inputs to one named action
    inputManager.addMapping("Pause",  new KeyTrigger(KeyInput.KEY_P));
    inputManager.addMapping("Left",   new KeyTrigger(KeyInput.KEY_J));
    inputManager.addMapping("Right",  new KeyTrigger(KeyInput.KEY_K));
    inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE),
                                      new MouseButtonTrigger(MouseInput.BUTTON_LEFT));

Agora você precisa registrar seus mapeamentos de gatilho.

  • Você registra a ação pause para o ActionListener, porque ela é uma ação "ligado/desligado" ("on/off").
  • Você registra as ações de movimento para o AnalogListener, porque elas são ações graduais.
    // Add the names to the action listener.
    inputManager.addListener(actionListener, new String[]{"Pause"});
    inputManager.addListener(analogListener, new String[]{"Left", "Right", "Rotate"});

Este código vai dentro do método simpleInitApp(). Mas, desde que nós provavelmente adicionaremos muitas ligações de tecla, nós extrairemos estas linhas e as envolveremos no método auxiliar, initKeys(). O método initKeys() não é parte da interface Controles de Entrada (Input Controls) – você pode nomeá-lo da maneira que quiser. Apenas não se esqueça de chamar seu método do método initSimpleApp().

Implementando as Ações

Você tem mapeado nomes de ações para gatilhos de ação. Agora você especifica as ações por elas mesmas.

Os dois importantes métodos aqui são ActionListener com seu método onAction(), e o AnalogListener com seu método onAnalog(). Nestes dois métodos, você testa por cada mapeamento nomeado, e chama a ação do jogo que você quer ativar.

Neste exemplo, nós ativamos as seguintes ações:

  • Os mapeamento Rotacionar (Rotate) dispara a ação player.rotate(0, valor, 0).
  • Os mapeamentos Esquerda (Left) e Direita (Right) aumentam e diminuem a coordenada x do jogador.
  • O mapeamento Pause mude o valor de um booleano nomeado isRunning.
  • Nós também queremos checar o booleano isRunning antes de qualquer ação (diferente de despausar) seja executada.
  private ActionListener actionListener = new ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Pause") && !keyPressed) {
        isRunning = !isRunning;
      }
    }
  };

  private AnalogListener analogListener = new AnalogListener() {
    public void onAnalog(String name, float value, float tpf) {
      if (isRunning) {
        if (name.equals("Rotate")) {
          player.rotate(0, value*speed, 0);
        }
        if (name.equals("Right")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x + value*speed, v.y, v.z);
        }
        if (name.equals("Left")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x - value*speed, v.y, v.z);
        }
      } else {
        System.out.println("Press P to unpause.");
      }
    }
  };

Você tambpem pode combinar ambos os listeners em um, a engine enviará os eventos apropriados para cada método (onAction ou onAnalog). Por exemplo:

  private MyCombinedListener combinedListener = new MyCombinedListener();

  private static class MyCombinedListener implements AnalogListener, ActionListener {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Pause") && !keyPressed) {
        isRunning = !isRunning;
      }
    }

    public void onAnalog(String name, float value, float tpf) {
      if (isRunning) {
        if (name.equals("Rotate")) {
          player.rotate(0, value*speed, 0);
        }
        if (name.equals("Right")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x + value*speed, v.y, v.z);
        }
        if (name.equals("Left")) {
          Vector3f v = player.getLocalTranslation();
          player.setLocalTranslation(v.x - value*speed, v.y, v.z);
        }
      } else {
        System.out.println("Press P to unpause.");
      }
    }
  }
// ...
inputManager.addListener(combinedListener, new String[]{"Pause", "Left", "Right", "Rotate"});

Está OK usar somente um dos dois Listeners, e não implementar o outrio, se você não está usando este tipo de interação. A seguir, nós damos uma olhada mais de perto para decidir qual dos dois listeners é melhor adequado para qual situação.

Analog, Pressed, ou Released?

Técnicamente, toda entrada pode ser uma ação "analógica" ("analog") ou "digital". Aqui é como você descobre qual listener é o certo para qual tipo de entrada.

Mapeamentos register com o AnalogListener são ativados repetidamente e gradualmente.

  • Parâmetros:
    1. JME dá a você acesso ao nome da ação disparada.
    2. JME dá a você acesso ao valor gradual exibindo a força daquela entrada. No caso de um pressionamento de tec;a isto será o valor tpf que ela foi pressionada desde o último quadro. Para outras entradas tais como um joystick que dá controle analógico contudo então o valor também indicará a força da entrada multiplicada por tpf. Para um exemplo nisto vá para jMonkeyEngine 3 Tutorial (5) - Hello Input System - Variação sobre o tempo que a tecla é pressionada.

Para poder ver o tempo total que uma tecla foi pressionada então o valor de entrada pode ser acumulado. O ouvinte analógico pode também ser combinado com um ouvinte de ação (ActionListener) para que você seja notificado quando a tecla é liberada.

  • Exemplo: Eventos navegacionais (e.g. Esquerda (Left), Direita (Right), Rotacionar (Rotate), Correr (Run), Bombardeios (Strafe)), situações onde você interage continuamente.

Mapeamentos registrados para o ActionListener são tanto digital ou a;óes – "Pressionada ou liberada? Ligado ou desligado?"

  • Parâmetros
    1. JME dá a você acesso ao nome da ação disparada
    2. JME dá a você acesso ao booleano se a tecla está pressionada ou não.
  • Exemplo: Botáo de pausar, disparro, selecionar, pular, interações de clique de "um tempo" (one-time).

Dica: é muito comum que vocë queira que uma ação seja disparada uma vez, no momento quando a tecla é liberada. Por exemplo, quando abrindo uma porta, alterando o estado de um booleanom ou pegando um item. Para atingir isso, você usa um ActionListener e testa por … && !keyPressed. Por exemplo, olhe no código do botão Pause:
[[cide]]
if (name.equals("Pause") && !keyPressed) {
isRunning = !isRunning;
}
[[/code]]

Tabela de gatilhos

Você pode achar a lista de constantes de entrada nos arquivos src/core/com/jme3/input/KeyInput.java, JoyInput.java, and MouseInput.java. Aqui está uma visão geral das constantes de gatilho mais comuns:

Gatilho Código
Botão do mouse: Clique do botão esquerdo MouseButtonTrigger(MouseInput.BUTTON_LEFT)
Botão do mouse: Clique do botão direito MouseButtonTrigger(MouseInput.BUTTON_RIGHT)
Teclado: Caracteres e Números KeyTrigger(KeyInput.KEY_X)
Teclado: Barra de espaço KeyTrigger(KeyInput.KEY_SPACE)
Teclado: Return, Enter KeyTrigger(KeyInput.KEY_RETURN), KeyTrigger(KeyInput.KEY_NUMPADENTER)
Teclado: Escape (ESC) KeyTrigger(KeyInput.KEY_ESCAPE)
Teclado: Setas KeyTrigger(KeyInput.KEY_UP), KeyTrigger(KeyInput.KEY_DOWN), KeyTrigger(KeyInput.KEY_LEFT), KeyTrigger(KeyInput.KEY_RIGHT)

Dica: Se você não lembra uma constante de entrada durante o desenvolvimento, você se beneficia da funcionalidade de completar código de um IDE: Coloque o cursor (|) depois e.g. KeyInput.| e ative o completamento de código para selecionar possíveis identificadores de entrada.

Exercícios

  • Adicione mapeamentos para mover o jogador (caixa) para cima e para baixo com as teclas H e L!
  • Modifique os mapeamentos para que você também dispare o movimento para cima e para baixo com o rolar do eixo do mouse!
    • Dica: Use o novo MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)
  • Em qual situação seria melhor usar variáveis ao invés de literais para as definições de MouseInput/KeyInput?
    int usersPauseKey = KeyInput.KEY_P; 
    ...
    inputManager.addMapping("Pause",  new KeyTrigger(usersPauseKey));

Link para soluções propostas pelos usuários: http://jmonkeyengine.org/wiki/doku.php/jm3:solutions Tenha certeza de tentar resolvê-las por si mesmo antes!

Conclusão

Você é agora caoaz de adicionar interações customizadas para seu jogo: Você sabe que você primeiro tem de definir mapeamentos de tecla, e então as ações para cada mapeamento. Você aprendeu a responder a eventos de mouse e ao teclado. Você entende a diferença entre entrada "análogica" (gradualmente repetida) e "digital" (ligado/desligado).

Agora você já pode escrever um pequeno jogo interativo! Mas não seria mais legal se estas velhas caixas fossem um pouco mais luxuosas? Vamos continuar aprendendo sobre materiais.

jMonkeyEngine 3 Tutorial (6) - Hello Materials

Anterior: Hello Input System, Próximo: Hello Animation

O termo Material inclui tudo que influencia o que a superfície de um modelo 3D parece: A cor, a textura. o brilho, e opacidade/transparência. Coloração de uma única cor é coberta em Hello Node. Carregar modelos que vêm com materiais é coberto com Hello Asset. Neste tutorial você aprende a criar e usar Definições de Material JME3.

Para usar os ativos de exemplo em um novo projeto do SDK da jMonkeyEngine, dê um clique com o botão direito do projeto, selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.texture.Texture;
import com.jme3.util.TangentBinormalGenerator;
import com.jme3.renderer.queue.RenderQueue.Bucket;

/** Sample 6 - how to give an object's surface a material and texture.
 * How to make objects transparent, or let colors "leak" through partially
 * transparent textures. How to make bumpy and shiny surfaces.  */

public class HelloMaterial extends SimpleApplication {
  public static void main(String[] args) {
    HelloMaterial app = new HelloMaterial();
    app.start();
  }

  @Override
  public void simpleInitApp() {
    /** A simple textured cube -- in good MIP map quality. */
    Box boxshape1 = new Box(new Vector3f(-3f,1.1f,0f), 1f,1f,1f);
    Geometry cube = new Geometry("My Textured Box", boxshape1);
    Material mat_stl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    Texture tex_ml = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
    mat_stl.setTexture("ColorMap", tex_ml);
    cube.setMaterial(mat_stl);
    rootNode.attachChild(cube);

    /** A translucent/transparent texture, similar to a window frame. */
    Box boxshape3 = new Box(new Vector3f(0f,0f,0f), 1f,1f,0.01f);
    Geometry window_frame = new Geometry("window frame", boxshape3);
    Material mat_tt = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_tt.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png"));
    mat_tt.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
    window_frame.setMaterial(mat_tt);

    /** Objects with transparency need to be in the render bucket for transparent objects: */
    window_frame.setQueueBucket(Bucket.Transparent);
    rootNode.attachChild(window_frame);

    /** A cube with base color "leaking" through a partially transparent texture */
    Box boxshape4 = new Box(new Vector3f(3f,-1f,0f), 1f,1f,1f);
    Geometry cube_leak = new Geometry("Leak-through color cube", boxshape4);
    Material mat_tl = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_tl.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png"));
    mat_tl.setColor("Color", new ColorRGBA(1f,0f,1f, 1f)); // purple
    cube_leak.setMaterial(mat_tl);
    rootNode.attachChild(cube_leak);

    /** A bumpy rock with a shiny light effect */
    Sphere rock = new Sphere(32,32, 2f);
    Geometry shiny_rock = new Geometry("Shiny rock", rock);
    rock.setTextureMode(Sphere.TextureMode.Projected); // better quality on spheres
    TangentBinormalGenerator.generate(rock);           // for lighting effect
    Material mat_lit = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
    mat_lit.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond.jpg"));
    mat_lit.setTexture("NormalMap", assetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png"));
    mat_lit.setBoolean("UseMaterialColors",true);    
    mat_lit.setColor("Specular",ColorRGBA.White);
    mat_lit.setColor("Diffuse",ColorRGBA.White);
    mat_lit.setFloat("Shininess", 5f); // [1,128]    
    shiny_rock.setMaterial(mat_lit);
    shiny_rock.setLocalTranslation(0,2,-2); // Move it a bit
    shiny_rock.rotate(1.6f, 0, 0);          // Rotate it a bit
    rootNode.attachChild(shiny_rock);

    /** Must add a light to make the lit object visible! */
    DirectionalLight sun = new DirectionalLight();
    sun.setDirection(new Vector3f(1,0,-2).normalizeLocal());
    sun.setColor(ColorRGBA.White);
    rootNode.addLight(sun);
  }
}

Você deveria ver

  • Esquerda - Um cubo com uma textura de macaco marrom.
  • Meio - Uma figura de macaco translucente em frente de uma pedra brilhante.
  • Direita - Um cubo com uma textura de macaco roxa.

Mova ao redor com as teclas para dar um olhar mais próximo na translucência, e na rugosidade da pedra.

Textura não Tonalizada Simples

Tipicamente você quer dar aos objetos em suas cena texturas: Pode ser rocha, grama, tijolo, madeira, água, metal , papel… Uma textura é um arquivo de imagem normal em formato JPG ou PNG. Neste exemplo, você cria uma caixa com uma textura de Macaco não tonalizada como material.

    /** A simple textured cube. */
    Box boxshape1 = new Box(new Vector3f(-3f,1.1f,0f), 1f,1f,1f);
    Geometry cube = new Geometry("My Textured Box", boxshape1);
    Material mat_stl = new Material(assetManager, 
        "Common/MatDefs/Misc/Unshaded.j3md");
    Texture tex_ml = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
    mat_stl.setTexture("ColorMap", tex_ml);
    cube.setMaterial(mat_stl);
    rootNode.attachChild(cube);

Aqui está o que nós fizemos:

  • Criar uma geometria (Geometry) de uma malha caixa (Box). Vamos chamá-la cubo.
  • Criar uma textura com o arquivo Monkey.jpg e carregá-lo dentro do material.
  • O Mapa de Cor (ColorMap) é a camada de material típica onde texturas vão.
  • Aplique o material para o cubo, e anexe o cubo ao nó raiz (rootnode).

Textura não Tonalizada Transparente

Monkey.png é a mesma textura que Monkey.jpg, mas com um canal alfa adicionado. O canal alfa permite você especificar quais áreas da textura você quer que sejam opacas ou transparentes: Áreas pretas permancem opacas, áreas cinza se tornam translucentes, e áreas branca se tornam transparentes.

Para uma textura translucente/transparente, você precisa:

  • Uma textura com canal alfa
  • Um modo de mistura de textura BlendMode.Alpha
  • Uma geometria no "balde" de renderização Bucket.Transparent. Este balde assegura que o objeto translucente é desenhado no topo dos objetos atrás dele, e eles aparecem corretamente sob as partes translucentes. (Para objetos não-translucentes a ordem de desenho não é importante, porque o z-buffer mantém pista de se um pixel está atrás de alguma coisa mais ou não, e a cor de um pixel não depende dos pixels debaixo dele, isto é porque geometrias (Geometries) podem ser desenhadas em qualquer ordem.)
    /** A translucent/transparent texture. */
    Box boxshape3 = new Box(new Vector3f(0f,0f,0f), 1f,1f,0.01f);
    Geometry seethrough = new Geometry("see-through box", boxshape3);
    Material mat_tt = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat_tt.setTexture("ColorMap", assetManager.loadTexture("Textures/ColoredTex/Monkey.png"));
    mat_tt.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); // activate transparency
    seethrough.setMaterial(mat_tt);
    seethrough.setQueueBucket(Bucket.Transparent);
    rootNode.attachChild(seethrough);

O que você fez é o mesmo que antes, com somente um passo adicionado para a transparência.

  • Criar uma geometria (Geometry) de uma malha. Esta Geometria (Geometry) é uma caixa ereta plana.
  • Criar um Material baseado na definição de material padrão da jME3 Unshaded.j3md.
  • Criar uma textura do arquivo Monkey.png e carregue ela dentro do material.
  • O Mapa de Cor (ColorMap) é a camada de material onde texturas vão. Este arquivo PNG deve ter uma camada alfa.
  • Ative a transparência no material por configurar o modo de mistura para Alfa (Alpha)!
  • Aplique o material para a Geometria (Geometry).
  • Configure o QueueBucket da Geometria (Geometry) para Bucket.Transparent.
  • Anexe o cubo para o nó raiz (rootnode).

Dica: aprenda mais sobre criar imagens PNG com uma camada alfa no sistema de ajuda de seu editor gráfico.

Brilho e Rugosidade

Mas texturas não são tudo. Dê uma olhada na esfera brilhante - você não pode conseguir tal material rugoso com apenas uma textura. JME3 também suporta os assim chamados materiais iluminados por Phong:

Em um material iluminado, a camada de textura padrão é referida como Mapa Difuso (Diffuse Map), qualqurt material pode usar esta camada. Um material iluminado pode adicionalmente ter efeitos de iluminação tais como Brilho usado junto com uma camada de Mapa Especular (Specular Map), e mesmo uma superfície realisticamente rugosa ou quebrada com a ajuda de uma camada de Mapa de Normal (Normal Map).

Vamos dar uma olhada na parte do exemplo de código onde você cria a pedra rugosa brilhante.

  1. Crie uma Geometria (Geometry) de uma forma Esfera (Sphere). Note que esta forma é uma malha de esfera suave.
        Sphere rock = new Sphere(32,32, 2f);
        Geometry shiny_rock = new Geometry("Shiny rock", rock);
  1. (Somente para Esferas (Spheres)) Mude o Modo de Textura (TextureMode) da esfera para fazer a textura quadra se projetar melhor na esfera.
            rock.setTextureMode(Sphere.TextureMode.Projected);
  1. Você gera Binormais de Tangente (TangentBinormals) para a malha de esfera para que você use a camada de Mapa de Normal (NormalMap) da textura.
            TangentBinormalGenerator.generate(rock);
  1. Crie um material baseado no material padrão Lighting.j3md.
        Material mat_lit = new Material(assetManager, 
        "Common/MatDefs/Light/Lighting.j3md");
  1. Configure uma textura rochosa padrão na camda do Mapa Difuso (DiffuseMap) - http://jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_pond_pond.jpg
            mat_lit.setTexture("DiffuseMap", assetManager.loadTexture(
            "Textures/Terrain/Pond/Pond.jpg"));
  1. Configure a camada de Mapa Normal (NormalMap) que contém a rugosidade. O Mapa Normal (NormalMap) foi gerado para este Mapa Difuso (DiffuseMap) particular com uma ferramenta especial (e.g. Blender).
            mat_lit.setTexture("NormalMap", assetManager.loadTexture(
            "Textures/Terrain/Pond/Pond_normal.png"));
  1. Configure o Brilho do Material para um valor entre 1 e 128. Para uma rocha, um brilho fuzzy é apropriado
            mat_lit.setFloat("Shininess", 5f); // [1,128]
  1. Atribua seu material recém criado para a Geometria (Geometry).
        shiny_rock.setMaterial(mat_lit);
  1. Vamos mover e rotacionar a geometria um pouco para posicioná-lo melhor.
        shiny_rock.setLocalTranslation(0,2,-2); // Move it a bit
        shiny_rock.rotate(1.6f, 0, 0);          // Rotate it a bit
        rootNode.attachChild(shiny_rock);

Lembre que qualquer material baseado em Lighting.j3md requer uma fonte de luz, como mostrado na amostra de código completa acima.

Dica: Para desativar o Brilho, não configure o Brilho para 0, mas ao invés disso configure a cor Especular (Specular) para ColorRGBA.Black.

Definições de Material Padrão

Como você já viu, você pode achar os seguintes materiais padrão em jme/core-data/Common/….

Definição Padrão Uso Parâmetros
Common/MatDefs/Misc/Unshaded.j3md

Texturizado: Use com mat.setTexture() e Texture. || Color : Color
ColorMap : Texture2D ||

Common/MatDefs/Light/Lighting.j3md

Requer uma fonte de luz. || Ambient, Diffuse, Specular : Color
DiffuseMap, NormalMap, SpecularMap : Texture2D
Shininess : Float ||

Para um jogo, você cria Materiais Customizadps baseados nestas Definições Materiais existentes – como você já viu no exemplo com o material de rocha brilhante.

Exercícios

Exercício 1: Material .j3m Personalizado

Olhe na amostra de vazamento (leak-through) roxo acima de novo. Ela leva quatro linhas para criar e configurar o Material.

  • Note como isto carrega a definição de Material Unshaded.j3md.
  • Note como isto configura o parâmetro de cor (Color) para roxo (new ColorRGBA(1f,0f,1f,1f)).
  • Note como isto configura o Mapa de Cor (ColorMap) para um caminho de textura.

Se você quer usar um material customizado para vários modelos, você pode armazená-lo em um arquivo .j3m, e salvar algumas linhas de código toda vez. Você cria um arquivo j3m como se segue:

  • Crie um arquivo assets/Materials/LeakThrough.j3m em seu diretório de projeto, com o seguinte conteúdo:
    Material Leak Through : Common/MatDefs/Misc/Unshaded.j3md {
         MaterialParameters {
             Color : 1 0 1 1
             ColorMap : Flip Textures/ColoredTex/Monkey.png
         }
    }
  • Note que Material é uma palavra-chave fixa.
  • Note que Leak Through é uma String que você pode escolher para nomear o material.
  • Note como o código configura as mesmas três propriedades, Cor (Color), Mapa de Cor (ColorMap), e Unshaded.j3md.
  • Na amostra de código comente as três linhas com mat_tl nelas.
  • Abaixo delas, adicione a seguinte linha:
    cube_leak.setMaterial((Material) assetManager.loadMaterial( "Materials/LeakThrough.j3m"));
  • Execute o aplicativo. O resultado é o mesmo.

Usando este novo material customizado LeakThrough.j3m somente leva uma linha. Você substituiu as três linhas de uma definição de material "na hora" (on-the-fly) com uma linha que carrega um material personalizado de um arquivo. Este método é muito prático se você usa o mesmo material frequentemente.

Exercício 2: Rugosidade e Brilho

Volte à amostra de rocha rugosa acima:

  • Comente a linha de Mapa Difuso (DiffuseMap) e execute a aplicação. (Descomente ela de novo.)
    • Qual propriedade da rocha é perdida?
  • Comente a linha do Mapa de Normal (NormalMap), e execute a aplicação. (Descomente ela de novo.)
    • Qual propriedade da rocha é perdida?
  • Mude o valor de Brilho para valores como 0, 63, 127.
    • Que aspecto do Brilho muda?

Conclusão

Você aprendeu a como criar um Material, especificar suas propriedades, e usá-lo em uma Geometria (Geometry). Você sabe como carregar um arquivo de imagem (.png, .jpg) como textura em um material. Você sabe como salvar arquivos de textura em uma subpasta do diretório assets/Textures/ de seu projeto.

Você também aprendeu que um material pode ser armazenado em um arquivo .j3m. O arquivo referência uma Definição de Material (MaterialDefinition) embutida e especifica valores para propriedades daquela Definição de Material (MaterialDefinition). Você sabe salvar seus arquivos .j3m customizados em seu diretório assets/Materials/ do seu projeto.

Agora que você sabe como carregar modelos e como atribuir materiais de boa aparência para eles, vamos dar uma olhada em como animar modelos no próximo capítulo Hello Animation.

Veja também

  • Como Usar Materiais
  • Edição de Material
  • Thread de fórum de Materiais
  • Documentação de Materiais jME3 (PDF)
  • Vídeo Tutorial: Edição e Atribuição de Materiais para Modelos na SDK de jMonkeyEngine
  • Criando texturas em Blender
  • Vários instantâneos de Material (Não prontos com JME3, isto é apenas para mostrar o fantástico alcance de parâmetros Material nas mãos de um expert, até que nós tenhamos uma demo JME3 para ele.)

jMonkeyEngine 3 Tutorial (7) - Hello Animation

Anterior: Hello Material, Próximo: Hello Picking

Este tutorial mostra como adicionar um controlador de animação e canais, e como responder a entrada do usuário por disparar uma animação em um modelo carregado.

Para usar os ativos de exemplo em um novo projeto do SDK da jMonkeyEngine, dê um clique com o botão direito em seu projeto, selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código da Amostra

package jme3test.helloworld;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;

/** Sample 7 - how to load an OgreXML model and play an animation,
 * using channels, a controller, and an AnimEventListener. */
public class HelloAnimation extends SimpleApplication
  implements AnimEventListener {
  private AnimChannel channel;
  private AnimControl control;
  Node player;
  public static void main(String[] args) {
    HelloAnimation app = new HelloAnimation();
    app.start();
  }

  @Override
  public void simpleInitApp() {
    viewPort.setBackgroundColor(ColorRGBA.LightGray);
    initKeys();
    DirectionalLight dl = new DirectionalLight();
    dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
    rootNode.addLight(dl);
    player = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");
    player.setLocalScale(0.5f);
    rootNode.attachChild(player);
    control = player.getControl(AnimControl.class);
    control.addListener(this);
    channel = control.createChannel();
    channel.setAnim("stand");
  }

  public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
    if (animName.equals("Walk")) {
      channel.setAnim("stand", 0.50f);
      channel.setLoopMode(LoopMode.DontLoop);
      channel.setSpeed(1f);
    }
  }

  public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
    // unused
  }

  /** Custom Keybinding: Map named actions to inputs. */
  private void initKeys() {
    inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(actionListener, "Walk");
  }
  private ActionListener actionListener = new ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Walk") && !keyPressed) {
        if (!channel.getAnimationName().equals("Walk")) {
          channel.setAnim("Walk", 0.50f);
          channel.setLoopMode(LoopMode.Loop);
        }
      }
    }
  };
}

Criando e Carregando Modelos Animados

Você cria modelos animados com uma ferramenta tal como o Blender. Tome algum tempo e aprenda a como criar seus próprios modelos nestes Tutoriais de Animação do Blender. Por enquanto, baixe e use um modelo gratuito, tal como o incluso aqui como exemplo (Oto Golem, and Ninja).

Carregar um modelo animado é muito simples, como você aprendeu nos capítulos anteriores. Modelos Ogre Animados vêm com um conjunto de arquivos: O modelo está em Oto.mesh.xml, e os detalhes de animação estão em Oto.skeleton.xml, mais os arquivos usuais para materiais e texturas. Cheque que todos os arquivos estão juntos no mesmo subdiretório Model.

  public void simpleInitApp() {
    /* Displaying the model requires a light source */
    DirectionalLight dl = new DirectionalLight();
    dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
    rootNode.addLight(dl);
    /* load and attach the model as usual */
    player = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
    player.setLocalScale(0.5f); // resize
    rootNode.attachChild(player);
    ...
    }

Não esqueça de adicionar uma fonte de luz para fazer o material visível.

Controlador de Animação e Canal

Depois de carregar o modelo animado, você o registra para o Controlador de Animação (Animation Controller).

  • o objeto controlador dá a você acesso às sequências de animação disponíveis.
  • O controlador pode ter vários canais, cada canal pode executar uma sequência de animação por vez.
  • Para executar várias sequências, você cria vários canais, e configura cada um deles para a animação deles.
  private AnimChannel channel;
  private AnimControl control;

  public void simpleInitApp() {
    ...
    /* Load the animation controls, listen to animation events,
     * create an animation channel, and bring the model in its default position.  */
    control = player.getControl(AnimControl.class);
    control.addListener(this);
    channel = control.createChannel();
    channel.setAnim("stand");
    ...

Em resposta a uma questão sobre animações em diferentes canais interferindo uma com a outra, , Nehon no fórum da jME escreveu,

"Você tem de considerar canais como parte do esqueleto que é animado. O comportamento padrão é o uso do esqueleto inteiro para um canal. No seu exemplo o primeiro canal toca a animação de caminhar, então o segundo canal toca a animação de se esquivar. Braços e pés não são provavelmente afetados pela animação de aresta então você pode ver a animação de caminhar para eles, mas o teso do cor reproduz a animação de se esquivar.

Normalmente múltiplos canais são usados para animar diferentes partes do corpo. Por exemplo, você cria um canal para a parte inferior do corpo e um para a parte superior. Isto permite você reproduzir uma animação de caminhar com a parte inferior e, por exemplo, uma animação de atirar com a parte superior. Desta maneira seu personagem pode caminhar enquanto atirando.

No seu caso, onde você quer animações encadearem o esqueleto inteiro, você tem de usar um canal."

Respondendo para Eventos de Animação

Adicione implements AnimEventListener para a declaração de classe. Esta interface dá a você acesso aos eventos que notificam quando uma sequência termina, ou quando você muda de uma sequência para outra, então você pode responder para isso. Neste exemplo, você reinicia o personagem para uma posição de permanecer em pé depois que um ciclo de Caminhar (Walk) termina.

public class HelloAnimation extends SimpleApplication
                         implements AnimEventListener {
  ...

  public void onAnimCycleDone(AnimControl control, 
                              AnimChannel channel, String animName) {
    if (animName.equals("Walk")) {
      channel.setAnim("stand", 0.50f);
      channel.setLoopMode(LoopMode.DontLoop);
      channel.setSpeed(1f);
    }
  }
  public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
    // não usado
  }

Dispare Animações Depois da Entrada do Usuário

Há animações ambiente como animais ou árvores que você quer ativar no loop de evento principal. Em outros casos, animações são ativadas por interação do usuário, como por exemplo uma entrada advinda de uma tecla. Você quer reproduzir a animação de Caminhar (Walk) quando o jogador pressiona uma tecla determinada 9aqui a barra de espaço), ao mesmo tempo que o avatar realiza a ação de caminhar e muda sua localização.

  • Inicialize um novo controlador de entrada (em simpleInitApp()).
    • Escreva o método de conveniência initKey() e chame ele de simpleInitApp().
  • Adicione um mapeamento de tecla com o nome da ação que você quer ativar.
    • Aqui, por exemplo, você mapeia Caminhar (Walk) para a tecla Barra de Espaço.
  • Adicione um ouvinte de entrada (input listener) para a ação Caminhar (Walk).
  private void initKeys() {
    inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(actionListener, "Walk");
  }

Para usar o controlador de entrada, você precisa implementar o ouvinte de ação (actionListener): Teste por cada ação pelo nome, e configure o canal para que a animação correspondente execute.

  • O segundo parâmetro de setAnim() é o tempo de mistura (blendTime) (quanto tempo a animação atual deveria se sobrepor com a última).
  • Modo de Repetição (LoopMode) pode ser Loop (repetir), Ciclo (Cycle) (para frente e então para trás), e Não repita (DontLoop) (somente uma vez).
  • Se necessário use channel.setSpeed() para configurar a velocidade desta animação.
  • Opcionalmente use channel.setTime() para Avançar rápido (Fast-forward) ou retroceder (rewind) para um certomomento no tempo desta animação.
  private ActionListener actionListener = new ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
        if (name.equals("Walk") && !keyPressed) {
            if (!channel.getAnimationName().equals("Walk")){
                channel.setAnim("Walk", 0.50f);
                channel.setLoopMode(LoopMode.Cycle);
            }
        }
    }
  };

Exercícios

Exercício 1: Duas Animações

Faça um clique de mouse disparar uma outra sequência de animação!

  • Crie um segundo canal no controlador
  • Crie um novo mapeamento de disparo de tecla e ação (veja: Hello Input)
  • Dica: Você quer descobrir que sequências de animação estão disponíveis no modelo? Use:
    for (String anim : control.getAnimationNames()) { System.out.println(anim); }

Exercício 2: Revelando o Esqueleto (Skeleton) (1)

Abra o arquivo skeleton.xml em um editor de texto de sua escolha. Você não tem de ser capaz de ler ou escrever estes arquivos xml (Blender faz isto por você) – mas é bom saber como esqueletos funcionam. "Não há mágica para isso!"

  • Note como os ossos são numerados e nomeados. Todos os nomes de um modelo aniamdo seguem um esquema de nomeação.
  • Note que a hierarquia de osso especifica como os ossos são conectados.
  • Note a lista de animações: Cada animação tem um nome, e várias trilhas. Cada trilha diz a ossos individuais como e quando transformar. Estes passos de animação são chamados quadros-chave.

Exercício 3: Revelando o Esqueleto (Skeleton) (2)

Adicione as seguintes declarações import para as classes SkeletonDebugger e Material:

     import com.jme3.scene.debug.SkeletonDebugger;
     import com.jme3.material.Material;

Adicione o seguinte trecho de código para simpleInitApp() para fazer os ossos (que você acabou de ler) visíveis!

     SkeletonDebugger skeletonDebug = 
         new SkeletonDebugger("skeleton", control.getSkeleton());
     Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
     mat.setColor("Color", ColorRGBA.Green);
     mat.getAdditionalRenderState().setDepthTest(false);
     skeletonDebug.setMaterial(mat);
     player.attachChild(skeletonDebug);

Você pode identificar ossos individuais no esqueleto?

Conclusão

Agora você pode carregar modelos animados, identificar animações armazenadas, e disparar animações por usar onAnimCycleDone() e onAnimChange(). Você também aprendeu que você pode reproduzir várias animações simultaneamente, por iniciar cada em um canal de si própria. Isto poderia ser útil se você mesmo quer animar as partes superioras e inferioras dos personagens independentemente, por exemplo, as pernas correrem, enquanto os braços usam uma arma.

Agora que seu personagem pode caminhar, não seria legal se ele também pudesse pegar coisas, ou mirasse uma arma em coisas, ou abrisse portas? Tempo para revelar os secretos de pega com o mouse!

Veja também: Ctiando modelos OgreXML animados no Blender

jMonkeyEngine 3 Tutorial (8) - Hello Picking

Anterior: Hello Animation, Próximo: Hello Collision

Interações típicas em jogos incluem disparar, pegar objetos, e abrir portas. De um ponto de vista de implementação, estas interações aparentemente diferentes são surepreendentemente similares: O usuário primeiro mira e seleciona um alvo na cena 3D, e então ativa uma ação nele. Nós chamamos este processo de pega.

Você pode pegar alguma coisa por tanto pressionar uma tecla no teclado, ou por clicar com o mouse. Em ambos os casos você identifica a mira por mirar um raio - uma linha reta - dentro da cena. Este método de implementar pega é chamado invocação de raio (ray casting) (que não é o mesmo que ray tracing).

Este tutorial depende no que você aprendey no tutorial Hello Input. Você acha mais amostras de código relacionadas sob Pega de Mouse (Mouse Picking) e Colisão e Intersecção (Collision and Intersection).

Código da Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;

/** Sample 8 - how to let the user pick (select) objects in the scene 
 * using the mouse or key presses. Can be used for shooting, opening doors, etc. */
public class HelloPicking extends SimpleApplication {

  public static void main(String[] args) {
    HelloPicking app = new HelloPicking();
    app.start();
  }
  Node shootables;
  Geometry mark;

  @Override
  public void simpleInitApp() {
    initCrossHairs(); // a "+" in the middle of the screen to help aiming
    initKeys();       // load custom key mappings
    initMark();       // a red sphere to mark the hit

    /** create four colored boxes and a floor to shoot at: */
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
    shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
    shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
    shootables.attachChild(makeFloor());
  }

  /** Declaring the "Shoot" action and mapping to its triggers. */
  private void initKeys() {
    inputManager.addMapping("Shoot",
      new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar
      new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); // trigger 2: left-button click
    inputManager.addListener(actionListener, "Shoot");
  }
  /** Defining the "Shoot" action: Determine what was hit and how to respond. */
  private ActionListener actionListener = new ActionListener() {

    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Reset results list.
        CollisionResults results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Collect intersections between Ray and Shootables in results list.
        shootables.collideWith(ray, results);
        // 4. Print the results
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // For each hit, we know distance, impact point, name of geometry.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0) {
          // The closest collision point is what was truly hit:
          CollisionResult closest = results.getClosestCollision();
          // Let's interact - we mark the hit with a red dot.
          mark.setLocalTranslation(closest.getContactPoint());
          rootNode.attachChild(mark);
        } else {
          // No hits? Then remove the red mark.
          rootNode.detachChild(mark);
        }
      }
    }
  };

  /** A cube object for target practice */
  protected Geometry makeCube(String name, float x, float y, float z) {
    Box box = new Box(new Vector3f(x, y, z), 1, 1, 1);
    Geometry cube = new Geometry(name, box);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.randomColor());
    cube.setMaterial(mat1);
    return cube;
  }

  /** A floor to show that the "shot" can go through several objects. */
  protected Geometry makeFloor() {
    Box box = new Box(new Vector3f(0, -4, -5), 15, .2f, 15);
    Geometry floor = new Geometry("the Floor", box);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.Gray);
    floor.setMaterial(mat1);
    return floor;
  }

  /** A red ball that marks the last spot that was "hit" by the "shot". */
  protected void initMark() {
    Sphere sphere = new Sphere(30, 30, 0.2f);
    mark = new Geometry("BOOM!", sphere);
    Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mark_mat.setColor("Color", ColorRGBA.Red);
    mark.setMaterial(mark_mat);
  }

  /** A centred plus sign to help the player aim. */
  protected void initCrossHairs() {
    guiNode.detachAllChildren();
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    BitmapText ch = new BitmapText(guiFont, false);
    ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
    ch.setText("+"); // crosshairs
    ch.setLocalTranslation( // center
      settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
      settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
    guiNode.attachChild(ch);
  }
}

Você deveria ver quatro cubos coloridos flutuando sobre um chão cinza, e miras de cruz (cross-hairs). Aponte nas miras de cruz e clique, ou pressione a barra de espaço para aturar. O lugar atingido é marcado com um ponto vermelho.

Mantenha um olho no fluxo de saída da aplicação, ele dará a você mais detalhes: O nome da malha que foi atingida, as coordenadas da colisão, e a distância.

Entendendo os Métodos Auxiliares

Os métodos makeCube(), makeFloor(), initMark(), e initCrossHairs, são métodos auxiliares personalizados. Nós chamamos eles de simpleInitApp() para inicializar o grafo de cena com conteúdo de amostra.

  • makeCube() cria caixas coloridas simples para "prática de alvo".
  • makeFloor() cria um nó de chão cinza para "prática de alvo".
  • initMark() cria uma esfera vermelha ("marca"). Nós usaremos ela mais tarde para marcar o ponto que foi atingido.
    • Note que a marca não está anexada e portanto não esá visível no início!
  • initCrossHairs() cria simples miras de cruzes por imprimir um sinal de "+" no meio da tela.
    • Note que as miras de cruzes estão ligadas no nó da GUI (guiNode), e não ao nó raiz (rootNode).

Neste exemplo nós vinculamos todos os objetos "disparáveis" para um nó customizado, Shootables. Isto é uma otimização então o motor somente tem de calcular intersecções com objetos que nós estamos na verdade interessados. O nó Shootables é ligado ao nó raiz (rootNode) como usual.

Entendendo Inovação de Raio (Ray Casting) Para Teste de Colisão

Nosso objetivo é determinar em qual caixa o usuário "disparou" (pegou/escolheu). Em geral, nós queremos determinar qual malha o usuário selecionou por mirar a mira de cruz nela. Matematicamente, nós desenhamos uma linha da câmera e vemos se ela intersecta com objetos na cena 3D scene. Esta linha é chamado um raio.

Aqui está nosso algoritmo simples de invocar raio para pegar objetos:

  • Reinicie a lista de resultados.
  • Invoque um raio da localização da câmera para a direção que a cãmera se encontra.
  • Colete todas as intersecções entre os nós de raio e Shootable na lista de resultados.
  • Use a lista de resultados para determinar o que foi atingido:
    • Para cada colisão, JME relata sua distância da câmera, ponto de impacto, e o nome da malha.
    • Organize ps resultados pela distância
    • Pegue o resultado mais próximo, isto é, a malha que foi atingida.

Implementando o Teste de Colisão

Carregando a cena

Primeiro inicialize alguns nós disparáveis (shootable) e anexe ele para a cena. Você usará o objeto da marca depois.

  Node shootables;
  Geometry mark;

  @Override
  public void simpleInitApp() {
    initCrossHairs();
    initKeys();
    initMark();

    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon",    -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can",    1f,-2f, 0f));
    shootables.attachChild(makeCube("the Sheriff",  0f, 1f,-2f));
    shootables.attachChild(makeCube("the Deputy",   1f, 0f, -4));
    shootables.attachChild(makeFloor());
  }

Configurando o Ouvinte de Entrada (Input Listener)

Depois você declara a ação de disparo. Ela pode ser disparada por clicar, ou por pressionar a barra de espaço. O método initKeys() é chamado de simpleInitApp() para configurar estes mapeamentos de entrada.

  /** Declaring the "Shoot" action and its triggers. */
  private void initKeys() {
    inputManager.addMapping("Shoot",      // Declare...
      new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar, or
      new MouseButtonTrigger(0));         // trigger 2: left-button click
    inputManager.addListener(actionListener, "Shoot"); // ... and add.
  }

Ação de Pega Usando Miras de Cruzes

Em seguida nós implementamos o Ouvinte de Ação (ActionListener) que responde ao gatilho de Disparar (Shoot) com uma ação. A ação segue o algoritmo de invocar raio descrito acima:

  • Para todo clique ou pressionamento da barra de espaço, a ação Disparar (Shoot) é ativada.
  • A ação invoca um raio para frente e determina intersecções com objetos disparáveis (= invocação de raio (ray casting)).
  • Para qualquer alvo que foi atingido, ele imprime o nome, distância e coordenadas da colisão.
  • Finalmente ele anexa uma marca vermelha para o resultado mais próximo, para destacar o ponto que na verdade foi atingido.
  • Quando nada foi atingido, a lista de resultados é vazia, e a marca vermelha é removida.

Note como isso imprime muita saída para mostrar a você colisões que foram registradas.

  /** Defining the "Shoot" action: Determine what was hit and how to respond. */
  private ActionListener actionListener = new ActionListener() {
    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Reset results list.
        CollisionResults results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Collect intersections between Ray and Shootables in results list.
        shootables.collideWith(ray, results);
        // 4. Print results.
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // For each hit, we know distance, impact point, name of geometry.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0){
          // The closest collision point is what was truly hit:
          CollisionResult closest = results.getClosestCollision();
          mark.setLocalTranslation(closest.getContactPoint());
          // Let's interact - we mark the hit with a red dot.
          rootNode.attachChild(mark);
        } else {
        // No hits? Then remove the red mark.
          rootNode.detachChild(mark);
        }
      }
    }
  };

Dica: Note como você usa o método fornecido results.getClosestCollision().getContactPoint() para determinar a localização de colisão mais próxima. Se seu jogo inclue uma "arma" ou "feitiço" que pode atingir múltiplos alvos, você poderia também repetir sobre a lista de resultados, e interagir com cada um deles.

Ação de Pega Usando o Ponteiro do Mouse

O exemplo acima assume que o jogador está apontando em miras de cruz (anexadas para o centro da tela) no alvo. Mas você pode trocar o código de pega para permitir que você livremente clique nos objetos com um ponteiro de mouse visível. Para poder fazer isto, você tem de converter as coordenadas de tela 2d do clique para coordenadas do mundo 3D para obter o ponto de início do raio de pega.

  • Reinicie a lista de resultados.
  • Obtenha coordenadas de clique 2D.
  • Converta coordenadas de tela 2D para o equivalente 3D deles.
  • Mire o raio da localização 3D clicada para frente na cena.
  • Colete intersecções entre o raio e todos os nós em uma lista de resultados.
...
CollisionResults results = new CollisionResults();
Vector2f click2d = inputManager.getCursorPosition();
Vector3f click3d = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 0f).clone();
Vector3f dir = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
Ray ray = new Ray(click3d, dir);
shootables.collideWith(ray, results);
...

Use isto junto com inputManager.setCursorVisible(true) para ter certeza que o cursor está visível.

Note que desde que você usa o mouse para pega, você não pode mais usá-lo para rotacionar a câmera. Se você quer ter um ponteiro de mouse visível em seu jogo, você tem de redefinir os mapeamentos de rotação da câmera.

Exercícios

Depois que uma colisão foi registrada, o objeto mais próximo é indentificado com o alvo, e marcado com um ponto vermelho. Modifique a amostra de código para resolver estes exercícios:

Exercício 1: Feitiço Mágico

Mude a cor do alvo clicado mais próximo!
Aqui estão algumas dicas:

  • Vá para a linha onde a mira mais próxima é identificada, e adicione suas mudanças depois dela.
  • Para mudar a cor de um objeto, você deve primeiro conhecer sua Geometria (Geometry). Identifique o nó por identificar o nome do alvo.
    • Use Geometry g = closest.getGeometry();
  • Crie um novo material de cor e configure o Material do nó para esta cor.
    • Olhe dentro do método makeCube() para um exemplo de como configurar cores aleatórias.

Exercício 2: Atire em um Personagem

Disparar em caixas não é muito excitante - você pode adicionar código que carrega e posiciona um modelo na cena, e disparar nele?

  • Dica> você pode usar Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); de jme3-test-data.jar do motor.
  • Dica: Modelos são tonalizados! Você precisa de alguma luz!

Exercício 3: Pega dentro de inventário

Mude o código como se segue para simular o jogador pegando objetos dentro do inventário: Quando você clicar uma vez, a mira mais próxima é identificada e retirada da cena. Quando você clica uma segunda vez, a mira é reanexada no local que você clicou. Aqui estão algumas dicas:

  • Crie um nó inventário e armazene os nós retirados temporariamente.
  • O nó inventário não é anexado para o nó raiz (rootNode).
  • Você pode fazer o inventário visível por anexar o nó inventário no nó da GUI (guiNode) (que anexa ele para o HUD). Fique atento às seguintes armadilhas:
    • se seus nós usam um Material iluminado (não "Unshaded.j3md"), também adicione uma luz para o nó da GUI (guiNode).
    • Unidades de tamanho são pixels no HUD, portanto um cubo de a 2-wu (unidades do mundo) é exeibido somente 2 pixels de comprimento no HUD. – Aumente seu tamanho (Escalone ele)!
    • Posicione os nós: o canto da esquerda-fundo do HUD é (0f,0f), e o canto da direita topo está em (settings.getWidth(),settings.getHeight()).

Link para as soluções propostas por usuários: http://jmonkeyengine.org/wiki/doku.php/jm3:solutions Tenha certeza de tentar resolvê-las por si só primeiro!

Conclusão

Você aprendeu a como usar invocação de raio para resolver a tarefa de determinar qual objeto um usuário selecionou na tela. Você aprendeu que isto pode ser usado para uma variedade de interações, tais como diapro, abertura, pega e derrubar itens, pressionando um botão ou alavanca, etc.

Use sua imaginação daqui:

No seu jogo, o clique pode disparar qualquer ação na Geometria (Geometry) identificada: Desvincule ele e coloque ele no inventário, anexe algo a ele, dispare uma animação ou efeito, abra uma porta ou baú, – etc.
No seu jogo, você poderia substituit a marca vermelha com um emissor de partícula e adicionar um efeito de explosão, tocar um som, calcular a nova pontuação depois de cada colisão dependendo o que foi atingido – etc.

Agora, não seria legal se aqueles alvos e o chão fossem objetos sólidos e você pudesse caminhar ao redor deles? Vamos continuar a aprender sobre Detecção de Colisão.

Veja também

  • Hello Input
  • Mouse Picking
  • Collision and Intersection

jMonkeyEngine 3 Tutorial (9) - Hello Collision

Anterior: Hello Picking, Próximo: Hello Terrain

Este tutorial demonstra como carregar um modelo de cena e dar a ele chãos e paredes sólidos para caminhar. Você usa um Controle de Corpo Rígido (RigidBodyControl) para a cena estática colidível, e um Controle de Personagem (CharacterControl) para o personagem móvel em primeira pessoa. Você também aprende a como configurar a câmera padrão de primeira pessoa para trabalhar com navegação controlada por física. Você pode usar a solução mostrada aqui para atiradores em primeira pessoa, labirintos e jogos similares.

Código de Amostra

Se você não tem ainda, faça download da cena de amostra town.zip.

jMonkeyProjects$ ls -1 BasicGame
assets/
build.xml
town.zip
src/

Coloque town.zip no diretório raiz do seu projeto JME3. Aqui está o código:

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.ZipLocator;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

/**
 * Example 9 - How to make walls and floors solid.
 * This collision code uses Physics and a custom Action Listener.
 * @author normen, with edits by Zathras
 */
public class HelloCollision extends SimpleApplication
        implements ActionListener {

  private Spatial sceneModel;
  private BulletAppState bulletAppState;
  private RigidBodyControl landscape;
  private CharacterControl player;
  private Vector3f walkDirection = new Vector3f();
  private boolean left = false, right = false, up = false, down = false;

  public static void main(String[] args) {
    HelloCollision app = new HelloCollision();
    app.start();
  }

  public void simpleInitApp() {
    /** Set up Physics */
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    //bulletAppState.getPhysicsSpace().enableDebug(assetManager);

    // We re-use the flyby camera for rotation, while positioning is handled by physics
    viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));
    flyCam.setMoveSpeed(100);
    setUpKeys();
    setUpLight();

    // We load the scene from the zip file and adjust its size.
    assetManager.registerLocator("town.zip", ZipLocator.class);
    sceneModel = assetManager.loadModel("main.scene");
    sceneModel.setLocalScale(2f);

    // We set up collision detection for the scene by creating a
    // compound collision shape and a static RigidBodyControl with mass zero.
    CollisionShape sceneShape =
            CollisionShapeFactory.createMeshShape((Node) sceneModel);
    landscape = new RigidBodyControl(sceneShape, 0);
    sceneModel.addControl(landscape);

    // We set up collision detection for the player by creating
    // a capsule collision shape and a CharacterControl.
    // The CharacterControl offers extra settings for
    // size, stepheight, jumping, falling, and gravity.
    // We also put the player in its starting position.
    CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1);
    player = new CharacterControl(capsuleShape, 0.05f);
    player.setJumpSpeed(20);
    player.setFallSpeed(30);
    player.setGravity(30);
    player.setPhysicsLocation(new Vector3f(0, 10, 0));

    // We attach the scene and the player to the rootNode and the physics space,
    // to make them appear in the game world.
    rootNode.attachChild(sceneModel);
    bulletAppState.getPhysicsSpace().add(landscape);
    bulletAppState.getPhysicsSpace().add(player);
  }

  private void setUpLight() {
    // We add light so we see the scene
    AmbientLight al = new AmbientLight();
    al.setColor(ColorRGBA.White.mult(1.3f));
    rootNode.addLight(al);

    DirectionalLight dl = new DirectionalLight();
    dl.setColor(ColorRGBA.White);
    dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal());
    rootNode.addLight(dl);
  }

  /** We over-write some navigational key mappings here, so we can
   * add physics-controlled walking and jumping: */
  private void setUpKeys() {
    inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(this, "Left");
    inputManager.addListener(this, "Right");
    inputManager.addListener(this, "Up");
    inputManager.addListener(this, "Down");
    inputManager.addListener(this, "Jump");
  }

  /** These are our custom actions triggered by key presses.
   * We do not walk yet, we just keep track of the direction the user pressed. */
  public void onAction(String binding, boolean value, float tpf) {
    if (binding.equals("Left")) {
      left = value;
    } else if (binding.equals("Right")) {
      right = value;
    } else if (binding.equals("Up")) {
      up = value;
    } else if (binding.equals("Down")) {
      down = value;
    } else if (binding.equals("Jump")) {
      player.jump();
    }
  }

  /**
   * This is the main event loop--walking happens here.
   * We check in which direction the player is walking by interpreting
   * the camera direction forward (camDir) and to the side (camLeft).
   * The setWalkDirection() command is what lets a physics-controlled player walk.
   * We also make sure here that the camera moves with player.
   */
  @Override
  public void simpleUpdate(float tpf) {
    Vector3f camDir = cam.getDirection().clone().multLocal(0.6f);
    Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f);
    walkDirection.set(0, 0, 0);
    if (left)  { walkDirection.addLocal(camLeft); }
    if (right) { walkDirection.addLocal(camLeft.negate()); }
    if (up)    { walkDirection.addLocal(camDir); }
    if (down)  { walkDirection.addLocal(camDir.negate()); }
    player.setWalkDirection(walkDirection);
    cam.setLocation(player.getPhysicsLocation());
  }
}

Execute a amostra. Você deveria ver uma cidade quadrada com casas e um monumento. Use as teclas WASD e o mouse para navegar ao redor com uma perspectiva em primeira pessoa. Corra para frente e pule por pressionar W e Espaço. Note como você dá um passo sobre a calçada, e acima para o monumento. Você pode caminhar nos corredores entre as casas, mas as paredes são sólidas. Não caminhe sobre a aresta do mundo! :-)

Entendendo o Código

Vamos iniciar com a declaração de classe:

public class HelloCollision extends SimpleApplication
        implements ActionListener { ... }

Você já sabe que SimpleApplication é a classe base para todos os jogos jME3. Você faz esta classe implementar a interface ActionListener porque você quer customizar as entradas navegacionais mais tarde.

  private Spatial sceneModel;
  private BulletAppState bulletAppState;
  private RigidBodyControl landscape;
  private CharacterControl player;
  private Vector3f walkDirection = new Vector3f();
  private boolean left = false, right = false, up = false, down = false;

Você inicializa alguns campos privados:

  • O BulletAppState dá este acesso a SimpleApplication para características físicas (tais como detecção de colisão) fornecida pela integração jBullet da jME3
  • O Espacial (Spatial) sceneModel é para carregar um modelo OgreXML de uma cidade.
  • Você precisa de um Controle de Corpo Rígido (RigidBodyControl) para fazer o modelo da cidade sólido.
  • O jogador em primeira pessoa (invisível) é representado por um objeto Controle de Personagem (CharacterControl).
  • Os campos walkDirection e os quatro Booleans são usados para naegação controlada por física.

Vamos dar uma olhada em todos os detalhes:

Inicializando o Jogo

Como normal você inicializa o jogo no método simpleInitApp().

    viewPort.setBackgroundColor(new ColorRGBA(0.7f,0.8f,1f,1f));
    flyCam.setMoveSpeed(100);
    setUpKeys();
    setUpLight();
  • Você configura a cor de plano de fundo para azul claro desde esta é uma cena com um céu.
  • Você refaz o propósito do controle de câmera padrão "flyCam" como uma câmera de primeira pessoa e configura sua velocidade.
  • O método auxiliar setUpLights() adiciona suas fontes de luz.
  • O método auxiliar setUpKeys() configura mapeamentos de entrada–nós olharemos nisso mais tarde.

A Cena Controlada por Física

A primeira coisa que você faz em todo jogo que usa física é criar um objeto BulletAppState. Ele dá a você acesso para a integração jBullet da jME3 que manuseia forças físicas e colisões.

    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);

Para a cena você carrega o Modelo da cena (sceneModel) de um arquivo zip, e ajusta o tamanho.

    assetManager.registerLocator("town.zip", ZipLocator.class);
    sceneModel = assetManager.loadModel("main.scene");
    sceneModel.setLocalScale(2f);

O arquivo town.zip é incluso como um modelo de amostra nos códigos-fonte da JME3 – você pode baixá-lo aqui. (Opcionalmente, use qualquer cena OgreXML de si próprio.) Para este exemplo, coloque o arquivo zip no diretório topo de nível da aplicação (isto é, próximo a src/, assets/, build.xml).

    CollisionShape sceneShape =
      CollisionShapeFactory.createMeshShape((Node) sceneModel);
    landscape = new RigidBodyControl(sceneShape, 0);
    sceneModel.addControl(landscape);
    rootNode.attachChild(sceneModel);

Para usar detecção de colisão, você adiciona um Controle de Corpo Rígido (RigidBodyControl) para o Espacial (Spatial) sceneModel. O Controle de Corpo Rígido (RigidBodyControl) para um modelo complexo leva dois argumentos: Uma Forma (Shape) de Colisão (Collision), e a massa do objeto.

  • JME3 oferece CollisionShapeFactory que pré-calcula uma forma de malha precisa para colisão para um Espacial (Spatial). Você escolhe gerar uma CompoundCollisionShape (que tem MeshCollisionShapes como suas crianças) porque este tipo de forma de colisão é ótima para objetos imutáveis, tais como terreno, casas, e níveis de atirador inteiros.
  • Você configura a massa para zero desde que a cena é estática e sua massa é irrelevante.
  • Adicione o controle para o Espacial (Spatial) para dar a ele propriedades físicas.
  • Como sempre, anexe o sceneModel para o nó raiz (rootNode) para fazê-lo visível.

Dica: lembre-se de adicionar uma fonte de luz para que você possa ver a cena.

O Jogador COntrolado por Física

Um jogador de primeira pessoa é tipicamente invisível. Quando você usa a câmera flyCam padrão como câmera em primeira pessoa, ela nem mesmo testa para colisão e atravessa paredes. Isto é porque o controle flyCam não tem qualquer forma física atribuída. Nesta amostra de código você representa o jogador em primeira pessoa como uma forma física (invisível). Você usa as teclas WASD para dirigir esta forma física ao redor, enquanto que o motor físico gerencia para você como ele anda ao longo de paredes sólidas e em chãos sólidos e pula sobre obstáculos sólidos. Então você simplesmente faz a câmera seguir a localização da forma caminhante – e você consegue a ilusão de ser um corpo físico em um ambiente sólido vendo através da câmera.

Então vamos configurar detecção de colisão para o jogador em primeira pessoa.

    CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1);

Novamente você cria uma Forma de Colisão (CollisionShape): Desta vez você escolhe uma Forma de Colisão Cápsula (CapsuleCollisionShape), um cilindro com o topo e o fundo arredondados. Esta forma é ótima para uma pessoa. Ela é alta e a parte redonda ajuda a ficar atolado menos frequentemente nos obstáculos.

  • Fornece ao construtor da Forma de Colisão de Cápsula (CapsuleCollisionShape) com o raio desejado e altura da cápsula de limitação para adequar a forma de seu personagem. Neste exemplo o personagem é 2*1.5f unidades comprido, e 6f unidades alto.
  • O argumento inteiro final especifica a orientação do cilindro: 1 é o eixo Y, que se adequa a uma pessoa ereta. Para animais que são mais longos que altos você usaria 0 or 2 (dependendo de como ele está rotacionado).
    player = new CharacterControl(capsuleShape, 0.05f);

"Aquela Forma de Colisão (CollisionShape) me faz parecer gordo?" Se você mesmo obtém um comportamento físico confuso, lembre-se de dar uma olhada nas formas de colisão. Adicione a seguinte linha depois da inciialização de bulletAppState para fazer as formas visíveis:

bulletAppState.getPhysicsSpace().enableDebug(assetManager);

Agora você usa a Forma de Colisão (CollisionShape) para criar um Controle de Personagem (CharacterControl) que representa o jogador em primeira pessoa. O último argumento do construtor CharacterControl (aqui .05f) é o tamanho de um passo que o personagem deveria ser capaz de realizar.

    player.setJumpSpeed(20);
    player.setFallSpeed(30);
    player.setGravity(30);

Aparte da altura do passo e do tamanho do personagem, o Controle de Personagem (CharacterControl) deixa você configurar pulo, queda, e velocidades de gravidade. Ajuste os valores para se adequar a situação de seu jogo.

    player.setPhysicsLocation(new Vector3f(0, 10, 0));

Finalmente nós colocamos o jogador em sua posição de início e atualizamos seu estado – lembre-se de usar setPhysicsLocation() ao invés de setLocalTranslation() agora, desde que você está lidando com um objeto físico.

Espaço Físico (PhysicsSpace)

Lembre-se que em um jogo físico você dev registrar todos os objetos sólidos (normalmente os personagens e a cena) para o Espaço Físico (PhysicsSpace)!

    bulletAppState.getPhysicsSpace().add(landscape);
    bulletAppState.getPhysicsSpace().add(player);

O corpo invisível do personagem se situa no chão físico. Ele não pode caminhar ainda – você lidará com isso depois.

Navegação

O controlador de câmera padrao é uma câmera em terceira pessoa. JME3 também oferece um controlador de câmera de primeira pessoa, flyCam, que nós usamos aqui para manusear a rotação da câmera. O controle flyCam move a câmera usando setLocation().

Entretanto, você deve redefinir como a caminhada (movimento da câmera) é manusada para objetos controlados por física: Quando você navega em um nó não físico (e.g. a padrão flyCam), você simplesmente especifica a localização do alvo. Não há testes que previnem a flyCam de ficar atolada em uma parede! Quando você move um Controle Físico (PhysicsControl), você deseja especificar uma direção de caminhada ao invés disso. Então o Espaço Físico (PhysicsSpace) pode calcular para você quão distante o personagem pode na verdade se mover na direção desejada – ou se um obstáculo previne ele de ir amis adiante.

Em breve, você deve redefinir os mapeamentos de tecla regionais da flyCam para usar setWalkDirection() ao invés de setLocalTranslation(). Aqui estão os passos:

1. inputManager

No método simpleInitApp() você reconfigura as entradas familiares WASD para caminhar, e a Barra de Espaço para pular.

private void setUpKeys() {
    inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(this, "Left");
    inputManager.addListener(this, "Right");
    inputManager.addListener(this, "Up");
    inputManager.addListener(this, "Down");
    inputManager.addListener(this, "Jump");
}

Você pode mover este bloco de código em um método auxiliar setupKeys() e chamar este método de simpleInitApp()– para manter o código mais legível.

2. onAction()

Lembre-se que esta classe implementa a interface ActionListener, então você pode customizar as entradas da flyCam. A interafce ActionListener requer que você implemente o método onAction(): Você redefine as ações disparadas por pressionamentos de tecla de navegação para trabalhar com física.

  public void onAction(String binding, boolean value, float tpf) {
    if (binding.equals("Left")) {
      if (value) { left = true; } else { left = false; }
    } else if (binding.equals("Right")) {
      if (value) { right = true; } else { right = false; }
    } else if (binding.equals("Up")) {
      if (value) { up = true; } else { up = false; }
    } else if (binding.equals("Down")) {
      if (value) { down = true; } else { down = false; }
    } else if (binding.equals("Jump")) {
      player.jump();
    }
  }

O único movimento que você não tem de implementar por você mesmo é a ação de pular. A chamada a player.jump() é um método especial que lida com um movimento de pulo correto para seu Nó de Personagem Físico (PhysicsCharacterNode).

Para todas as outras direções: Toda vez que o usuário pressiona uma das teclas WASD, você mantém pista da direção que o usuário quer ir por armazenar esta informação em quatro Booleans direcionais. Nenhuma ação de caminhar verdadeira ocorre ainda. O loop de atualização é o que age na informação direcional armazenada nos booleans e faz o jogador se mover, como mostrado no próximo trecho de código:

3. setWalkDirection()

Anteriormente no método onAction(), você tem coletado a informação em qual direção o usuário quer entrar em termos de "para frente" ("forward") ou "esquerda" ("left"). No loop de atualização, você repetidamente pesquisa a atual rotação da câmera. Você calcula os vetores verdadeiros que correspondem a "para frente" ("forward") ou "esquerda" ("left") para o sistema de coordenada.

Este último e mais importante trecho de código vai no método simpleUpdate().

  public void simpleUpdate(float tpf) {
    Vector3f camDir = cam.getDirection().clone().multLocal(0.6f);
    Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f);
    walkDirection.set(0, 0, 0);
    if (left)  { walkDirection.addLocal(camLeft); }
    if (right) { walkDirection.addLocal(camLeft.negate()); }
    if (up)    { walkDirection.addLocal(camDir); }
    if (down)  { walkDirection.addLocal(camDir.negate()); }
    player.setWalkDirection(walkDirection);
    cam.setLocation(player.getPhysicsLocation());
  }

Isto é como a caminhada é atovada:

  • Inicialize o vetor walkDirection para zero. Isto é onde você quer armazenar a direção de caminha calculada.
  • Adicone para walkDirection os vetores de movimento recentes que você consultou da câmera. Desta maneira é possível para um personagem mover para frente e para a esquerda simultaneamente, por exemplo!
  • Esta última linha faz a "mágica de caminhar":
    player.setWalkDirection(walkDirection);
  • Sempre use setWalkDirection() para fazer o objeto controaldo por física se mover continuamente e o motor físico manuseia a detecção de colisão para você.
  • Faça o objeto de câmera em primeira pessoa seguir ao longo do jogador controlado por física:
    cam.setLocation(player.getPhysicsLocation());

Importante: De novo, não use setLocalTranslation() para controlar aonde o personagem caminha. Você ficará atolado por se sobrepor comum outro objeto físico. Você po por o jogador em uma posição de início com with setPhysicalLocation() se você tiver certeza de colocá-lo um pouco acima do chão e distante de obstáculos.

Conclusão

Você aprendeu a como carregar um modelo de cena físico "sólido" e caminhar ao redor com uma perspectiva de primeira pessoa. Você aprendeu a acelerar os cálculos físicos por usar a CollisionShapeFactory para criar Formas de Colisão (CollisionShapes) eficientes pra Geometrias (Geometries) complexas. Você sabe como adicionar Controles Físicos (PhysicsControls) para suas geometrias colidíveis e você as registra do Espaço Físico (PhysicsSpace). Você tambem aprendeu a usar player.setWalkDirection(walkDirection) para mover os personagens de uma maneira consciente de colisão e não setLocalTranslation().

Terrenos são um outro tipo de cna que você irá querer caminhar. Vamos prosseguir em como aprender a gerar terrenos agora.

Informação Relacionada:

  • Como carregar modelos e cenas: Hello Asset, Scene Explorer, Scene Composer
  • Colisão de Terreno
  • Para aprender mais sobre cenas físicas complexas onde vários objetos físicos móveis batem um no outro, leia Hello Physics.
  • Para sua informação, há soluções de detecção de colisão mais simples sem física também. Dê uma olhada em jme3test.collision.TestSimpleCollision.java (e SphereMotionAllowedListener.java).

jMonkeyEngine 3 Tutorial (10) - Hello Terrain

Anterior: Hello Collision, Próximo: Hello Audio

Uma maneira de criar terreno 3D é esculpir um modelo de terreno enorme. Isto dá a você muita liberdade artística - mas o rendering de ral modelo enorme pode ser muito lento. Este tutorial explica como criar terrenos com rápido rendering de mapas de altura, e como usar splatting de textura para fazer o terreno parecer bom.

Note: Se você obter um erri enquanto tentando criar seu objeto ImageBasedHeightMap, você pode precisar atualizar o SDK, clique em "Ajuda" ("Help") / "Checar por atualizações" ("Check for updates")

Para usar os ativos de exemplo em um novo projeto SDK da jMonkeyEngine, dê um clique no botão direito do seu projeto, selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.renderer.Camera;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.HillHeightMap; // for exercise 2
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import java.util.ArrayList;
import java.util.List;

/** Sample 10 - How to create fast-rendering terrains from heightmaps,
and how to use texture splatting to make the terrain look good.  */
public class HelloTerrain extends SimpleApplication {

  private TerrainQuad terrain;
  Material mat_terrain;

  public static void main(String[] args) {
    HelloTerrain app = new HelloTerrain();
    app.start();
  }

  @Override
  public void simpleInitApp() {
    flyCam.setMoveSpeed(50);

    /** 1. Create terrain material and load four textures into it. */
    mat_terrain = new Material(assetManager, 
            "Common/MatDefs/Terrain/Terrain.j3md");

    /** 1.1) Add ALPHA map (for red-blue-green coded splat textures) */
    mat_terrain.setTexture("Alpha", assetManager.loadTexture(
            "Textures/Terrain/splat/alphamap.png"));

    /** 1.2) Add GRASS texture into the red layer (Tex1). */
    Texture grass = assetManager.loadTexture(
            "Textures/Terrain/splat/grass.jpg");
    grass.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex1", grass);
    mat_terrain.setFloat("Tex1Scale", 64f);

    /** 1.3) Add DIRT texture into the green layer (Tex2) */
    Texture dirt = assetManager.loadTexture(
            "Textures/Terrain/splat/dirt.jpg");
    dirt.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex2", dirt);
    mat_terrain.setFloat("Tex2Scale", 32f);

    /** 1.4) Add ROAD texture into the blue layer (Tex3) */
    Texture rock = assetManager.loadTexture(
            "Textures/Terrain/splat/road.jpg");
    rock.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex3", rock);
    mat_terrain.setFloat("Tex3Scale", 128f);

    /** 2. Create the height map */
    AbstractHeightMap heightmap = null;
    Texture heightMapImage = assetManager.loadTexture(
            "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
    heightmap.load();

    /** 3. We have prepared material and heightmap. 
     * Now we create the actual terrain:
     * 3.1) Create a TerrainQuad and name it "my terrain".
     * 3.2) A good value for terrain tiles is 64x64 -- so we supply 64+1=65.
     * 3.3) We prepared a heightmap of size 512x512 -- so we supply 512+1=513.
     * 3.4) As LOD step scale we supply Vector3f(1,1,1).
     * 3.5) We supply the prepared heightmap itself.
     */
    int patchSize = 65;
    terrain = new TerrainQuad("my terrain", patchSize, 513, heightmap.getHeightMap());

    /** 4. We give the terrain its material, position & scale it, and attach it. */
    terrain.setMaterial(mat_terrain);
    terrain.setLocalTranslation(0, -100, 0);
    terrain.setLocalScale(2f, 1f, 2f);
    rootNode.attachChild(terrain);

    /** 5. The LOD (level of detail) depends on were the camera is: */
    TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
    terrain.addControl(control);
  }
}

Quando você executar esta amostra você deveria ver um terreno com montanhas sujas, mais algumas estradas onludando entre.

O que é um Mapa de Altura (Heightmap)?

Mapas de Altura (Heightmaps) são uma maneira eficiente de representar a forma de um terreno montanhoso. Nem todo pixel do terreno é armazenado,ao invés disso, uma grade de valores de amostra é usada para dobrar a altura do terreno em certos pontos. As alturas entre as amostras é interpolada.

Em Java, um mapa de altura é um array de ponto flutuante contendo valores de altura entre 0f e 255f. Aqui está um exemplo muito simples de terreno gerado de um mapa de altura com 5x5=25 valores de altura.

Coisas importantes para se notar

  • Valores baixo (e.g. 0 ou 50) são vales.
  • Valores alto (e.g. 200, 255) são montanhas.
  • O mapa de altura só especifica alguns pontos e o motor interpola o resto. Interpolação é mais efciiente do que criar um modelo com vários milhões de vértices.

Quando carregando em Java tipos de dado para armazenar um array de floats entre 0 e 255, a classe Imagem vem a mente. Armazenar os valores de altura de um terreno como imagem em escala de cinza têm uma grande vantagem: O resultado é muito amigável para o usuário, como um mapa topográfico:

  • Valores baixo (e.g. 0 ou 50) são cinza escuro – estes são vales.
  • Valores alto (e.g. 200, 255) são cinza claro – estes são montanhas.

Olhe no próximo instantâneo: Na esquerda-topo você vê uma imagem em escala de cinza 128x128 (mapa de altura) que foi usada como uma base para gerar o terreno ilustrado. Para fazer a forma montanhosa melhor visível, os topos da montanha são coloridos em branco, vales marrom, e as áreas intermediárias verde:

}

Em um jogo real, você irá querer usar terrenos mais suaves e mais complexos que os mapas de altura simples mostrados aqui. Mapas de altura tipicamente têm tamanhos quadrados de 512x512 ou 1024x1024, e contém centenas de milhares para 1 milhão de valores de altura. Não importa qual o tamanho, o conceito é o mesmo que o descrito aqui.

Olhando no Código do Mapa de Altura

O primeiro passo para a criação de terreno é o mapa de altura. Você pode criar um por si mesmo em qualquer aplicação gráfica padrão. Tenha certeza que ele tem as seguintes propriedades:

  • O tamanho deve ser quadrado e potência de dois.
    • Exemplos: 128x128, 256x256, 512x512, 1024x1024
  • Modo de cor deve ser 255 escalas de cinza.
    • Não forneça uma imagem colorida, ela será interpretada como escala de cinzam cin resultados possivelmente estranhos.
  • Salve o mapa como um arquivo de imagem .jpg ou .png

O aqruivo mountains512.png que você vê aqui é um exemplo típico de uma imagem de um mapa de altura.

Aqui está como você cria o objeto de mapa de altura em seu código jME:

  • Crie um objeto Textura (Texture).
  • Carregue sua imagem de mapa de altura preparada dentro do objeto textura.
  • Crie um objeto Mapa de Altura Abstrato (AbstractHeightmap) de um ImageBasedHeightMap.
  • Ele requer uma imagem de uma Textura JME.
  • Carregue o mapa de altura.
AbstractHeightMap heightmap = null;
    Texture heightMapImage = assetManager.loadTexture(
            "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
    heightmap.load();

O que é Splatting de Textura?

Anteriormente você aprendeu a como criar um material para uma forma simples como a de um cubo. Todos os lados do cubo têm a mesma cor. Você pode aplicar o mesmo material para um terreno, mas então você tem uma grande planície, um grande deserto de rocha, etc. Isto não é sempre o que você quer.

Splatting de textura permite a você criar um material customizado e "pintar" texturas nele como com um "pincel de pintura". Isto é muito útil para terrenos. Como você vê no exemplo aqui, você pode pintar uma textura de grama dentro dos vales, uma textura de sujeira nas montanhas, e estradas livre de forma de forma intermediária.

O SDK da jMonkeyEngine vem com um plugin Editor de Terreno (TerrainEditor). Usando o plugin TerrainEditor plugin, você pode esculpir o terreno com o mouse e salvar o resultado como um mapa de altura. Você pode pintar texturas no terreno e o plugin salva as texturas splat resultantes como mapas de alfa (alphamap(s)). Os próximos parágrafos descrevem o processo manual para você. Você pode escolher criar o terreno a mão, ou usar o plugin TerrainEditor.

Texturas de Splat são baseadas na definição de material Terrain.j3md. Se você abrir o arquivo Terrain.j3md e olhar na seção Parâmetros do Material (Material Parameters), você verá que você tem várias camadas de textura para pintar: Tex1, Tex2, Tex3, etc.

Antes de você iniciar a pintarm você tem de fazer algumas decisões:

Escolha três texturas. Por exemplo grass.jpg, dirt.jpg, e road.jpg. jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_road.jpg jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_dirt.jpg jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_grass.jpg
Você "pinta" três camadas de textura por usar três cores: vermelho, verde e azul. Você arbitrariamente decide que…

  • Vermelho é grama - vermelho é a camada Tex1, então ponha a textura de grama em Tex1.
  • Verde é sujeira - verde é a camada Tex2, então ponha a textura de sujeira em Tex2.
  • Azul são as estradas - azul é a camada Tex3, então ponha a textura de estradas em Tex3.

Agora você inicia a pintar a textura:

  • Faça uma cópia do mapa de altura dos seus terrenos, mountains512.png. Você quer ela como uma referência para a forma do terreo.
  • Nomeie a cópia alphamap.png.
  • Abra alphamap.png em um editor gráfico e mude o modo de imagem para imagem colorida.
    • Pinte o vales negros de vermelho - isto será a grama.
    • Pinte as encostas brancas de verde - isto será a sujeira das montanhas.
    • Pinte linhas azuis onde você quer estradas para para cruzar (criss-cross) o terreno.
  • O resultado final deveria parecer similar a isto:

Olhando no Código de Texturização

Como normal, você cria um objeto Material. Baseie ele na Definição de Material Terrain.j3md que é inclusa na estrutura jME3.

Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");

Carregue quatro texturas dentro deste material. A primeira, Alfa, é o mapa de alfa que você acabou de criar.

mat_terrain.setTexture("Alpha",
    assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));

As três outras texturas são as camadas que você tem previamente decidido pintar: grama, sujeira e estrada. Você cria objetos de textura e carrega as três texturas como normal. Note como você atribui eles para as respectivas camadas de textura (Tex1, Tex2, e Tex3) dentro do Material!

    /** 1.2) Add GRASS texture into the red layer (Tex1). */
    Texture grass = assetManager.loadTexture(
            "Textures/Terrain/splat/grass.jpg");
    grass.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex1", grass);
    mat_terrain.setFloat("Tex1Scale", 64f);

    /** 1.3) Add DIRT texture into the green layer (Tex2) */
    Texture dirt = assetManager.loadTexture(
            "Textures/Terrain/splat/dirt.jpg");
    dirt.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex2", dirt);
    mat_terrain.setFloat("Tex2Scale", 32f);

    /** 1.4) Add ROAD texture into the blue layer (Tex3) */
    Texture rock = assetManager.loadTexture(
            "Textures/Terrain/splat/road.jpg");
    rock.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex3", rock);
    mat_terrain.setFloat("Tex3Scale", 128f);

As escalas de textura individual (e.g. mat_terrain.setFloat("Tex3Scale", 128f);) dependem do tamanho das texturas que você usa.
  • Você pode dizer que você escolheu uma escala pequena demais se, por exemplo, os tiles da sua estrada parecem como grãos de areia.
  • Você pode dizer que você escolheu uma escala grande demais se, por exemplo, as laminas da grama parecem gravetos.

Use setWrap(WrapMode.Repeat) para fazer a pequena tetxura preencher a área comprida. Se a repetição é muito visível, tente ajustar o respectivo valor Tex*Scale.

O que é um Terreno?

Internamente, a malha de terreno gerada é quebrada em tiles e blocos. Isto é uma otimização para fazer culling mais fácil. Você não precisa se preocupar sobre "tiles e blocos" tanto, apenas use os valores recomendados por enquanto - 64 é um bom início.

Vamos assumir que você queira gerar um terreno 512x512. Você já tem criado o objeto mapa de altura. Aqui estão os passos que você realiza a cada vez que você cria um novo terreno.

Crie um TerrainQuad com os seguintes argumentos:

  • Especificque um nome: E.g. my terrain.
  • Especifique o tamanho do tile: Você quer tiles de terreno do tamanho 64x64, então você fornece 64+1 = 65.
    • Em geral 64 é um bom valor de início para tiles de terreno.
  • Especifique o tamanho do bloco: Desde que você preparou um mapa de altura de tamanho 512x512, você fornece 512+1 = 513.
    • Se você fornecer um tamanho de bloco de 2x o tamanho do mapa de altura (1024+1=1025), você consegue um terreno mais esticado, comprido e plano.
    • Se você fornecer um tamanho de bloco de 1/2 o tamanho do mapa de altura (256+1=257), você consegue um terreno menor, mais detalhado.
  • Forneça o objeto de mapa de altura 512x512 que você criou.

Olhando no Código de Terreno

Aqui está o código:

terrain = new TerrainQuad(
  "my terrain",               // name
  65,                         // tile size
  513,                        // block size
  heightmap.getHeightMap());  // heightmap

Você criou o ibjeto de terreno.

  • lembre-se de aplicar o material criado:
    terrain.setMaterial(mat_terrain);
  • Lembre-se de anexar o terreno para o nó raiz (rootNode).
    rootNode.attachChild(terrain);

Se necessário, escalone e translade o objeto terreno, apenas como quaqluer outro Espacial (Spatial).

DEica: Terrain.j3md é uma definição de material não tonalizada, então você não precisa de uma fonte de luz. Você também pode usar TerrainLighting.j3md mais uma luz, se você quer um terreno tonalizado.

O que é LOD (Level of Detail - Nível de Detalhe)?

JME3 inclui uma otimização que ajusta o nível de detalhe (LOD) do terreno renderizado dependendo de quão próximo ou longe a câmera está.

    TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
    terrain.addControl(control);

Partes próximas do terreno são renderizados em detalhe integral. Partes do terreno que estão distantes não estão claramente visíveis de qualquer forma, e JME3 melhora o desempenho por renderizar elas de maneira menos detalhada. Desta maneira você pode carregar enormes terrenos sem penalidade causada por detalhes invisíveis.

Exercícios

Exercício 1: Camadas de Textura

O que acontece quando você troca duas camadas, por exemplo Tex1 e Tex2?

...
mat_terrain.setTexture("Tex2", grass);
...
mat_terrain.setTexture("Tex1", dirt);

Você vê que é mais fácil trocar duas camadas no código, do que trocar as cores no mapa de alfa (alphamap).

Exercício 2: Terrenos Aleatorizados

As seguintes duas linhas geram o objeto mapa de altura baseado na sua imagem definida pelo usuário:

Texture heightMapImage = assetManager.loadTexture(
        "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());

Ao invés disso pode também pode deixar JME3 gerar um terreno aleatório para você:

  • Qual resultado você obtém quando você substitui as duas linhas do mapa de altura acima pelas linhas seguintes e executa a amostra?
    HillHeightMap heightmap = null;
    HillHeightMap.NORMALIZE_RANGE = 100; // optional
    try {
        heightmap = new HillHeightMap(513, 1000, 50, 100, (byte) 3); // byte 3 is a random seed
    } catch (Exception ex) {
        ex.printStackTrace();
    }

Mude um parâmetro por vez, e então execute a amostra de novo. Note as diferenças. Você pode descobrir qual dos valores tem efeito no terreno gerado (olhe no javadoc também)?

  • Qual valor controla o tamanho?
    • O que acontece se o tamanho nãio é um número quadrado +1?
  • Qual valor controla o número de montes gerados?
  • Quais valores controlam o tamanho e a aclividade dos montes?
    • O que acontece se o mínimo é maior que ou igual ao máximo?
    • O que acontece se tanto o mínimo quanto o máximo são valores pequenos (e.g. 10/20)?
    • O que acontece se tanto o mínimo quanto o máximo são valores grandes (e.g. 1000/1500)?
    • O que acontece se o mínimo e o máximo são muito próximos (e.g. 1000/1001, 20/21)? Muito distantes (e.g. 10/1000)?

Você vê a variedade de terrenos montanhosos que podem ser gerados usando este método.

Para este exercício você pode continuar usando o Material de splat do código de amostra acima. Apenas não esteja surpreso que o Material não combina com a forma do terreno recentemente aleatorizado. Se você quiser gerar texturas de splat de casamento real para mapas de altura randomizados, você precisa escrever um método customizado que, por exemplo, cria um mapa de alfa (alphamap) do mapa de altura por substituir certos valores de escala de cinza por certos valores RGB.

Exercício 3: Terrenos Sólidos

Você pode combinar o que você aprendeu aqui e em Hello Collision, e fazer o terreno sólido?

Conclusão

Você aprendeu a como criar terrenos que são mais eficientes que carregar um modelo gigante. Você sabe como gerar mapas de altura aleatórios ou feitos a mão. Você também pode adicionar um controle de LOD para renderizar grandes terrenos mais rápido. Você está consciente que você pode combinar o que você aprendeu sobre detecção de colisão para fazer o terreno sólido para um jogador físico. Você é também capaz de texturizar um terreno "like a boss" usando Materiais em camada e splatting de textura. Você está consciente que o SDK da jMonkeyEngine fornece um Editor de Terreno (TerrainEditor) que ajuda com a maioria destas tarefas manuais.

Você quer ouvir os jogadores dizerem "Ai!" quando eles batem em uma parede ou caem de um monte? Continue aprendendo como adicionar som para seu jogp.

Veja Também:

  • Colisão de Terreno

jMonkeyEngine 3 Tutorial (11) - Hello Audio

Anterior: Hello Terrain, Próximo: Hello Effects

Este tutorial explica como adicionar som 3D para um jogo, e como fazer sons tocarem juntos com eventos, tais como clicar. Você aprende a como usar um Ouvinte de Áudio (Audio Listener) e Nós de Áudio (Audio Nodes). Você também faz uso de um Ouvinte de Ação (Action Listener) e um MouseButtonTrigger do tutorial anterior Hello Input para fazer um clique do mouse disparar um som de tiro.

Para usar os ativos de exemplo em um novo projeto do SDK da jMonkeyEngine, dê um clique com o botão direito em seu projeto e selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.audio.AudioNode;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;

/** Sample 11 - playing 3D audio. */
public class HelloAudio extends SimpleApplication {

  private AudioNode audio_gun;
  private AudioNode audio_nature;
  private Geometry player;

  public static void main(String[] args) {
    HelloAudio app = new HelloAudio();
    app.start();
  }

  @Override
  public void simpleInitApp() {
    flyCam.setMoveSpeed(40);

    /** just a blue box floating in space */
    Box box1 = new Box(Vector3f.ZERO, 1, 1, 1);
    player = new Geometry("Player", box1);
    Material mat1 = new Material(assetManager, 
            "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.Blue);
    player.setMaterial(mat1);
    rootNode.attachChild(player);

    /** custom init methods, see below */
    initKeys();
    initAudio();
  }

  /** We create two audio nodes. */
  private void initAudio() {
    /* gun shot sound is to be triggered by a mouse click. */
    audio_gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false);
    audio_gun.setLooping(false);
    audio_gun.setVolume(2);
    rootNode.attachChild(audio_gun);

    /* nature sound - keeps playing in a loop. */
    audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", false);
    audio_nature.setLooping(true);  // activate continuous playing
    audio_nature.setPositional(true);
    audio_nature.setLocalTranslation(Vector3f.ZERO.clone());
    audio_nature.setVolume(3);
    rootNode.attachChild(audio_nature);
    audio_nature.play(); // play continuously!
  }

  /** Declaring "Shoot" action, mapping it to a trigger (mouse click). */
  private void initKeys() {
    inputManager.addMapping("Shoot", new MouseButtonTrigger(0));
    inputManager.addListener(actionListener, "Shoot");
  }

  /** Defining the "Shoot" action: Play a gun sound. */
  private ActionListener actionListener = new ActionListener() {
    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        audio_gun.playInstance(); // play each instance once!
      }
    }
  };

  /** Move the listener with the a camera - for 3D audio. */
  @Override
  public void simpleUpdate(float tpf) {
    listener.setLocation(cam.getLocation());
    listener.setRotation(cam.getRotation());
  }

}

Quando você executar a amostra você deveria ver um cubo azul. Você deveria ouvir um som ambiente ao estilo da natureza. Quando você clicar, você ouve um disparo alto.

Entendendo a Amostra de Código

No método initSimpleApp(), você cria uma geometria de cubo simples chamada player e anexa ela para a cena – isto é apenas conteúdo da amostra arbitrário, então você vê alguma coisa quando executando a amostra de áudio.

Vamos dar uma olhada mais de perto em initAudio() para aprender a como usar AudioNodes.

AudioNodes

Adicionar som para seu jogo é muito simples: Salve seus arquivos de áudio no seu diretório assets/Sound. JME3 suporta os formatos de arquivo Ogg Vorbis (.ogg) e Wave (.wav).

Para cada som, você cria um AudioNode. Você pode usar um AudioNode como qualquer nó no grafo de cena JME, e.g. anexe ele para outros Nós (Nodes). Você cria um nó para um som de disparo de arma, e um nó para o som da natureza.

  private AudioNode audio_gun;
  private AudioNode audio_nature;

Olhe no método customizado initAudio(): Aqui você inicializa os objetos de som e configura seus parâmetros.

audio_gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false);
    ...
audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", false);

Estas duas linhas criam novos nós de som dos arquivos de áudio dados no AssetManager. A flag false significa que você quer bufferizar estes sons antes de tocar. (Se você configurar esta flag para true, o som será streamed, o que faz sentido para sons realmente longos.)

Você quer que o som do disparo de arma toque uma vez (você não quer que ele repita). Você também especifica seu volume como um fator de ganho (em 0, som é mudo, em 2, ele é duas vezes tão alto, etc.).

    audio_gun.setLooping(false);
    audio_gun.setVolume(2);
    rootNode.attachChild(audio_gun);

O som da natureza é diferente: Você quer repetí-lo continuamente como som de plano de fundo. Isto é porque você configura repetição para true, e imediatamente chama o método play() no nó. Você também escolhe configurar seu volume para 3.

    audio_nature.setLooping(true); // activate continuous playing
    ...
    audio_nature.setVolume(3);
    rootNode.attachChild(audio_nature);
    audio_nature.play(); // play continuously!
  }

Aqui você faz audio_nature um som posicional que vem de um certo lugar. Para isso você dá ao nó uma translação explícita, neste exemplo você escolhe Vector3f.ZERO (que representa as coordenadas 0.0f,0.0f,0.0f, o centro da cena.) Desde que jME suporta áudio 3D, você é agora capaz de ouvir o som vindo desta localização particular. Fazer o som posicional é opcional. Se você não usar estas linhas, o som ambiente vem de todas as direções.

    ...
    audio_nature.setPositional(true);
    audio_nature.setLocalTranslation(Vector3f.ZERO.clone());
    ...

Dica: Anexe AudioNodes dentro do grafo de cena como todos os nós, para fazer certos nós em movimento ficarem atualizados. Se você não anexá-los, eles ainda são audíveis e você não consegue uma mensagem de erro mas som 3D não funcionará como esperado. AudioNodes podem ser ligados diretamente ao nó raiz ou eles podem ser anexados dentro de um nó que está movendo através da cena e tanto o AudioNode quanto a posição 3d do som que ele está gerando se moverão de acordo.

Dica: playInstance sempre reproduz o som da posição do AudioNode então múltiplos disparos de uma arma (por exemplo) podem ser gerados desta maneira, entretanto se múltiplas armas estão atirando de uma vex então um AudioNode é necessário para cada.

Disparando Som

Vamos dar uma olhada mais de perto em initKeys(): Como você aprendeu nos tutoriais anteriores, você usa o inputManager para responder a entrada do usuário. Aqui você adiciona um mapeamento para um clique do botão esquerdo do mouse, e nomeia esta nova ação de Shoot (Disparar).

  /** Declaring "Shoot" action, mapping it to a trigger (mouse click). */
  private void initKeys() {
    inputManager.addMapping("Shoot", new MouseButtonTrigger(0));
    inputManager.addListener(actionListener, "Shoot");
  }

Configurar o ActionListener também deveria ser familiar dos tutoriais anteriores. Você declara que, qaundo o gatilho (o botão do mouse) é pressionado e liberado, você quer reproduzir um som de arma.

  /** Defining the "Shoot" action: Play a gun sound. */
  private ActionListener actionListener = new ActionListener() {
    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        audio_gun.playInstance(); // play each instance once!
      }
    }
  };

Desde que você quer ser capaz de disparar rápido repetidamente, então você não quer esperar para o som de disparo anterior terminar para que o próximo possa iniciar. Isto porque você reproduz este som usando o método playInstance(). Isto significa que todo clique inicia uma nova instância do som, então duas instâncias podem se sobrepor. Você configura este som para não repetir, para que cada instância somente toque uma vez. Como você esperaria de um disparo de arma.

Ambiente ou Situacional?

Os dois sons são dois diferentes casos de uso:

  • Um disparo é situacional. Você quer tocá-lo uma vez, imediatamente quando ele é disparado.
    • Isto é porque você setLooping(false).
  • O som da natureza é um ruído ambiente, de plano de fundo. Você quer que ele inicie tocando do início, tanto tempo quanto o jogo execute.
    • Isto é porque você setLooping(true).

Agora todo dom dabe se ele deveria repetir ou não.

Salvo o boolean de repetição, uma outra diferenã é onde play().playInstance() é chamado naqueles nós:

  • Você inicia a reprodução do som da natureza de plano de funo imediatamente após você ter o criado, no método initAudio().
        audio_nature.play(); // play continuously!

O som de disparo, entretanto, é disparado automaticamente, uma vez, somente como parte da entrada de ação Disparar (Shoot) que você definiu no ActionListener.

      /** Defining the "Shoot" action: Play a gun sound. */
      private ActionListener actionListener = new ActionListener() {
        @Override
        public void onAction(String name, boolean keyPressed, float tpf) {
          if (name.equals("Shoot") && !keyPressed) {
            audio_gun.playInstance(); // play each instance once!
          }
        }
      };

Bufferizado ou Streaming?

O Boolean no construdor AudioNode define se o áudio é bufferizado (false) ou streamed (true). Por exemplo:

audio_nature = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false); // buffered
...
audio_nature = new AudioNode(assetManager, "Sound/Environment/Nature.ogg", true); // streamed

Tipicamente você stream sons longos, e bufferiza sons curtos.

Note que sons streamed não podem repetir (i.e. setLooping não trabalhar como você espera). Cheque o getStatus no nó e se ele tem parado recrie o nó.

Play() or PlayInstance()?

audio.play() audio.playInstance()
Toca sons bufferizados; Toca sons bufferizados.
Toca sons streamed. Não pode tocar sons streamed.
O mesmo som não pode tocar duas vezes ao mesmo tempo. Os mesmos sons podem tocar múltiplas vezes e se sobrepor.

Seu Ouvido na Cena

Para criar um efeito de áudio 3D, JME3 precisa saber a posição da fonte de som, e a posição dos ouvidos do jogador. Os ouvidos são representados por um objeto Ouvinte de Áudio (Audio Listener) 3D. O objeto ouvinte é um objeto padrão em uma SimpleApplication.

Para extrair o máximo do efeito de áudio 3D, você deve usar o método simpleUpdate() para mover e rotacionar o ouvinte (os ouvidos do jogador) junto com a câmera (os olhos do jogador).

  public void simpleUpdate(float tpf) {
    listener.setLocation(cam.getLocation());
    listener.setRotation(cam.getRotation());
  }

Se você não fizer isso, os resultados do áudio 3D serão muito aleatórios.

Global, Direcional, Posicional?

Neste exemplo você definiu o som da natureza como vindo de uma certa posição, mas não o som do disparo. Isto sigmifica que seu disparo é global e pode ser ouvido em todo lugar com o mesmo volume. JME3 também suporta sons direcionais que você pode somente ouvir de uma certa direção.

Faz sentido fazer o som do disparo posicional, e deixar o som ambiente vir de todas as direções. Como você decide qual tipo de som 3D usar de caso a caso?

Em um jogo com inimigos se movendo você pode querer fazer os sons do disparo ou da arma posicionais. Nestes casos você deve mover o AudioNode para a localização do inimigo antes de playInstance()ing ele. Nesta maneira um jogador com alto-falantes estéreo ouve de qual direção o inimigo está vindo.
Similarmente, você pode ter níveis do jogo onde você quer que um som de plano de fundo toque globalmente. Nste caso você faria o AudioNode nem posicional nem drecional (coloque ambos para false).
Se você quer que o som seja "absorvido pelas paredes" e somente transmitido em uma direção, você faria este AudioNode direcional. Este tutorial não discute sons direcionais. Você pode ler sobre Áudio Avançado aqui.

Em resumo, você deve escolher em toda situação se faz sentido para um som ser global, direcional, ou posicional.

Conclusão

Você agora sabe como adicionar os dois tipos mais comuns de sons para seu jogo: Sons globais e sons posicionais. Você pode tocar sons em duas maneiras: Ou continuamente m um loop, ou situacionalmente apenas uma vez. Você sabe a diferença entre bufferizar sons curtos e streaming sons longos. Você sabe a difereça entre tocar instâncias de sons sobrepostos e tocar sons únicos que não podem se sobrepor com eles mesmos. Você também aprendeu a usar arquivos de som que estão em formato .ogg ou .wav.

Dica: A implementação de Áudio da JME também suporta efeitos mais avançados como o efeito Doppler. Use estas características "pro" para fazer o som do áudio diferente dependendo se ele está em um corredor, em uma caverna, nos outdoors, ou em uma sala com carpet. Descubra mais sobre efeitos ambientais da amostra de código inclusa no diretório jme3test e dos documentos de Áudio avançados.

Quer alguns fogos e explosões para acompanhar seus sons? Prossiga a leitura para aprender mais sobre efeitos.

Veja também:

  • Audio

jMonkeyEngine 3 Tutorial (12) - Hello Effects

Anterior: Hello Audio, Próximo: Hello Physics

Quando você vê um dos seguintes em um jogo, então um sistema de partículas está provavelmente atrás disso:

  • Fogo, chamas, faíscas;
  • Chuva, neve, cascatas, folhas;
  • Explosões, detritos, ondas de choque;
  • Poeira, neblina, nuvens, fumaça;
  • Enxames de inseto, banhos de meteoro;
  • Feitiços mágicos.

Estes elementos de cena não podem ser modelados por malhas. Em termos muito simples:

  • A diferença entre uma explosão e uma nuvem de poeira é a velocidade do efeito de partícula.
  • A diferença entre chamas e uma cachoeira é a direção e a cor do efeito da partícula.

Efeitos de partícula podem ser animados (e.g. faíscas, quedas) e estáticos (cordões de grama, cabelo). Efeitos de não partícula incluem bloom/brilho, e motion blur/afterimage. Neste tutorial você aprende a como fazer partículas animadas (com.jme3.effect).

Para usar os ativos de exemplo em um novo projeto do SDK da jMonkeyEngine dê um clique com o botão direito em seu projeto, selecione "propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;

/** Sample 11 - how to create fire, water, and explosion effects. */
public class HelloEffects extends SimpleApplication {

  public static void main(String[] args) {
    HelloEffects app = new HelloEffects();
    app.start();
  }

  @Override
  public void simpleInitApp() {

    ParticleEmitter fire = 
            new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30);
    Material mat_red = new Material(assetManager, 
            "Common/MatDefs/Misc/Particle.j3md");
    mat_red.setTexture("Texture", assetManager.loadTexture(
            "Effects/Explosion/flame.png"));
    fire.setMaterial(mat_red);
    fire.setImagesX(2); 
    fire.setImagesY(2); // 2x2 texture animation
    fire.setEndColor(  new ColorRGBA(1f, 0f, 0f, 1f));   // red
    fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow
    fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 2, 0));
    fire.setStartSize(1.5f);
    fire.setEndSize(0.1f);
    fire.setGravity(0, 0, 0);
    fire.setLowLife(1f);
    fire.setHighLife(3f);
    fire.getParticleInfluencer().setVelocityVariation(0.3f);
    rootNode.attachChild(fire);

    ParticleEmitter debris = 
            new ParticleEmitter("Debris", ParticleMesh.Type.Triangle, 10);
    Material debris_mat = new Material(assetManager, 
            "Common/MatDefs/Misc/Particle.j3md");
    debris_mat.setTexture("Texture", assetManager.loadTexture(
            "Effects/Explosion/Debris.png"));
    debris.setMaterial(debris_mat);
    debris.setImagesX(3); 
    debris.setImagesY(3); // 3x3 texture animation
    debris.setRotateSpeed(4);
    debris.setSelectRandomImage(true);
    debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 4, 0));
    debris.setStartColor(ColorRGBA.White);
    debris.setGravity(0, 6, 0);
    debris.getParticleInfluencer().setVelocityVariation(.60f);
    rootNode.attachChild(debris);
    debris.emitAllParticles();
  }
}

Você deveria ver um explosão que envia detritos voando, e um fogo. Mais código de exemplo se encontra aqui.

Animação de Textura e Variação

Inicie por escolher uma textura de material para seu efeito. Se você fornece ao emissor com um conjunto de texturas (veja imagem), ele pode usá-las tanto por variação (ordem aleatória), ou como passos de animação (ordem fixa).

Configurar texturas do emissor trabalha como você aprendeu nos capítulos anteriores. Desta vez você baseia o material na definição de material Particle.j3md. Vamos dar uma olhada mais de perto no material para o efeito de detritos.

    ParticleEmitter debris = 
            new ParticleEmitter("Debris", ParticleMesh.Type.Triangle, 10);
    Material debris_mat = new Material(assetManager, 
            "Common/MatDefs/Misc/Particle.j3md");
    debris_mat.setTexture("Texture", assetManager.loadTexture(
            "Effects/Explosion/Debris.png"));
    debris.setMaterial(debris_mat);
    debris.setImagesX(3); 
    debris.setImagesY(3); // 3x3 texture animation
    debris.setSelectRandomImage(true);
        ...
  1. Crie um material e carregue a textura
  2. Diga ao Emissor quantos passos de animação (x*y) a textura é dividida.
  3. A textura de detritos tem 3x3 quadros.
  4. Opcionalmente diga ao Emissor se os passos de animação são para serem aleatórios ou em ordem.
  5. Para os detritos os quadros reproduzem de forma aleatória.

Como você pode ver no exemplo de detritos animação de textura melhora os efeitos pois cada "chama" ou "pedaço de detrito" agora parece diferente. Também pense de efeitos mágicos ou elétricos, onde você cria animações muito interessantes por usar uma série de trovões de transformação ordenada; ou folhas voando ou flocos de neve, por exemplo.

O material do fogo é criado da mesma maneira, apenas usando a textura "Effects/Explosion/flame.png", com 2x2 passos de animação ordenados.

Texturas de partícula padrão

As seguintes texturas de partícula estão inclusas em test-data.jar. Você pode copiá-las e usá-las em seus próprios efeitos.

~Caminho da Textura Dimensão Previsualização
Effects/Explosion/Debris.png 3*3
Effects/Explosion/flame.png 2*2
Effects/Explosion/shockwave.png 1*1
Effects/Explosion/smoketrail.png 1*3
Effects/Smoke/Smoke.png 1*15

Copie elas em seu diretório assets/Effects para usá-las.

Criando Textura Customizada

Para seu jogo, você provavelmente criará texturas de partícula personalziadas. Olhe no exemplo do fogo de novo.

    ParticleEmitter fire = 
            new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30);
    Material mat_red = new Material(assetManager, 
            "Common/MatDefs/Misc/Particle.j3md");
    mat_red.setTexture("Texture", assetManager.loadTexture(
            "Effects/Explosion/flame.png"));
    fire.setMaterial(mat_red);
    fire.setImagesX(2); 
    fire.setImagesY(2); // 2x2 texture animation
    fire.setEndColor(  new ColorRGBA(1f, 0f, 0f, 1f));   // red
    fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow

Compare a textura com o efeito resultante.

  • Partes negras da imagem se tornam completamente transparentes.
  • Partes brancas/cinzas da imagem são transulcentes e ficam coloridas.
  • Você configura a cor usando setStartColor() e setEndColor().
  • Para o fogo é um gradiente de amarelo para vermelho.
  • Por padrão a animação é tocada em ordem e repete.

Crie uma textura escala de cinza em um editor gráfico e salve ele para seu diretório assets/Effects. Se você dividir um arquivo de imagem em x*y passos de animação, tenha certeza de que cada quadrado é de tamanho igual - da mesma forma que você vê nos exemplos aqui.

Parãmetros do Emissor

Um sistema de partícula é sempre centrado ao redor de um emissor.

Use o método setShape() para mudar a Forma do Emissor (EmitterShape):

    EmitterPointShape(Vector3f.ZERO) – particles emit from a point (default)
    EmitterSphereShape(Vector3f.ZERO,2f) – particles emit from a sphere-sized area
    EmitterBoxShape(new Vector3f(-1f,-1f,-1f),new Vector3f(1f,1f,1f)) – particles emit from a box-sized area

Exemplo:

emitter.setShape(new EmitterPointShape(Vector3f.ZERO));

Você cria diferentes efeitos por mudar os parâmetros do emissor:

~Parâmetro Método Padrão ~Descrição
number setNumParticles() N/A O número máximo de partículas simultaneamente visíveis. O valor é especificado pelo usuário no contrutor. Isto influencia a densidade e o tamanho da "trilha".
velocity getParticleInfluencer().setInitialVelocity() Vector3f.ZERO Especifica a um vetor quão rápido partículo se movem e qual a direção de início.
direção

setFacingVelocity() -
setRandomAngle() -
setFaceNormal() -
setRotateSpeed() || 0.2f -
false -
false -
Vector3f.NAN -
0.0f || Acessores opcionais que controlam em qual direção as partículas encaram enquanto voando. ||

lifetime

setHighLife() || 3f -
7f || Período do tempo mínimo e máximo antes que a partícula desapareça. ||

emission rate setParticlesPerSec() 20 Quantas novas partículas são emitidas por segundo.
color

setEndColor() || cinza || Configure para as mesmas cores ou para duas cores diferentes para um efeito de gradiente. ||

size

setEndSize() || 0.2f -
2f || Configure para dois diferentes valores para efeito de encolher/crescer, ou para o mesmo tamanho para efeito constante. ||

gravity setGravity() 0,1,0 Se partículas caem (positiva) ou voam (negativo). Configure para 0f para um efeito zero-g onde partículas continuam voando.

Você pode achar detalhes sobre parãmetros de efeito aqui. Adicione e modifique um parãmetro por vez, e experimente diferentes valores até que você consiga o efeito que você quer.

Dica: Use o SceneComposer no SDK da jMonkeyEngine para criar efeitos mais facilmente. Crie uma cena vazia e adiciona um objeto emissor para ela. Mude as propriedades do emissor e observe o resultado ao vivo. Você pode salvar efeitos criados como arquivo .j3o e carregá-los como cenas ou modelos.

Ecerício

Você pode "inverter" o efeito de fogo em uma pequena cascata? Aqui estão algumas dicas:

  • Mude a cor Vermelha e Amarela para Ciano e Azul
  • Inverta o vetor velocidade (direção) por usar um número negativo
  • Troca o tamanho do início e do fim.
  • Ative a gravidade por configurá-la para 0,1,0

Conclusão

Você tem aprendido que muitos efeitos diferentes podem ser criados por mudar os parãmetros e texturas de um objeto emissor geral.

Agora você prossegue para um outro capítulo excitante - a simulação de objetos físicos. Vamos disparar algumas bolas de canhão em uma parede de tijolos!

jMonkeyEngine 3 Tutorial (13) - Hello Physics

Anterior: Hello Effects, Próximo: documentação JME 3

Você se lembra do tutorial Hello Collision onde você fez o modelo de uma cidade sólido e caminhou através dele em uma perspectiva de primeira pessoa? Então você pode lembrar que, para a simulação de forças físicas, jME3 integra a biblioteca jBullet.

Exceto fazer modelos "sólidos" ("solid"), os casos de uso mais comum oara física em jogos 3D são:

  • Dirigir veículos com suspensão, fricção de pneu, pulando na rampa, a deriva (drifting) – Exemplo: carros de corrida
  • Rolando e balançando bolas - Exemplo: pong, bilhar, boliche
  • Caixas de deslizar e cair – Exemplo: Breakout, Arkanoid
  • Expor objetos para força e gravidade – Exemplo: espaçonaves ou vôo a zero-g
  • Animando bonecas de pano (ragdolls) – Exemplo: simulações de personagem "realísticos"
  • Balançando pêndulos, pontes de corda, cadeias flexíveis, e muito mais…

Todas estas propriedades físicas podem ser simuladas em JME3. Vamos dar uma olhada na simulação de forças físicas neste exemplo onde você atira bolas de canhão em uma parede de tijolos.

Para usar os ativos de exemplo em um novo projeto do SDK da jMonkeyEngine, dê um clique com o botão direito em seu projeto, selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" e adicione a biblioteca "jme3-test-data".

Código de Amostra

Obrigado a ouble1984 por contribuir com esta amostra divertida!

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.font.BitmapText;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Sphere.TextureMode;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;

/**
 * Example 12 - how to give objects physical properties so they bounce and fall.
 * @author base code by double1984, updated by zathras
 */
public class HelloPhysics extends SimpleApplication {

  public static void main(String args[]) {
    HelloPhysics app = new HelloPhysics();
    app.start();
  }

  /** Prepare the Physics Application State (jBullet) */
  private BulletAppState bulletAppState;

  /** Prepare Materials */
  Material wall_mat;
  Material stone_mat;
  Material floor_mat;

  /** Prepare geometries and physical nodes for bricks and cannon balls. */
  private RigidBodyControl    brick_phy;
  private static final Box    box;
  private RigidBodyControl    ball_phy;
  private static final Sphere sphere;
  private RigidBodyControl    floor_phy;
  private static final Box    floor;

  /** dimensions used for bricks and wall */
  private static final float brickLength = 0.48f;
  private static final float brickWidth  = 0.24f;
  private static final float brickHeight = 0.12f;

  static {
    /** Initialize the cannon ball geometry */
    sphere = new Sphere(32, 32, 0.4f, true, false);
    sphere.setTextureMode(TextureMode.Projected);
    /** Initialize the brick geometry */
    box = new Box(Vector3f.ZERO, brickLength, brickHeight, brickWidth);
    box.scaleTextureCoordinates(new Vector2f(1f, .5f));
    /** Initialize the floor geometry */
    floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f);
    floor.scaleTextureCoordinates(new Vector2f(3, 6));
  }

  @Override
  public void simpleInitApp() {
    /** Set up Physics Game */
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    //bulletAppState.getPhysicsSpace().enableDebug(assetManager);

    /** Configure cam to look at scene */
    cam.setLocation(new Vector3f(0, 4f, 6f));
    cam.lookAt(new Vector3f(2, 2, 0), Vector3f.UNIT_Y);
    /** Add InputManager action: Left click triggers shooting. */
    inputManager.addMapping("shoot", 
            new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addListener(actionListener, "shoot");
    /** Initialize the scene, materials, and physics space */
    initMaterials();
    initWall();
    initFloor();
    initCrossHairs();
  }

  /**
   * Every time the shoot action is triggered, a new cannon ball is produced.
   * The ball is set up to fly from the camera position in the camera direction.
   */
  private ActionListener actionListener = new ActionListener() {
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("shoot") && !keyPressed) {
        makeCannonBall();
      }
    }
  };

  /** Initialize the materials used in this scene. */
  public void initMaterials() {
    wall_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
    key.setGenerateMips(true);
    Texture tex = assetManager.loadTexture(key);
    wall_mat.setTexture("ColorMap", tex);

    stone_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
    key2.setGenerateMips(true);
    Texture tex2 = assetManager.loadTexture(key2);
    stone_mat.setTexture("ColorMap", tex2);

    floor_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");
    key3.setGenerateMips(true);
    Texture tex3 = assetManager.loadTexture(key3);
    tex3.setWrap(WrapMode.Repeat);
    floor_mat.setTexture("ColorMap", tex3);
  }

  /** Make a solid floor and add it to the scene. */
  public void initFloor() {
    Geometry floor_geo = new Geometry("Floor", floor);
    floor_geo.setMaterial(floor_mat);
    floor_geo.setLocalTranslation(0, -0.1f, 0);
    this.rootNode.attachChild(floor_geo);
    /* Make the floor physical with mass 0.0f! */
    floor_phy = new RigidBodyControl(0.0f);
    floor_geo.addControl(floor_phy);
    bulletAppState.getPhysicsSpace().add(floor_phy);
  }

  /** This loop builds a wall out of individual bricks. */
  public void initWall() {
    float startpt = brickLength / 4;
    float height = 0;
    for (int j = 0; j < 15; j++) {
      for (int i = 0; i < 6; i++) {
        Vector3f vt =
         new Vector3f(i * brickLength * 2 + startpt, brickHeight + height, 0);
        makeBrick(vt);
      }
      startpt = -startpt;
      height += 2 * brickHeight;
    }
  }

  /** This method creates one individual physical brick. */
  public void makeBrick(Vector3f loc) {
    /** Create a brick geometry and attach to scene graph. */
    Geometry brick_geo = new Geometry("brick", box);
    brick_geo.setMaterial(wall_mat);
    rootNode.attachChild(brick_geo);
    /** Position the brick geometry  */
    brick_geo.setLocalTranslation(loc);
    /** Make brick physical with a mass > 0.0f. */
    brick_phy = new RigidBodyControl(2f);
    /** Add physical brick to physics space. */
    brick_geo.addControl(brick_phy);
    bulletAppState.getPhysicsSpace().add(brick_phy);
  }

  /** This method creates one individual physical cannon ball.
   * By defaul, the ball is accelerated and flies
   * from the camera position in the camera direction.*/
   public void makeCannonBall() {
    /** Create a cannon ball geometry and attach to scene graph. */
    Geometry ball_geo = new Geometry("cannon ball", sphere);
    ball_geo.setMaterial(stone_mat);
    rootNode.attachChild(ball_geo);
    /** Position the cannon ball  */
    ball_geo.setLocalTranslation(cam.getLocation());
    /** Make the ball physcial with a mass > 0.0f */
    ball_phy = new RigidBodyControl(1f);
    /** Add physical ball to physics space. */
    ball_geo.addControl(ball_phy);
    bulletAppState.getPhysicsSpace().add(ball_phy);
    /** Accelerate the physcial ball to shoot it. */
    ball_phy.setLinearVelocity(cam.getDirection().mult(25));
  }

  /** A plus sign used as crosshairs to help the player with aiming.*/
  protected void initCrossHairs() {
    guiNode.detachAllChildren();
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    BitmapText ch = new BitmapText(guiFont, false);
    ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
    ch.setText("+");        // fake crosshairs :)
    ch.setLocalTranslation( // center
      settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
      settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
    guiNode.attachChild(ch);
  }
}

Você deveria ver uma parede de tijolos. Clique para disparar bolas de canhão. Assista os tijolos cairem e balançarem um e outro!

Uma Aplicação Física Básica

Nos tutoriais anteriores você usou Geometrias (Geometries0 estáticas (caixas, esferas e modelos) que você colocava na cena. Dependendo da translação delas, Geometrias (Geometries) podem "flutuar no meio do ar" e mesmo se sobrepor – elas não são afetadas pela "gravidade" e não têm massa física. Este tutorial mostra como adicionar propriedades físicas para Geometrias (Geometries).

Como sempre, inicie com uma com.jme3.app.SimpleApplication padrão. Para ativar física crie um com.jme3.bullet.BulletAppState, e ligue ela ao gerenciador de AppState da SimpleApplication.

public class HelloPhysics extends SimpleApplication {
  private BulletAppState bulletAppState;

  public void simpleInitApp() {
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    ...
  }
  ...
}

A BulletAppState dá ao jogo acesso a um Espaço Físico (PhysicsSpace). O Espaço Físico (PhysicsSpace) deixa você usar com.jme3.bullet.control.PhysicsControls que adicionam propriedades físicas para Nós (Nodes).

Ctiando Bolas de Canhão e Tijolos

Geometrias

Neste exemplo "atire na parede" você usa Geometrias (Geometries) como bolas e tijolos. Geometrias contém malhas, tais como Formas (Shapes). Vamos criar e inicializar algumas Formas: Caixas e Esferas.

  /** Prepare geometries and physical nodes for bricks and cannon balls. */
  private static final Box    box;
  private static final Sphere sphere;
  private static final Box    floor;
  /** dimensions used for bricks and wall */
  private static final float brickLength = 0.48f;
  private static final float brickWidth  = 0.24f;
  private static final float brickHeight = 0.12f;
  static {
    /** Initialize the cannon ball geometry */
    sphere = new Sphere(32, 32, 0.4f, true, false);
    sphere.setTextureMode(TextureMode.Projected);
    /** Initialize the brick geometry */
    box = new Box(Vector3f.ZERO, brickLength, brickHeight, brickWidth);
    box.scaleTextureCoordinates(new Vector2f(1f, .5f));
    /** Initialize the floor geometry */
    floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f);
    floor.scaleTextureCoordinates(new Vector2f(3, 6));
  }

Controle de Corpo Rígido (RigidBodyControl): Tijolos

Nós queremos Geometrias (Geometries) de tijolos daquelas caixas. Para cada Geomeotria (Geometry) com propriedades físicas você cria um Controle de Corpo Rígido (RigidBodyControl).

  private RigidBodyControl brick_phy;

Os métodos customziados makeBrick(loc) criam tijolos individuais no local loc. Um tijolo tem as seguintes propriedades:

  • Ele tem uma Geometria (Geometry) visível brick_geo (Geometria de Forma de Caixa).
  • Ele tem propriedades físicas brick_phy (RigidBodyControl)
  public void makeBrick(Vector3f loc) {
    /** Create a brick geometry and attach to scene graph. */
    Geometry brick_geo = new Geometry("brick", box);
    brick_geo.setMaterial(wall_mat);
    rootNode.attachChild(brick_geo);
    /** Position the brick geometry  */
    brick_geo.setLocalTranslation(loc);
    /** Make brick physical with a mass > 0.0f. */
    brick_phy = new RigidBodyControl(2f);
    /** Add physical brick to physics space. */
    brick_geo.addControl(brick_phy);
    bulletAppState.getPhysicsSpace().add(brick_phy);
  }

Estas amostra de código faz o seguinte:

  • Você cria um Geometria (Geometry) do tijolo brick_geo. Uma Geometria (Geometry) descreve a forma e a aparência de um objeto.
    • brick_geo tem uma forma de caixa
    • brick_geo tem um material colorido de tijlo.
  • Você anexa brick_geo para o nó raiz (rootNode_
  • Você posiciona brick_geo em loc.
  • Você cria um RigidBodyControl brick_phy para brick_geo.
    • brick_phy tem uma massa de 2f.
    • Você adiciona brick_phy para brick_geo.
    • Você registra brick_phy para o Espaço Físico PhysicsSpace.

RigidBodyControl: Bola de Canhão

Você nota que a bola do canhão é criada na mesma amneira usando o método personalizado makeCannonBall(). A bola de canhão tem as seguintes propriedades:

  • Ela tem uma Geometria (Geometry) visível ball_geo (Geometria de Forma Esfera)
  • Ela tem propriedades físicas ball_phy (RigidBodyControl)
    /** Create a cannon ball geometry and attach to scene graph. */
    Geometry ball_geo = new Geometry("cannon ball", sphere);
    ball_geo.setMaterial(stone_mat);
    rootNode.attachChild(ball_geo);
    /** Position the cannon ball  */
    ball_geo.setLocalTranslation(cam.getLocation());
    /** Make the ball physcial with a mass > 0.0f */
    ball_phy = new RigidBodyControl(1f);
    /** Add physical ball to physics space. */
    ball_geo.addControl(ball_phy);
    bulletAppState.getPhysicsSpace().add(ball_phy);
    /** Accelerate the physcial ball to shoot it. */
    ball_phy.setLinearVelocity(cam.getDirection().mult(25));

Esta amostra de código faz o seguinte:

  • Você cria uma Geometria (Geometry) de tijolo ball_geo. Uma Geometria (Geometry) descreve a forma e aparência de um objeto.
    • ball_geo tem uma forma esfera
    • ball_geo tem um material cor de pedra.
  • Você anexa ball_geo para o nó raiz (rootNode)
  • Você posiciona ball_geo na localização da câmera
  • Você cria um RigidBodyControl ball_phy para ball_geo.
    • ball_phy tem uma massa de 1f.
    • Você adiciona ball_phy para ball_geo.
    • Você registra ball_phy para o Espaço Físico (PhysicsSpace).

Desde que você está disparando bolas de canhão, a última linha acalera a bola na direção que a câmera está olhando, com uma velocidade de 25f.

RigidBodyControl: Chão

O chão (estático) tem uma importante diferença comparado aos tijolos (dinâmicos) e bolas de canhão: Objetos estáticos têm uma massa zero. Como antes, você escreve um método initFloor() customizado que cria uma caixa plana com uma textura de rocha que você usa como chão. O chão tem as seguintes propriedades:

  • Ele tem uma Geometria (Geometry) visível floor_geo (Geometria de Forma Caixa)
  • Ele tem propriedades físicas floor_phy (RigidBodyControl)
  public void initFloor() {
    Geometry floor_geo = new Geometry("Floor", floor);
    floor_geo.setMaterial(floor_mat);
    floor_geo.setLocalTranslation(0, -0.1f, 0);
    this.rootNode.attachChild(floor_geo);
    /* Make the floor physical with mass 0.0f! */
    floor_phy = new RigidBodyControl(0.0f);
    floor_geo.addControl(floor_phy);
    bulletAppState.getPhysicsSpace().add(floor_phy);
  }

Esta amostra de código faz o seguinte:

  • Você cria uma Geometria (Geometry) de chão floor_geo. Uma Geometria (Geometry) descreve a forma e a aparência de um objeto.
    • floor_geo tem uma forma de caixa
    • floor_geo tem um material cor de cascalho.
  • Você anexa floor_geo para o nó raiz (rootNode)
  • Você posiciona floor_geo um pouco abaixo y=0 (para prevenir sobreposição com outros Espaciais Controlados por Física).
  • Você cria um RigidBodyControl floor_phy para floor_geo.
    • floor_phy tem uma massa de 0f :!:
    • Você adiciona floor_phy para floor_geo.
    • Você registra floor_phy para o Espaço Físico (PhysicsSpace_.

Criando a Cena

Vamos dar uma olhada nos métodos prestativos customizados:

  • initMaterial() – Este método inicializa todos os materiais que nós usamos neste demo.
  • initWall() – um loop duplo que gera uma parede por posicionar objetos tijolos: 15 linhas de altura com 6 tijolos por linha. É importante dar um espaço para os tijolos físicos para que eles não se sobreponham.
  • initCrossHairs() – Este método simplesmente exibe um sinal de mais que você usa como crosshairs para mirar. Note que elementos da tela como crosshairs são anexados para o nó da GUI (guiNode), não para o nó raiz (rootNode)!
  • initInputs() – Este método configura a ação clique para disparar.

Cada um destes métodos é chamado uma vez no método simpleInitApp() no início do jogo. Como você vê, você pode escrever qualquer número de métodos customizados para configurar a cena do seu jogo.

A ação de Disparar Bola de Canhão

No método initInputs(), você adiciona um mapeamento de entrada que ativa uma ação de disparar quando o botão esquerdo do mouse é pressionado.

  private void initInputs() {
    inputManager.addMapping("shoot", 
            new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addListener(actionListener, "shoot");
  }

Você define a ação verdadeira de disparar uma bola de canhão como se segue:

    private ActionListener actionListener = new ActionListener() {
        public void onAction(String name, boolean keyPressed, float tpf) {
            if (name.equals("shoot") && !keyPressed) {
                makeCannonBall();
            }
        }
    };

No momento em que a bola de canhão aparece na cena ela voa com a velocidade (e a direção) que você especificou usando setLinearVelocity() dentro de makeCannonBall(). A bola de canhão recém criada voa, atinge a parede e exerce uma força que impacta os tijolos individualmente.

Movendo um Espacial Físico

A localização de espaciais dinâmicos é controlada por seu RigidBodyControl. Mova o RigidBodyControl para mover o Espacial. Se é um PhysicsControl dinâmico você pode usar setLinearVelocity() e aplicar forças e torques para ele. Outros objetos Controlados por Corpo Rígidos (RigidBodyControl) podem empurrar o Espacial (Spatial) dinãmico ao redor (como bolas de bilhar).

Você pode fazer Espaciais que não são dinãmicos: Mude o RigidBodyControl para setKinematic(true) para que ele se mova junto de seu Espacial (Spatial).

  • Um cinemático não é afetado pelas forças nem pela gravidade, o que significa que ele pode flutuar no meio de ar e não pode ser empurrado pelas "bolas de canhão" dinâmicas etc.
  • Um Corpo Rígido (RigidBody) cinemático tem uma massa.
  • Um cinemático pode ser movido e pode exercer forças em Corpos Rígidos (RigidBodys) dinâmicos. Isto significa que você pode usar um nó cinemático como uma fila de bilhar ou um ariete controlado remotamente.

Aprenda mais sobre estático versus cinemático versus dinâmico no documento de física avançada.

Exercícios

Exerício 1: Formas de Debug

Adicione a seguinte linha depois da inicialização de bulletAppState.

bulletAppState.getPhysicsSpace().enableDebug(assetManager);

Agora você vê as Formas de colisão (collisionShapes) dos tijolos e das esferas, e o chão destacado.

Exercício 2: Sem Mo estático

O que acontece se você dar a um nó estático como o chão, uma massa de mais que 0.0f?

Exerício 3: Atrás das Cortinas

Preencha suas cenas com paredes, tijolos e bolas de canhão. Quando você inicia a ver um impacto de desempenho?

Jogos AAA populares usam uma mistura inteligente de física, animação e gráficos pré-renderizados para dar a você a ilusão de mundo "físico", real. Pense de seus video games favoritos e tente encontrar onde e como os projetistas do jogo enganam você em acreditar que a cena inteira é física. Por exemplo, pense de um edifício se "partindo" de 4-8 partes depois de uma explosão. As peças que mais provavelmente voam em caminhos pré-definidos (assim chamados cinemáticos) e que são somente substituídos por Espaciais dinâmicos depois que eles tocam o chão… Agora que você iniciar a implementar um jogo físico por si mesmo, olhe atrás das cortinas!

Usar física em todo lugar em um jogo parece uma idéia legal, mas ela é facilmente exagerada. Embora os nós físicos são postos para "dormir" quando eles não estão se movendo, criar um mundo unicamente de nós físicos rapidamente trará você aos limites das capacidades de seu computador.

Conclusão

Você aprendeu a como ativar o Espaço Físico (PhysicsSpace) da jBullet em uma aplicação por adicionar um BulletAppState. Você criou PhysicsControls para simpels Geometrias baseadas em Forma (para formas mais complexas leia sobre CollisionShapes). Voc~e aprendey qye objetos físicos não são somente anexados para o nó raiz (rootNode), mas também registrados para o Espaço Físico (PhysicsSpace). Você sabe que faz diferença se um objeto físico tem uma massa (dinâmico) ou não (estático). Você está consciente que "super-usar" físicas tem um enorme impacto de desempenho.

parabéns! - Você completou o último tutorial de iniciante. Agora você está pronto para iniciar a combinar o que você aprendeu, para criar um jogo legal 3D por si mesmo. Mostre-nos o que você pode fazer e sinta-se livre para compartilhar seus demos, vídeos de jogos, e instantâneos no fórum de Anúncios Livres (Free Announcements)!

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.