SimpleFPS game – FINISHED! With much shooting

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.
3 Likes

Looks cool, gratz. Also thanks for the detailed dev report :slight_smile:

Problems and Questions:


  1. When you run the program, you should notice the sphere moving with your WASD keys. If you turn the camera, the sphere behaves relative to the new orientation, as it should. However, the player doesn't stop when the key is released. This is my crummy coding job - I never tell it to zero the "direction" vector. For some reason, this wasn't a problem with the depreciated input. I tried the commented-out line near the bottom: it didn't do what I expect, which was to zero the vector after each move. Instead, it stopped all movement, so the way the loop runs is unclear to me. Anyone know how I can make the player stop when the keys are released?



    (The camera isn't "first person," but it's easier to see what's happening for now. Later, I'll move the camera inside the sphere.)


  2. When should the following methods be called?


physicsFloor.updateModelBound();
      physicsFloor.updateGeometricState();



3. The following appears in the log:

warning CollisionDispatcher.needsCollision: static-static collision!



Is this bad? I assume one is the floor; the other can only be the player.

To the material question,



look into the file you loaded, jm3d or so, think of it as a material class, it defines behaviour and parameters,

then you instanciate a object of the class, setting the parameters, and the shaders use those to render to model ocrrectly.



In your case you set the the variable with the texture value, wich in this material defines, wich texture to use when rendering.



The texture key is used, so that you don't have copyes, but instead use reference to only one instance of the loaded texture.(as far as I understood)





To the vector, it might be that the direction vector is applied next physic update, wich is around 60hz by defualt (no matter the fps you have)

now you pass a reference to it, then set the stored one to zero, so the reference now points at the same vector but the contet of it has changed before the physic update is processed. (java uses for objects a mixture of reference /copy, beware here that there are some immutable classes like STring, if you store them in a variable the reference stored actually changes, and is not the same anymore.


I don't think I understand what you suggested. The reason the depreciated class worked is because it had a onPreUpdate method, so the direction vector was set to 0 there and overwritten only if a key was pressed. I think that by passing a reference to the direction vector, the intent is to make it !null when a key has been pressed. But I can't have the vector defined only in one loop, because the binding and update loops are different and run at different speeds. So if I define "Vector3f direction;" and later set it, it will keep that information even when I release a key.



Furthermore, about what you said about immutable classes, this code causes no change in behavior:


public void simpleUpdate(float tpf)
   {
      Vector3f d2 = direction;
      direction = Vector3f.ZERO;
      player.setWalkDirection(d2);
   }



But this code, which seems the same, cancels all movement:


public void simpleUpdate(float tpf)
   {
      Vector3f d2 = direction;
      direction.set(Vector3f.ZERO);
      player.setWalkDirection(d2);
   }



I think this is what you said, and one is a copy while the other just points to the same thing? Thanks for your help, and please advise further regarding setting the vector!

Thank you for the thread, this is going to help a lot of people, including me.



I have one question



InputManager doesn't work like that in the TestJme3 examples, and it's not compiling whith my JME3 version (last update from nightly builds).

My question is what version of JME3 do you use?





thanks

I was using the 7/3 nightly build. From what I understand, the whole input system is undergoing revision; the stuff from the examples (Q3 and test character node) use an old version Eclipse displays as crossed-out and depreciated. It still works, but the movement is jerky and inconsistent. The "new" method from the input tutorial on the wiki doesn't have the onPreUpdate() method, which is what's blocking my progress.



The same code using the "old" registerKeyBinding() method does exactly what it's supposed to in the old versions of jME3, like the platform. I'm trying to replicate this functionality with the new input version.



Also, I tried the 7/4 build and I didn't notice any changes. The following should compile and run fine, but the sphere will continue to move after the key has been released:



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();
   
   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);
      
      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(.10f);
         Vector3f camLeft = cam.getLeft().mult(.10f);
      
         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") && keyPressed)
         {
            player.jump();
         }
      }
   };
   
   public void simpleUpdate(float tpf)
   {
      player.setWalkDirection(direction);
   }
}



Also, I added the "&& keyPressed" to the jump action. This way, the character only jumps when you press space, and not again when you release it. If it was "!keyPressed," the character would jump only when you released the button.

the quake loading thingy, always was kinda jerky for me, I guess this is more a collision issue.

I thought i had the latest build version, but it's seems i was wrong.



udated to the 7/4 and your example works

That's strange JMP failed to retrieve the latest updates till 7/1.



About the non-stoping walking character

I'm trying to use PhysicsCharacterNode for my character, and at some point…i'll have to make it stop walking :stuck_out_tongue:

I have the same issue as you, i'll look into it and post my progress here.




regarding the going,

i have a private vector variable movevec in my player,



in update(){

movevec.set(Vector.ZERO))



if key w then movevector.add(forward);

ect for all keys

then setting it.



}



As long as the input is longer than one jbullet tick this works, wich should be in most cases, as presseing a button with more than 60hz is kinda unlikly.

Thanks Empire! I got an idea while looking at your post, but I should have figured it out before. Once I used booleans it worked and I was on familiar ground, so I took it further and added some nifty features, which I'll explain below.



Part II



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 boolean[] move = new boolean[4];
   
   private float baseMoveX = .2f;
   private float baseMoveZ = .2f;
   
   private float addMoveX = .001f;
   private float addMoveZ = .001f;
   
   private float maxMoveX = .3f;
   private float maxMoveZ = .3f;
   
   private float speedX = baseMoveX;
   private float speedZ = baseMoveZ;
   
   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)
      {
         if (name.equals("forward"))
         {
            move[0] = keyPressed;
         }
         else if (name.equals("backward"))
         {
            move[1] = keyPressed;
         }
         if (name.equals("strafeLeft"))
         {
            move[2] = keyPressed;
         }
         else if (name.equals("strafeRight"))
         {;
            move[3] = keyPressed;
         }
         else if (name.equals("jump") && keyPressed)
         {
            player.jump();
         }
      }
   };
   
   public void simpleUpdate(float tpf)
   {
      Vector3f camDir = cam.getDirection().mult(speedZ);
      Vector3f camLeft = cam.getLeft().mult(speedX);
      
      Vector3f moveDir = new Vector3f(0, 0, 0);
      Vector3f moveLeft = new Vector3f(0, 0, 0);
      
   //   System.out.println((double)camDir.x + "," + (double)camDir.y + ", " + (double)camDir.z);
      
      if (move[0])
      {
         moveDir.set(camDir);
         if (speedZ < maxMoveZ)
            speedZ += addMoveZ;
      }
      else if (move[1])
      {
         camDir.negateLocal();
         moveDir.set(camDir);
         if (speedZ < maxMoveZ)
            speedZ += addMoveZ;
      }
      else
      {
         speedZ = baseMoveZ;
      }
      
      if (move[2])
      {
         moveLeft.set(camLeft);
         if (speedX < maxMoveX)
            speedX += addMoveX;
      }
      else if (move[3])
      {
         camLeft.negateLocal();
         moveLeft.set(camLeft);
         if (speedX < maxMoveX)
            speedX += addMoveX;
      }
      else
      {
         speedX = baseMoveZ;
      }
      
      direction.set(moveDir.addLocal(moveLeft));
      
      player.setWalkDirection(direction);
   }
}



If you run this code, you should see the ball (finally) reacting appropriately to your inputs. Multiple key presses should move the ball at oblique angles, it stops when you release the key, and there should be no problems moving in any direction while rotating the mouse. Better yet, you may notice that the ball starts out slow and then speeds up the longer you hold the key down. If you're designing a realistic shooter, you probably don't want your players bouncing around dodging bullets. Plus, each axis is interdependent. This means you could set a "sprint" feature, so you can increase speed in the forward direction while reducing it in the lateral direction. There are a bunch of new variables, so I'll explain some things further.

   private float baseMoveX = .2f;
   private float baseMoveZ = .2f;
   
   private float addMoveX = .001f;
   private float addMoveZ = .001f;
   
   private float maxMoveX = .3f;
   private float maxMoveZ = .3f;
   
   private float speedX = baseMoveX;
   private float speedZ = baseMoveZ;



If you want to play with the program, these variables control every aspect of the player's movement. The base and max speeds are pretty self-explanatory. The add speed is the acceleration, and is added to the axis when the move speed is less than the max. For an arcade shooter, you can set the base speed high, to give players high initial agility. Setting the base at 0 creates a mushy feel, but it could be good for more strategic games. Each axis is independent, so although they're set the same here, you can make your character strafe more slowly than it runs.

private boolean[] move = new boolean[4];



The button presses no longer control the direction vector - they just change their corresponding boolean array value instead. These are set like this:

private ActionListener actionListener = new ActionListener()
   {
      public void onAction(String name, boolean keyPressed, float tpf)
      {
         if (name.equals("forward"))
         {
            move[0] = keyPressed;
         }
         else if (name.equals("backward"))
         {
            move[1] = keyPressed;
         }
         if (name.equals("strafeLeft"))
         {
            move[2] = keyPressed;
         }
         else if (name.equals("strafeRight"))
         {;
            move[3] = keyPressed;
         }
         else if (name.equals("jump") && keyPressed)
         {
            player.jump();
         }
      }
   };



The actionListener is called once when you press a key (keyPressed = true) and once when you release it (keyPressed = false). This sets the boolean to the keyPressed, meaning the boolean is true when the key is down, and false when it's up. The jump button controls the character directly still, since it was problem-free.

public void simpleUpdate(float tpf)
   {
      Vector3f camDir = cam.getDirection().mult(speedZ);
      Vector3f camLeft = cam.getLeft().mult(speedX);
      
      Vector3f moveDir = new Vector3f(0, 0, 0);
      Vector3f moveLeft = new Vector3f(0, 0, 0);
      
   //   System.out.println((double)camDir.x + "," + (double)camDir.y + ", " + (double)camDir.z);
      
      if (move[0])
      {
         moveDir.set(camDir);
         if (speedZ < maxMoveZ)
            speedZ += addMoveZ;
      }
      else if (move[1])
      {
         camDir.negateLocal();
         moveDir.set(camDir);
         if (speedZ < maxMoveZ)
            speedZ += addMoveZ;
      }
      else
      {
         speedZ = baseMoveZ;
      }
      
      if (move[2])
      {
         moveLeft.set(camLeft);
         if (speedX < maxMoveX)
            speedX += addMoveX;
      }
      else if (move[3])
      {
         camLeft.negateLocal();
         moveLeft.set(camLeft);
         if (speedX < maxMoveX)
            speedX += addMoveX;
      }
      else
      {
         speedX = baseMoveZ;
      }
      
      direction.set(moveDir.addLocal(moveLeft));
      
      player.setWalkDirection(direction);
   }



The beefed-up update method now handles the speed. The code should be easy enough to understand; there are two separate vectors for the X and Z axes, and these are added at the end. Notice that I only use the "set" instead of "add," because otherwise we'd have problems when turning while moving.

Right now, we could translate the camera inside the sphere, and we'd have the basic FPS-style controls down. However, I'm leaving it outside so it's easier to tell what's happening when I add a character collision model. But before that, there's one more important feature to add...

Questions:

Frame-rate independent movement! Right now, movement updates without regard to tpf. Any advice on how to standardize this?

Well I slow my game logik down to 50ms per tick resulting in a fixed update rate, also they still have the normal tpf update,

as well as an interpolation controller, taking care of the transition between the 50ms steps (sounds compilcated, but is needed since it runs over network). Turning is clientside, so you see no lag, also it is with the faster update, while the rest of the steering data is only send to the server in the pauses inbetween. (server side you lag around yourself if you rotate)





another solution would be to calcualte every stuff with the tpf in regard, kinda like the higer the tfp the more it must move per frame as the framerate is lower.

nehon said:

That's strange JMP failed to retrieve the latest updates till 7/1.

jMP does not update to jme versions its not yet compatible with, rather smart than strange I'd say ;)
normen said:

jMP does not update to jme versions its not yet compatible with, rather smart than strange I'd say ;)

Good point!  ;)

Hey there kidneytrader.

Just letting you know I'll be following your progress closely as I'm in the same (or perhaps a sligthly more nonadjacent) position as yourself :stuck_out_tongue:



Keep at it!

Thanks! I'm learning how to rig and animate stuff, which is why there hasn't been any progress on the code recently.

kidneytrader said:

Thanks! I'm learning how to rig and animate stuff, which is why there hasn't been any progress on the code recently.
Any help you need setting up a wiki series for this when the time comes just gimme a shout. Wiki registrations has been down for a while due to a miscommunication, but it's available again now, though soon to be replaced by our awesome new collaboration suite.

I haven’t forgotten about this thread - this is a learning experience for me, too, and I just got a properly UV mapped and animated character into jME!



Part (2.5?)







Obviously I can’t explain all about how to create the assets, but in case anyone is curious, I will describe my process. I model and UV map using 3ds Max (personal preference - I’m familiar with it, and Autodesk generously offers free student versions), then export to .obj and import it into Blender to do the animations (nifty armature and weight paint tools) before exporting again to the Ogre format.



Now, on to business: Before we can get to the really fun stuff, we have to be able to detect collisions with the actual character. The mesh is just a visual model, and doesn’t serve any other function besides visually displaying. The sphere previously used for the character controller is accurate enough to detect character collisions with geometry - if we stuck an animated visual model inside it, we could stop the player from going through walls and floors. However, since we want to be able to shoot our characters, we need a much more accurate collision mesh to let us know if we actually hit a more accurate representation of the visual character.



I’m not an expert on the topic, but even if it was possible to generate a collision mesh from the asset mesh, it would be very inefficient (conformation needed). Instead, we can use many simple primitives (like cylinders and boxes) for each limb, and then connect them to the bones from the model we imported. That means if our character is playing a walking animation, the primitives will move along with the bones in the animation. These shapes will form out hitboxes - in the method to check collisions, if the “shot” intersects the character controller sphere, we can then check for intersections against these primitives and we will know where are character is hit.



I hope this explanation is clear, but if not, maybe this picture will make more sense:







This is also how commercial engines (like UE3 - Mass Effect, Gears of War, Batman, etc.) test for bullet collisions.



To actually implement this in code, we need to form primitives that closely follow the part of the mesh that it represents. Then, we need to use methods to get the related bone’s position and rotation. After that, we just tell the shape to follow the bone by setting it’s position and rotation in update. That way, if your character has a walking animation, and you shoot it in the leg while it’s playing a walking animation, we can detect collisions accurately because the hitbox is based on the actual visual mesh’s controller.



Going a bit further, we can also do something closely relayed - setting bone positions from the code. Animations are fine for walking, etc., but if we want our character to point his gun where we’re looking, we need to make the arm bones point to the camera vector.



(Just FYI, the above mesh was a rushed five-minute job to test everything, and I’ll probably make a better character to use for this tutorial before long. It’ll have arms, so it can use the set bone rotation methods. Anyway, I’ve included the files for the above character, including the .blend, in case anyone wants to test something or mess around.)



So, my question is:



  1. What are the methods we can use to detect a bone’s position and rotation? I’m sure the “get” method involves the AnimController class, but I don’t know what it is specifically. I don’t even know if the “set” method exists. (And I checked the docs.)



    Edit: I didn’t actually use any of this because jME already detects collisions accurately on an animated mesh.
1 Like
kidneytrader said:

So, my question is:

1. What are the methods we can use to detect a bone's position and rotation? I'm sure the "get" method involves the AnimController class, but I don't know what it is specifically. I don't even know if the "set" method exists. (And I checked the docs.)
It's okay if you re-post that question in a separate thread. Actually I'd recommend it, since someone in a rush might just go 'cool pictures; he's making progress; will check back later'. Besides, the question is so general it'd be nice to have it in its own thread for easy searching.

I'm following your project with great delight :) Just can't ever be enough game samples for jME3! Loads of people learned jME2 by and large with the help of the 'flag rush tutorial'. I'm sure we'd widen our appeal if our docs could offer greater variety of games in differing genres and complexity.



Whoops.