Tutorial 8 - Hello Picking

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
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.