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).