Achieving a proper First Person Shooter Camera

I think I asked a question related to creating a fps camera before, but I have decided to take an approach that is listed in a Unity3d tutorial as found in this video:

Before I proceed with development my questions are:

  1. is it possible to set a camera or a cameranode to depth only
  2. can I have a “layer” in which the camera shows only part of a scene as show in the Unity Manual:
  3. Changing the depth on a camera, would getViewToProjectionZ as found in the Jmonkey docs do the trick?
  4. can I set a “culling mask” to a camera or a cameranode
  5. is there a tutorial for doing full mouse look with a camera node
    Please let me know, any help will be appreciated.

It might be helpful if you described what you meant by “proper first person shooter camera”. What makes it ‘proper’?

By proper I mean being able to collide with some thing and not have the weapon model go through the wall.

So, from your description it sounds like you want to fake it, yes? Just have a gun picture overlaid on the screen and not really be a part of the scene? Or is it just that you want the physics to not allow the gun to penetrate the wall?

Thank you for your prompt reply. Hmmm, it seems as if the links I posted on the original post did not show up

I think I asked a question related to creating a fps camera before, but I have decided to take an approach that is listed in a Unity3d tutorial as found in this video:

Before I proceed with development my questions are:

  1. is it possible to set a camera or a cameranode to depth only
  2. can I have a “layer” in which the camera shows only part of a scene as show in the Unity Manual:
  3. Changing the depth on a camera, would getViewToProjectionZ as found in the Jmonkey docs do the trick?
  4. can I set a “culling mask” to a camera or a cameranode
  5. is there a tutorial for doing full mouse look with a camera node
    Please let me know, any help will be appreciated.
So the Unity3d tutorial link was: https://www.youtube.com/watch?v=RLzssixo7SE

and the link in the Unity manual:

Well I tried the approach which was using viewports, but I’m not too satisfied by the results I’m getting, since the lighting and shadows were pretty much static. I noticed in the approach done in the Unity3d tutorial the lighting and shadows did show up on the first person hands. I’ve also tried making the CapsuleCollision radius big enough to encompass the weapon within the CapsuleCollision, but I can see that leading to problems later on because if I have a larger weapon I will need to enlarge the CapsuleCollision model to encompass this. I’m looking for an approach of a first person weapon that is found in most modern day first person shooters such as Bioshock, Crysis, Halo, Battlefield, to name a few, the weapon doesn’t go through wall and in addition the player collision pretty much stays the same no matter which weapon the player would use. I hope this will provide enough for my question to be answered.

So, if this were multiplayer, it would be ok to see other players stick their guns through walls… just not your own?

If it were me, I’d prefer just not having the gun stick through things in the first place by giving it a collision shape and attaching that to the player somehow. I think it’s strange when my giant gun is still in front of me when I’m so close to a door that I can chat with the termite eating the wood…

You could turn off depth testing for the gun but then it won’t test against itself either.

The easiest way is still probably with viewports and just fixing the lighting… even with a separate viewport you can still move that viewport’s camera + gun and just make sure to add the lights in both places. (I think the viewports can even share the same camera.) If you use shadow rendering, the shadows won’t look right on the gun… but then that’s impossible anyway because your gun can physically poke through walls even if you aren’t displaying it that way.

Once again thank you for you posting, I think in the case of a multiplayer game to someone who is not the player the weapon would go through the wall, but from a players perspective it would show that the weapon isn’t going through the wall. I guess I could try the approach that is done ARMA?

I guess this might seem as a more reasonable way to do the first person weapon handeling

In arma this is by design btw, the idea here is that you wont run with a ironsighted assault rifle trough a house, as it will hinder your movement.
-> Use a pistol or smg for this.

But using a similar system is kinda usefull, if you don’t care that you cannot shoot in certain situations then. eg if you stand behind a small wall you can look over, you need additional code to handle this, so the player holds the weapon over the obstacle.

I don’t know what the link is, but one of the solutions was to put the weapon in the Bucket.Translucent queue. I’m not sure if I’m doing it correctly because I’m still getting the weapon through wall problem.
prob

And here is my code incase anyone wants to see it, am I using the QueueBuckets correctly?

public class Main 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;
 
  //Temporary vectors used on each frame.
  //They here to avoid instanciating new vectors on each frame
  private Vector3f camDir = new Vector3f();
  private Vector3f camLeft = new Vector3f();
 
  //Render Queue
  private RenderQueue renderQueue;
  //Gun Node
  private Node weapon;
  
  public static void main(String[] args) {
    Main app = new Main();
    app.start();
  }
 
  public void simpleInitApp() {
    /** Set up Physics */
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);
    //bulletAppState.getPhysicsSpace().enableDebug(assetManager);
    renderQueue = new RenderQueue();
    
    // 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();
   // cam.setFrustumPerspective(45f, (float)cam.getWidth()/cam.getHeight(), 0.001f, 1000f);
    // 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);
    sceneModel.setQueueBucket(RenderQueue.Bucket.Opaque);
    System.out.println(sceneModel.getLocalQueueBucket().toString());
    // 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, 5f, 1);
    player = new CharacterControl(capsuleShape, 0.05f);
    player.setJumpSpeed(20);
    player.setFallSpeed(30);
    player.setGravity(30);
    player.setPhysicsLocation(new Vector3f(0, 10, 0));
    
    //Load gun model
    weapon = (Node)assetManager.loadModel("Models/Weapons/M81/M81.j3o");
    weapon.setLocalScale(0.5f);
    weapon.setLocalTranslation(0, 4f, 0);
    Material weaponMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    //weaponMat.getAdditionalRenderState().setDepthTest(false);
    //weaponMat.getAdditionalRenderState().setDepthWrite(false);
    //weapon.setMaterial(weaponMat);
    // 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);
    rootNode.attachChild(weapon);
    
    weapon.setQueueBucket(RenderQueue.Bucket.Translucent);
    System.out.println(weapon.getQueueBucket().toString());
    
    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 isPressed, float tpf) {
    if (binding.equals("Left")) {
      left = isPressed;
    } else if (binding.equals("Right")) {
      right= isPressed;
    } else if (binding.equals("Up")) {
      up = isPressed;
    } else if (binding.equals("Down")) {
      down = isPressed;
    } else if (binding.equals("Jump")) {
      if (isPressed) { 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) {
        camDir.set(cam.getDirection()).multLocal(0.6f);
        camLeft.set(cam.getLeft()).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());
        }
        walkDirection.setY(0.0f);
        player.setWalkDirection(walkDirection);
        cam.setLocation(player.getPhysicsLocation());
        
        //gun alignment
        Vector3f vectorDifference = new Vector3f(cam.getLocation().subtract(weapon.getWorldTranslation()));
        weapon.setLocalTranslation(vectorDifference.addLocal(weapon.getLocalTranslation()));
        
        Quaternion worldDiff = new Quaternion(cam.getRotation().mult(weapon.getWorldRotation().inverse()));
        weapon.setLocalRotation(worldDiff.multLocal(weapon.getLocalRotation()));
        //Move gun to the bottom right of the screen
        weapon.move(cam.getDirection().mult(3));
        weapon.move(cam.getUp().mult(-0.8f));
        weapon.move(cam.getLeft().mult(-1f));
        weapon.rotate(0.0f,0,0);
    }
}

The translucent queue requires a translucent post processor I guess.

Hmm, I added a translucent post processor filter and I’m still getting the same problem, the weapon through wall problem, I added the following code in the simpleInitApp()

fpp = new FilterPostProcessor(assetManager);
        viewPort.addProcessor(fpp);
    
        tbf = new TranslucentBucketFilter();
        fpp.addFilter(tbf);
        tbf.setEnabled(true);

What could be possibly be going wrong here to cause the weapon to still go through walls and other objects?

Actually, I think that’s normal. Things in the translucent bucket should still interact with the scene. It’s filters order that is the reason for the translucent bucket. I mean, it is drawn after everything else but it will still use the z-buffer.

You could disable Ztesting for the weapon via the material.
-> Since its in translucent, it should be rendered last, and be above everything then.

@Empire Phoenix said: You could disable Ztesting for the weapon via the material. -> Since its in translucent, it should be rendered last, and be above everything then.

As I mentioned before, if you disable z-testing then the gun may occlude itself unless it is a very simple model.

Really, the viewport approach is not hard. Use the same camera in both viewports, use the same lighting in both viewports, manage the viewport… done.

Hm, true, using a secondary viewport is probably the way to go.

Ok so I guess viewport state it is, thanks for all your help guys, it works finally, still has a stutter when the player moves though. Here is kind of the rough source code as to the 2 viewport approach in case anyone out there is in the same pickle as me. Looking through forums for previous posts and seeing the similar “weapon through wall” problems with no solution source code or an explanation posted really made me frustrated. I hope this comes useful for anyone else in the future.

public class FPSAppState extends SimpleApplication implements ActionListener{

    /**
     * @param args the command line arguments
     */
    final int SHADOWMAP_SIZE = 1024;
    private DirectionalLightShadowRenderer dlsr;
    
    private BulletAppState bulletAppstate;
    private RigidBodyControl landscape;
    private Node player;
    private BetterCharacterControl playerControl;
    
    private Vector3f walkDirection = new Vector3f(0,0,0);
    private boolean left,right,front,back;
    private boolean zoomEnabled = true;
    
    private Spatial weapon;
    private Node modelNode;
    
    public static void main(String[] args) {
       FPSAppState fas = new FPSAppState();
       fas.start();
    }
    public void simpleInitApp(){
        bulletAppstate = new BulletAppState();
        stateManager.attach(bulletAppstate);
        viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));
        rootNode.setShadowMode(RenderQueue.ShadowMode.Off);
        this.initTerrain();
        this.setUpLight();
        this.initKeys();
        this.initPlayer();
        this.initWeapon();
    }
    
    public void initTerrain(){
        this.assetManager.registerLocator("town.zip", ZipLocator.class);
        Spatial map = this.assetManager.loadModel("main.scene");
        map.setLocalScale(2f);
        map.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        CollisionShape sceneShape =
            CollisionShapeFactory.createMeshShape((Node) map);
        this.landscape = new RigidBodyControl(sceneShape,0.0f);
        map.addControl(this.landscape);
        this.bulletAppstate.getPhysicsSpace().add(this.landscape);
        this.rootNode.attachChild(map);
    }
    
    
    
    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);
        
        this.dlsr = new DirectionalLightShadowRenderer(assetManager, SHADOWMAP_SIZE, 3);
        this.dlsr.setLight(dl);
        viewPort.addProcessor(dlsr);
    }
    
    private void initPlayer(){
        player = new Node("player");
        playerControl = new BetterCharacterControl(0.5f, 1.8f, 80f);
        playerControl.setApplyPhysicsLocal(true);
        playerControl.setSpatial(player);
        player.addControl(playerControl);
        bulletAppstate.getPhysicsSpace().add(playerControl);
        playerControl.warp(new Vector3f(58f, 6f, 0.0f));
        rootNode.attachChild(player);
        playerControl.setGravity(new Vector3f(0f,-5f,0f));
    }
    
    private void initWeapon(){
//        modelNode = new Node();
//        
//        weapon = this.assetManager.loadModel("Models/Weapons/M81/M81.j3o");
//        weapon.rotate(0f, 270*FastMath.DEG_TO_RAD, 0f);
//        weapon.setLocalTranslation(new Vector3f(0.6f,0.8f,-1.3f));
//        
//        modelNode.attachChild(weapon);
//        player.attachChild(modelNode);
        ViewPortState fps = new ViewPortState();
        stateManager.attach(fps);
        cam.setViewPort(0.0f, 1.0f, 0.0f, 1.0f);
    }
    
     public void initKeys() {
        this.inputManager.addMapping("Left", new KeyTrigger(keyInput.KEY_A));
        this.inputManager.addMapping("Right", new KeyTrigger(keyInput.KEY_D));
        this.inputManager.addMapping("Front", new KeyTrigger(keyInput.KEY_W));
        this.inputManager.addMapping("Back", new KeyTrigger(keyInput.KEY_S));
        
        this.inputManager.addListener(this, "Left");
        this.inputManager.addListener(this, "Right");
        this.inputManager.addListener(this, "Front");
        this.inputManager.addListener(this, "Back");
        
    }
    @Override
    public void simpleUpdate(float tpf){
        Vector3f camDir = cam.getDirection().mult(10f);
        Vector3f camLeft = cam.getLeft().mult(5f);
        
        walkDirection.set(0, 0, 0);
        if(left) {
            walkDirection.addLocal(camLeft);
        }
        if(right) {
            walkDirection.addLocal(camLeft.negate());
        }
        if(front) {
            walkDirection.addLocal(camDir);
        }
        if(back) {
            camDir = cam.getDirection().mult(2.5f);
            walkDirection.addLocal(camDir.negate());
        }
        walkDirection.setY(0.0f);
 
        playerControl.setWalkDirection(walkDirection);
 
        cam.setLocation(player.getLocalTranslation().add(0,1.8f,0));
        player.setLocalRotation(cam.getRotation());
    }
    
    public void onAction(String name, boolean isPressed, float tpf){
        if(name.equals("Left")) {
            left = isPressed;
        } else if(name.equals("Right")) {
            right = isPressed;
        } else if(name.equals("Front")) {
            front = isPressed;
        } else if(name.equals("Back")) {
            back = isPressed;
        } else if(name.equals("Jump")) {
            playerControl.jump();
        }
    }
}

and the viewport state

public class ViewPortState extends AbstractAppState {
    
    final int SHADOWMAP_SIZE = 1024;
    private Camera cam;
    private ViewPort view;
    private Node root;
    private AssetManager assetManager;
    
    private Node weapon;
    public ViewPortState(){
        root = new Node("root");
    }
    public Node getRoot(){
        return this.root;
    }
    
    public Camera getCamera(){
        return this.cam;
    }
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.assetManager = app.getAssetManager();
        
        root = new Node("ViewPort Root");
        //root.setCullHint(CullHint.Never);
        
        cam = app.getCamera();
        cam.setViewPort(0.0f, 1.0f, 0.0f, 1.0f);
        
        this.initWeapon(app);
        
        this.view = app.getRenderManager().createPostView("WeaponViewPort", cam);
        view.setEnabled(true);
        view.setClearFlags(false, true, false);
        view.attachScene(root);
        
        //Light
        this.setUpLight();
 
        root.updateLogicalState(1);
        root.updateGeometricState();
    }
    public void setUpLight() {
        // We add light so we see the scene
        AmbientLight al = new AmbientLight();
        al.setColor(ColorRGBA.White.mult(1.3f));
        root.addLight(al);

        DirectionalLight dl = new DirectionalLight();
        dl.setColor(ColorRGBA.White);
        dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal());
        root.addLight(dl);
        
//        DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, this.SHADOWMAP_SIZE, 4);
//        dlsr.setLight(dl);
//        view.addProcessor(dlsr);
   
    }
    private void initWeapon(Application app){
        weapon = (Node)app.getAssetManager().loadModel("Models/Weapons/M81/M81.j3o");
        weapon.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        weapon.setLocalScale(0.75f);
        weapon.setLocalTranslation(4f, -5f, 0);
        weapon.rotate(0,FastMath.PI,0);
        
        //Material weaponMat = new Material(assetManager, "Common/MatDefs/Misc/ColoredTextured.j3md");
        //weapon.setMaterial(weaponMat);
        this.root.attachChild(weapon);
    }
    
    @Override
    public void update(float tpf) {
        root.updateLogicalState(tpf);
        //gun alignment
        Vector3f vectorDifference = new Vector3f(cam.getLocation().subtract(weapon.getWorldTranslation()));
        weapon.setLocalTranslation(vectorDifference.addLocal(weapon.getLocalTranslation()));
        
        Quaternion worldDiff = new Quaternion(cam.getRotation().mult(weapon.getWorldRotation().inverse()));
        weapon.setLocalRotation(worldDiff.multLocal(weapon.getLocalRotation()));
        //Move gun to the bottom right of the screen
        weapon.move(cam.getDirection().mult(3));
        weapon.move(cam.getUp().mult(-0.8f));
        weapon.move(cam.getLeft().mult(-1f));
        weapon.rotate(0.0f,0,0);
    }
    
    @Override
    public void render(RenderManager rm){
        root.updateGeometricState();
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        //TODO: clean up what you initialized in the initialize method,
        //e.g. remove all spatials from rootNode
        //this is called on the OpenGL thread after the AppState has been detached
    }
    
}

Once again thank you all for all your help.

1 Like

can u tell me why this gun is moving with a lag?

Sorry about this but i followed all this topic, i have loaded a city.zip model with Ziplocator but when i run the game it throws me the error “Uncaught exception thrown in Thread [jme3 Main, 5, main]
AssetNotFoundException: main.scene” so i guess my city.zip inside only has some images and The City.obj and The City.mtl Is this the problem? Thanks

Please don’t resurrect an old topic with an unrelated question. Also, try doing the tutorials.