Tutorial 9 - Hello Collision

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