Introduction:
Hello everyone! I recently got started with jME3, and like many others, I’ve decided I want to make a super-awesome video game. The problem is, there are few tutorials on the complete process that a beginner like myself can follow from beginning to end. In fact, the most complete game tutorial I’ve found (FlagRush) was built in jME2, and it doesn’t utilize a physics engine.
I figured that since I’m going to have to ask questions and learn this all anyway, I might as well document and explain my project. That way, other beginners will at least have my code to start on. Hopefully, I’ll even be able to get a simple but reasonably complete game coded, and this project will be a useful resource.
Of course, none of this will work without help from the experts, so please share your wisdom!
Objective:
The goal is to create a first-person shooter in jME3, using the integrated jBullet physics. The controls will work like any mainstream shooter I’m sure you’re familiar with: mouse to aim and rotate the character, and keys to control strafing movement. In the beginning, I’ll focus on getting the controls and mouse picking working, using standard primitives. Later, I’ll focus on importing models and creating the collision mesh using compound collision shapes. With luck, I’ll also be able to get to model animations and sound.
Format:
I’ll start by posting the code and explaining how I coded it and what it does. Subsequent posts should utilize the answers to the previous post’s code.
Part 1
import com.jme3.app.SimpleBulletApplication;
import com.jme3.asset.TextureKey;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.nodes.PhysicsCharacterNode;
import com.jme3.bullet.nodes.PhysicsNode;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
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;
public class SimpleFPS extends SimpleBulletApplication
{
private PhysicsCharacterNode player;
private Vector3f direction = new Vector3f();
// private ActionListener actionListener;
public static void main(String[] args)
{
SimpleFPS app = new SimpleFPS();
app.start();
}
public void simpleInitApp()
{
Material mat = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
TextureKey key = new TextureKey("Interface/Logo/Monkey.jpg");
Texture tex = assetManager.loadTexture(key);
mat.setTexture("m_ColorMap", tex); // No idea what the "m_ColorMap" does.
Geometry floor = new Geometry("floor", new Box(Vector3f.ZERO, 30f, .5f, 30f));
floor.setMaterial(mat);
PhysicsNode physicsFloor = new PhysicsNode(floor, new BoxCollisionShape(new Vector3f(30f, .5f, 30f)), 0); // "Half extents," last zero is mass.
physicsFloor.setLocalTranslation(0, -5, 0);
physicsFloor.updateModelBound();
physicsFloor.updateGeometricState();
rootNode.attachChild(physicsFloor);
getPhysicsSpace().add(physicsFloor);
Geometry junk = new Geometry("junk", new Box(Vector3f.ZERO, 2f, 2f, 2f));
junk.setMaterial(mat);
PhysicsNode physicsJunk = new PhysicsNode(junk, new BoxCollisionShape(new Vector3f(2, 2, 2)));
physicsJunk.setLocalTranslation(0, 5, -5);
physicsJunk.updateModelBound();
physicsJunk.updateGeometricState();
rootNode.attachChild(physicsJunk);
getPhysicsSpace().add(physicsJunk);
Geometry ball = new Geometry("ball", new Sphere(16, 16, 1));
ball.setMaterial(mat);
player = new PhysicsCharacterNode(ball, new SphereCollisionShape(1), .5f); // (radius), mass.
player.setLocalTranslation(0, 3, 1);
player.updateGeometricState();
player.updateModelBound();
rootNode.attachChild(player);
getPhysicsSpace().add(player);
player.setJumpSpeed(10);
player.setFallSpeed(10);
player.setGravity(20);
inputStuff(); // My input class.
}
private void inputStuff()
{
inputManager.addMapping("forward", new KeyTrigger(KeyInput.KEY_W));
inputManager.addMapping("backward", new KeyTrigger(KeyInput.KEY_S));
inputManager.addMapping("strafeLeft", new KeyTrigger(KeyInput.KEY_A));
inputManager.addMapping("strafeRight", new KeyTrigger(KeyInput.KEY_D));
inputManager.addMapping("jump", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(actionListener, new String[] { "forward", "backward", "strafeLeft", "strafeRight", "jump" });
}
private ActionListener actionListener = new ActionListener()
{
public void onAction(String name, boolean keyPressed, float tpf)
{
Vector3f camDir = cam.getDirection().mult(.05f);
Vector3f camLeft = cam.getLeft().mult(.05f);
if (name.equals("forward"))
{
direction.set(camDir);
}
else if (name.equals("backward"))
{
camDir.negateLocal();
direction.set(camDir);
}
if (name.equals("strafeLeft"))
{
direction.set(camLeft);
}
else if (name.equals("strafeRight"))
{
camLeft.negateLocal();
direction.set(camLeft);
}
else if (name.equals("jump"))
{
player.jump();
}
}
};
public void simpleUpdate(float tpf)
{
player.setWalkDirection(direction);
// direction.set(Vector3f.ZERO); // Doesn't work - it zeroes before the setWalkDirection is "applied." Don't know where this happens.
}
}
If you run this, you should see a monkey-textured box and sphere on top of a monkey-textured floor. The reason for the monkey texture is so you can see the shapes; without lights and the corresponding shadows and gradient, only the outlines of solid colors would be visible. Assuming you know how the nodes work, most of the simpleInitApp() should be... simple!
Material mat = new Material(assetManager, "Common/MatDefs/Misc/SimpleTextured.j3md");
TextureKey key = new TextureKey("Interface/Logo/Monkey.jpg");
Texture tex = assetManager.loadTexture(key);
mat.setTexture("m_ColorMap", tex); // No idea what the "m_ColorMap" does.
This is just creating a material; I stole this from the materials tutorial. The program crashes if you don't assign a material.
Geometry floor = new Geometry("floor", new Box(Vector3f.ZERO, 30f, .5f, 30f));
floor.setMaterial(mat);
PhysicsNode physicsFloor = new PhysicsNode(floor, new BoxCollisionShape(new Vector3f(30f, .5f, 30f)), 0); // "Half extents," last zero is mass.
physicsFloor.setLocalTranslation(0, -5, 0);
physicsFloor.updateModelBound();
physicsFloor.updateGeometricState();
rootNode.attachChild(physicsFloor);
getPhysicsSpace().add(physicsFloor);
Most of this is covered in the nodes tutorial. Notice that I'm not using the box's mesh; I'm using a BoxCollisionShape. When I import or create terrain, this will have to be changed. The 0 mass means this is a static physics object, and doesn't fall due to gravity.
Geometry junk = new Geometry("junk", new Box(Vector3f.ZERO, 2f, 2f, 2f));
junk.setMaterial(mat);
PhysicsNode physicsJunk = new PhysicsNode(junk, new BoxCollisionShape(new Vector3f(2, 2, 2)));
physicsJunk.setLocalTranslation(0, 5, -5);
physicsJunk.updateModelBound();
physicsJunk.updateGeometricState();
rootNode.attachChild(physicsJunk);
getPhysicsSpace().add(physicsJunk);
More of the same, except this box is a lot smaller and does fall. I've included it mostly to test collision. You have to add the physics node to the physics space to let the program know it needs to run physics on this!
Geometry ball = new Geometry("ball", new Sphere(16, 16, 1));
ball.setMaterial(mat);
player = new PhysicsCharacterNode(ball, new SphereCollisionShape(1), .5f); // (radius), mass.
player.setLocalTranslation(0, 3, 1);
player.updateGeometricState();
player.updateModelBound();
rootNode.attachChild(player);
getPhysicsSpace().add(player);
Believe it or not, this sphere is going to be our player for now. I'm using a PhysicsCharacterNode included with jBullet; it seems to have many of the features I need already built in!
player.setJumpSpeed(10);
player.setFallSpeed(10);
player.setGravity(20);
inputStuff(); // My input method.
I stole the first but from the Quake 3 demo; after those tweaks, it runs the input method. I had to modify the input from both the Q3 demo and the character demo; those methods seem to be depreciated. I used Hello Input as my guide here.
private void inputStuff()
{
inputManager.addMapping("forward", new KeyTrigger(KeyInput.KEY_W));
inputManager.addMapping("backward", new KeyTrigger(KeyInput.KEY_S));
inputManager.addMapping("strafeLeft", new KeyTrigger(KeyInput.KEY_A));
inputManager.addMapping("strafeRight", new KeyTrigger(KeyInput.KEY_D));
inputManager.addMapping("jump", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(actionListener, new String[] { "forward", "backward", "strafeLeft", "strafeRight", "jump" });
}
This is modeled from the Hello Input tutorial, above. It's pretty intuitive.
private ActionListener actionListener = new ActionListener()
{
public void onAction(String name, boolean keyPressed, float tpf)
{
Vector3f camDir = cam.getDirection().mult(.05f);
Vector3f camLeft = cam.getLeft().mult(.05f);
if (name.equals("forward"))
{
direction.set(camDir);
}
else if (name.equals("backward"))
{
camDir.negateLocal();
direction.set(camDir);
}
if (name.equals("strafeLeft"))
{
direction.set(camLeft);
}
else if (name.equals("strafeRight"))
{
camLeft.negateLocal();
direction.set(camLeft);
}
else if (name.equals("jump"))
{
player.jump();
}
}
};
This is from the Q3 demo. This is what happens when the key you pressed is detected. Note the vectors for camDir and camLeft: this is so the player will move relative to where you look. For example, if you turn 45 degrees right and strafe left, you'll go left relative to your new heading. Interestingly, the actual sphere doesn't change direction. It's not a big deal now, because it's a sphere, but when I use a model for the character, I'll have to rotate it to face the way the camera is looking.
public void simpleUpdate(float tpf)
{
player.setWalkDirection(direction);
// direction.set(Vector3f.ZERO); // Doesn't work - it zeroes before the setWalkDirection is "applied." Don't know where this happens.
}
The commented line is explained next, where I talk about what this program does and what needs to be improved.