PhysicsCharacterNode Questions

Hello!



I had originally posted something similar to this over in this thread:



http://www.jmonkeyengine.com/forum/index.php?topic=14664.0



However, I realized that the place I was posting was probably irrelevant and possibly a thread hijack. So I went and researched my issues a bit more and now I have some solid questions about the PhysicsCharacterNode.


  1. How exactly does moveDirection work? In the code I've posted below, I noticed that if I cap the fps using setVSync that the movement amount increases. But I thought I had normalized the velocity by the time per frame properly already… So I think I'm using moveDirection improperly. I can't tell though since I don't know how to get into the jBullet jar to look at the actual call! XD


  2. The jitter I described in my prior post is way more noticable at lower framerates (makes sense). The one big thing that jumps out at me is how there's a very violent jitter after a jump is issued - just as the node begins to fall after the apex of the jump. Will this jitter and the overall jitter be addressed later on down the road? Or is it something that the game dev needs to find a way to handle?


  3. I had described a method of avoiding 'seeing' the jitter by having the player's model simply get location updates from the physicsnode. This method seems less and less ideal the more I tinker with it. I'm guessing this is not the intended method and that the PhysicsCharacterNode should have all the things associated with the player as a child of it?


  4. Oh almost forgot. With the box updating it's location based on the local position of the node, why is it the box is always playing catchup? Is the box's updated location happening before the physics node is updated?



    Whew long winded post XD



    Thank you for reading! Framerate VSync'd code below!



    EDIT: Added 4.



import com.jme3.app.SimpleBulletApplication;
import com.jme3.asset.plugins.ZipLocator;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.nodes.PhysicsCharacterNode;
import com.jme3.bullet.nodes.PhysicsNode;
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.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
 
/**
 * Example 9 - How to make walls and floors solid.
 * This version uses Physics and a custom Action Listener.
 * @author normen, with edits by Zathras
 */
public class HelloCollisionMod
  extends SimpleBulletApplication
  implements ActionListener {
 
  private Spatial gameScene;
  private LifeformMod player;
  private Geometry playerModel;
  private Vector3f walkDirection = new Vector3f();
  private boolean left = false, right = false, up = false, down = false;
  
  private static AppSettings settings;
 
  public static void main(String[] args) {
    HelloCollisionMod app = new HelloCollisionMod();
    settings = new AppSettings(true);
   settings.setVSync(true);
   settings.setTitle("Hello Collision Modified Version");
   app.setSettings(settings);
    app.start();
  }
 
  public void simpleInitApp() {
    renderer.setBackgroundColor(ColorRGBA.Cyan);
 
    flyCam.setEnabled(false);
    setupKeys();
 
    // We add a light so we see the scene
    DirectionalLight dl = new DirectionalLight();
    dl.setColor(ColorRGBA.White.clone().multLocal(2));
    dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalize());
    rootNode.addLight(dl);
 
    // We load the scene from the zip file and adjust its size.
    assetManager.registerLocator("town.zip", ZipLocator.class.getName());
    gameScene = assetManager.loadModel("main.scene");
    gameScene.setLocalScale(2f);
 
    // We set up collision detection for the scene by creating a
    // compound collision shape and a physics node.
    CompoundCollisionShape sceneShape = CollisionShapeFactory.createMeshCompoundShape((Node) gameScene);
    PhysicsNode levelNode = new PhysicsNode(gameScene, sceneShape, 0);
 
    Box box = new Box(Vector3f.ZERO, 1, .5f, 1);
    playerModel = new Geometry("PlayerBox", box);
    Material playerMat = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
   playerMat.setColor("m_Color", ColorRGBA.Magenta);
   playerModel.setMaterial(playerMat);
   
   /** COMMENT THIS OUT TO SEE JITTER **/
   rootNode.attachChild(playerModel);
   /** COMMENT OUT ABOVE TO SEE JITTER **/
    
    // We set up collision detection for the player by creating
    // a capsule collision shape and a physics character node.
    // The physics character node offers extra settings for
    // size, stepheight, jumping, falling, and gravity.
    // We also put the player in its starting position.
    player = new LifeformMod(new Vector3f(box.xExtent, box.yExtent, box.zExtent),
          0.5f, "PlayerCharacter", playerModel, rootNode);
    player.setMovementProperties(100, 150, 100, 2.2f, 60, 40);
    Material wireMat = new Material(assetManager, "Common/MatDefs/Misc/WireColor.j3md");
   wireMat.setColor("m_Color", ColorRGBA.Red);
   player.attachDebugShape(wireMat);
    player.setJumpSpeed(20);
    player.setFallSpeed(30);
    player.setGravity(30);
    player.setMaxSlope(FastMath.PI/4);
    player.setLocalTranslation(new Vector3f(0, 10, 0));
    player.updateGeometricState();
 
    // We attach the scene and the player to the rootNode and the physics space,
    // to make them appear in the game world.
    rootNode.attachChild(levelNode);
    rootNode.attachChild(player);
    rootNode.updateGeometricState();
    getPhysicsSpace().add(levelNode);
    getPhysicsSpace().add(player);
  }
 
  /** We over-write some navigational key mappings here, so we can
   * add physics-controlled walking and jumping: */
  private void setupKeys() {
    inputManager.addMapping("Lefts",  new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Ups",    new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Downs",  new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Jumps",  new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(this, "Lefts");
    inputManager.addListener(this, "Rights");
    inputManager.addListener(this, "Ups");
    inputManager.addListener(this, "Downs");
    inputManager.addListener(this, "Jumps");
  }
 
  /** 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("Lefts")) {
      if (value) { left = true; }  else { left = false; }
    } else if (binding.equals("Rights")) {
      if (value) { right = true; } else { right = false; }
    } else if (binding.equals("Ups")) {
      if (value) { up = true; } else { up = false; }
    } else if (binding.equals("Downs")) {
      if (value) { down = true; }  else { down = false; }
    } else if (binding.equals("Jumps")) {
      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) {
    walkDirection.set(0, 0, 0);
    if(left)  {player.turn(tpf, 1);}
    if(right) {player.turn(tpf, -1);}
    if(up)    {player.forward(tpf);}
    if(down)  {player.backpedal(tpf);}
     player.update(tpf);
     System.out.println("LOC: " + player.getLocalTranslation());
     cam.setLocation(player.getLocalTranslation().addLocal(5, 5, 5));
     cam.lookAt(player.getWorldTranslation(), Vector3f.UNIT_Y);
  }
  
  public class LifeformMod extends PhysicsCharacterNode {

      /** Geometry **/
      private Geometry model;

      /** Movement Physics **/
      private float weight;
      private float velocity;
      private float acceleration;
      private float braking;
      private float turnRate;

      private float maxSpeed;
      private float minSpeed;
      private boolean isRunning = false;

      public LifeformMod(Vector3f collisionExtents, float step,
            String nodeName, Geometry model, Node floorRootNode) {
//         super(new BoxCollisionShape(collisionExtents), step);
//         super(new CapsuleCollisionShape(collisionExtents.x, .1f, 1), step);
//         super(new CylinderCollisionShape(collisionExtents, 1), step);
         super(new SphereCollisionShape(collisionExtents.x), step);
         setModel(model);
         this.setGravity(30);
         this.setFallSpeed(30);
         this.setJumpSpeed(20);
      }

      public void update(float tpf) {
         drift(tpf);
         Vector3f nextMove = new Vector3f();
         nextMove.set(model.getLocalRotation().getRotationColumn(2, null).mult(velocity * tpf));
         setWalkDirection(nextMove);
//         System.out.println("Next Move : " + nextMove);
         
         /** COMMENT THIS OUT IF YOU WANT TO SEE JITTER **/
         model.setLocalTranslation(this.getLocalTranslation());
         /** COMMENT ABOVE OUT IF YOU WANT TO SEE JITTER **/
         
         System.out.println("Velocity  : " + velocity);
//         System.out.println("Pos       : " + this.getLocalTranslation());
         System.out.println("


");
      }

      public void resetPlayer() {
         this.setLocalTranslation(Vector3f.ZERO);
      }

      public void setModel(Geometry model) {
         if(this.model != null) {
            this.detachChild(this.model);
         }
         this.model = model;
         /** UNCOMMENT THIS IF YOU WANT TO SEE JITTER **/
//         this.attachChild(this.model);
         /** UNCOMMENT ABOVE IF YOU WANT TO SEE JITTER **/
      }

      public Geometry getModel() {
         return model;
      }

      /**
       * Quick method to set all of the movement attributes of this character.
       * @param w Weight
       * @param v Velocity
       * @param a Acceleration Rate
       * @param b Braking Rate
       * @param tr Turning Rate
       * @param maxS Maximum Speed (Moving forwards)
       * @param minS Minimum Speed (Moving backwards)
       */
      public void setMovementProperties(float w, float a, float b, float tr, float maxS, float minS) {
         this.weight = w;
         this.acceleration = a;
         this.braking = b;
         this.turnRate = tr;
         this.maxSpeed = maxS;
         this.minSpeed = minS;
      }

      public void forward(float tpf) {
         if(onGround()) {
            velocity += tpf * acceleration;
            if(isRunning && velocity > maxSpeed * 2) {
               velocity = maxSpeed * 2;
            }
            else if(!isRunning && velocity > maxSpeed) {
               velocity = maxSpeed;
            }
         }
      }

      public void backpedal(float tpf) {
         if(onGround()) {
            velocity -= tpf * braking;
            if(velocity < -minSpeed) {
               velocity = -minSpeed;
            }
         }
      }

      public void turn(float tpf, int direction) {
         model.rotate(0, direction * turnRate * tpf, 0);
      }

      public void jump(float tpf) {
         jump();
      }

      private void drift(float tpf) {
         float driftWeight = weight;
         if(!onGround()) {
            driftWeight = weight * .05f;
         }
         else {
            driftWeight = weight;
         }
         if(velocity < -FastMath.FLT_EPSILON) {
            velocity += (driftWeight/5) * tpf;
            if(velocity > 0) {
               velocity = 0;
            }
         }
         else if(velocity > FastMath.FLT_EPSILON) {
            velocity -= (driftWeight/5) * tpf;
            if(velocity < 0) {
               velocity = 0;
            }
         }
      }
   }
}

The walkDirection vector of the PhysicsCharacterNode is framerate independent, you only need to set it once and the character will move correctly at all framerates.

The jitter can be reduced by lowering the stepSize of the character node. Note that character nodes are not "real" physics but a simple version.

Yes, the child of the character node should be modified, the character node itself is moved by the physics.

Cheers,

Normen

Ah excellent. So the graphical representation should be separate from the physics/collision representation then? And I should just continue updating the model's position based on the behind the scenes physics without actually attaching it to the PhysicsCharacterNode.



Thanks Normen! Gotta go wrap my head around this and hopefully fix my code!  XD

No, you can attach your model to the PhysicsCharacterNode so it move around with it, but if you want it to rotate the model in the walkDirection for example you should rotate the model and not the PhysicsCharacterNode.

Yup yup. I'd seen all the posts about not rotating PhysicsCharacterNodes. I guess as long as I'm not using a BoxCollisionShape it shouldn't matter since that's the only one with hard edges.



Also, I was using the CapsuleCollisionShape but I was just playing around with the SphereCollisionShape. It's A LOT smoother than the Capsule. For some reason the Capsule shape is more a cylinder than a capsule though since the top and bottom are flat!  :stuck_out_tongue:



I may just keep the model non-attached to the node. I was having a lot of trouble figuring out the distance between the model and the floor so that I could do effects below the character (Using Rays). For some reason the model ends up reporting distance values that are above the floor while jumping and distance values under the floor while falling because the model is lagging behind the PhysicsCharacterNode. What I was talking about before in (4) it seems like the physics model will always be 'ahead' of the model if the model's position is just being updated with setlocaltranslation() rather than being attached so I guess I shouldn't be surprised that it's still ahead when moving on the Y Axis.



But I don't know, still experimenting a bit. Slow day.  :lol:



Thanks!


tehflah said:

For some reason the Capsule shape is more a cylinder than a capsule though since the top and bottom are flat!  :P

No, thats only the debug view thats displaying a cylinder instead of a capsule because theres no capsule shape in jme3 yet :P

Oh haha. I guess that would do it!  :lol:



One thing I forgot to mention was that I have realized that if I call updateGeometricState() on the PhysicsCharacterNode before I start doing things like the ray determined distance or camera look-at that it's suddenly accurate and not lagging behind.



But I was under the impression that calling updateGeometricState() was bad unless you were adding things to the physics space or updating the entire scene in simpleUpdate?