Ho hum... Collision fun!

Hey again guys… This will probably be the last question of the week !



I'm trying to implement collision detection for my little tank game, and it's sort of working, but not actually stopping my tank going outside the walls ! Despite setting the origLocation and origRotation, it marches on (sometimes a slight blip beforehand). Here's my Controller.update(…) method:



    public void update(float time) {
        Vector3f origLocation = node.getLocalTranslation();
        Quaternion origRotation = node.getLocalRotation();

        hAngle += TURN_SPEED * time * (value(LEFT) - value(RIGHT));
        node.getLocalRotation().fromAngles(vAngle, hAngle, 0f);

        Vector3f newLocation = node.getLocalTranslation();

        // I don't get this next line. What is getRotationColumn? value must be
        // between 0 and 2. I think 0=x, 1=y and 2=z... But I is baffled!
        Vector3f direction = new Vector3f(node.getLocalRotation().getRotationColumn(2));

        float speed = time * FORWARD_SPEED * (value(DOWN) - value(UP));
        speed *= 1 + (2 * value(FAST)); // Double the speed if the fast button is being pressed.

        newLocation.addLocal(direction.mult(speed));
        node.setLocalTranslation(newLocation);

        if (node.hasCollision(arena, true)) {
            logger.info("COLLISION!!");
            node.setLocalTranslation(origLocation);
            node.setLocalRotation(origRotation);
        }
    }


Hope that's right, because I had to type some if it in (don't ask!).

So do you see that I've created my original location and rotation at the top. If I'm colliding with the walls, it does spew out lots of "COLLISION!" log messages, but still lets me carry on moving around.

Any help greatly appreciated. Happy to send more code if you like ;)

Richard

Hehe,…actually I can't count how often I struggled at this problem :smiley:


Vector3f origLocation = node.getLocalTranslation();


...

Vector3f newLocation = node.getLocalTranslation();



Both are getting the same object. So if you change the loaction after you got the orig-location you change the orig-location as well and so origLocation is actually already newLocation when you set the "old" value. (lol, hope you could follow.. :D )

This might help you:

Vector3f origLocation = node.getLocalTranslation().clone();



ps: for rotation same....

I came across the same problem last night; but not in the context of collisions … took me a couple of hours to work out what on Earth was going wrong … shakes fist.

You should also take care in making sure all the bounding information is updated before checking for collisions. If you just move the tank, then check for collisions, then it's going to be using the old bounding info before you did your transformations. So your tank may already be inside a wall, but you wont register a collision yet. (actually I know someone who ran into that problem, with a tank demo in the last two weeks ironically). So either ensure that the world you're testing against isn't moving, and that you update your tank's world bound before checking the collision. Or pull out the collision detection after the main update cycle (or perhaps before, depends on how you set it up).



You may not run into a problem at all, but it's something just to think about.



Also, the getRotationColumn(2) returns the heading (direction) that the node's oriented. The input doesn't really specify x,y,z but of the column of a rotation matrix (which is a vector inside the matrix - although we calculate those since this is a quaternion)).



Edit: Also, since you're using the one-parameter version of the method, it'll return a brand new vector, so you don't have to worry about cloning it (you're creating an extra object that isn't needed).

A big grin came accross my face as the realisation of my idiocy began to set in !



That makes perfect sense, and I am off to the corner wearing my dunces hat  :oops:



I'm at work now, but can't wait to give that a crack a bit later.


Also, the getRotationColumn(2) returns the heading (direction) that the node's oriented. The input doesn't really specify x,y,z but of the column of a rotation matrix (which is a vector inside the matrix - although we calculate those since this is a quaternion)).


Now I sit firmly in the corner, scared to come back in case I get a quaternion question  :?

Cheers
Richard
richard_hawkes said:

A big grin came accross my face as the realisation of my idiocy began to set in !

That makes perfect sense, and I am off to the corner wearing my dunces hat  :oops:

I'm at work now, but can't wait to give that a crack a bit later.

Also, the getRotationColumn(2) returns the heading (direction) that the node's oriented. The input doesn't really specify x,y,z but of the column of a rotation matrix (which is a vector inside the matrix - although we calculate those since this is a quaternion)).


Now I sit firmly in the corner, scared to come back in case I get a quaternion question  :?

Cheers
Richard


Not that scary (well it depends) :P

Quaternions and and matrices can define rotations, so you're also able to convert between the two. I honestly don't know the names of the columns you get back from the other parameters though (but Matrices usually have vectors that you can get from them such as Forward, Left, Up, etc).

In fact, after getting some experience with some other API's, some of jmonkey's quaternion/matrix methods do seem to be a bit confusing heh.

Hi,



Good news and bad news !



The good news is that it does now stop me at the wall. The bad news is that I can't move afterwards! Man, this is hurting my head !



I also tried the following:


      node.updateWorldBound();

      if (node.hasCollision(arena, true)) {
            logger.info("COLLISION!!");
            node.setLocalTranslation(origLocation);
            node.setLocalRotation(origRotation);
      }



Is that what you meant starnick? If it is, it ain't making any difference !

Perhaps it's not letting you move because even if you're trying to back up, you're still intersecting with the wall, e.g. the original position is inside the wall too…therefore it keeps setting you at the same position. Perhaps when you hit the wall, just nudge your guy back the opposite way?

try this:



...
node.setLocalRotation(origRotation);
node.updateWorldBound();



when resetting the location. Not so sure right now. What does debugging say? Have a look if the worldbound is updated.

Hi again,



Thanks for your continued effort with me on this. I'm at a sort of working stage, but it ain't great!



Here's what I'm doing:




        float speed = time * FORWARD_SPEED * (value(DOWN) - value(UP));
        speed *= 1 + (2 * value(FAST)); // Double the speed if the fast button is being pressed.
        newLocation.addLocal(direction.mult(speed));
        node.setLocalTranslation(newLocation);

        node.updateWorldBound();
        if (node.hasCollision(walls, false)) {
            origTranslation.subtractLocal(direction.mult( speed * 8 ));
            node.setLocalTranslation(origTranslation);
            node.setLocalRotation(origRotation);
            node.updateWorldBound();
        }



The origTranslation.subtractLocal(direction.mult( speed * 8 )); produces a shocking back-jump, but does work most of the time. I can force this rather weird speed-through the wall, ending up on the other side not able to get back. Removing that line takes me back to my "can't move" situation.... Arrrrgggghhhh !!

How hard can this be!?

Collision can be really annoying sometimes. I am making an fps demo for jme3 right now, following many papers, sites, essays, etc I still can't get it right. What you are doing barely scratches the surface…

Collisions arent easy if they shoud work right.



Saving a lastPosition doesnt work in most cases (many people tried this in this forum).

Normally you have to do much more calculation, like this to prevent your objekt going through walls at high speed:



Because of checking if the object has a collision at the end of moving the algorithmus never notifies that there was a wall between the start and the end. So instead of checking the end of movement, you'll have to split up the way the object moves in parts, which are as long as the object itself and check each part of the way for collisions. (Thats the brute force way).



Another "trick" is instead of moving the object, you' ll first do a ray cast in the direction and check if there are any obstacles in the way. Then you move the object only the length where the intersection of the ray with the obstacle was. Backdraw of this method is, that you'll have to cast many rays to check each border of your object.



There is a 3rd way to: You can "stretch"/project the bounding of object that it has the length of the way it should go and check this big BoundingBox for collisions.



These three points only solve the collision recognition; Things like Triangle accurate collision and sliding on walls is another topic.



Regards,

snare

Saving a lastPosition doesnt work in most cases (many people tried this in this forum)


That has messed with my head !

When (in the update method) can I not check a "before" and "after" state? If there were no collisions before I moved, why can't I move it back and assume a non-collision state??? Dude, if this is true, then I think I'm gonna' have to take up bowling !

Perhaps your approach works sometime, but it doubt it would be a good playing experience.



Even if momoko doesnt get it right i think what i described is only the the tip of the iceberg…

Now i am frustrated…



Regards





Ps: I already had the same issue:

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



If i member correctly i solved it this way:

I created 4 boundingboxes in each direction of the player (the player only could run in 4 directions) and if one bounding box had a collision, i knew that in that direction the player could go any more.

From above it looked like this:

  -

| o |

  -

For a simple tank game I don't think you need anything fancy. You can do it with 2D and grid based, find the exact point of time when the collision occurs and put the tank there.

I've realised there was a slight flaw in my code. I had forgotten to clone() the new location, so if it was colliding, the origLocation was exactly the same as the newLocation… This could drive a man to distraction !



Fixing that has made a big difference. It no longer sticks as much. Weirdly, I cannot reverse out of a collision.



I am coming to the conclusion that updateWorldBounds() is not reliable. Why? Well, I am now doing a check after the move. If I'm not colliding with the wall, store this away in a nonCollidingTranslation Vector3f. At the very very start of the update method, I check to see (before any keystroke checks) if I'm colliding already. If I am then set my location to the nonCollidingTranslation… No good… No difference in fact !



Thinking about all this, should I be using getWorldTranslation() instead? Hmmm… That's got me thinking !

Wouldn't say so. You should backup the the position you are changing.



Can you provide a full overview of your current collision-check-code? I thought we finished the clone-problem already…

I thought we finished the clone-problem already...


Yes, but what you have to bear in mind is that you're talking to an idiot ! :oops:

Sorry for the delay in responding. Business getting in the way of... erm... other business !

As requested then, here's the source code for the TankController in all its glory:


package com.jgcloud.sandbox.jme;

import com.jgcloud.sandbox.darkstar.DarkstarUpdater;
import com.jme.input.controls.GameControl;
import com.jme.input.controls.GameControlManager;
import com.jme.input.controls.binding.KeyboardBinding;
import com.jme.input.controls.binding.MouseButtonBinding;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.scene.Controller;
import com.jme.scene.Node;

import java.util.logging.Logger;
import static com.jgcloud.sandbox.jme.TankController.CubeAction.*;
import static com.jme.input.KeyInput.*;
import static com.jme.input.controls.binding.MouseButtonBinding.*;

/**
 * Basic Controller implementation for our tank.
 *
 * @author Richard Hawkes
 */
public class TankController extends Controller {
    Logger logger = Logger.getLogger(TankController.class.getName());

    enum CubeAction {FAST, LEFT, RIGHT, UP, DOWN, EXIT};

    private final static float TURN_SPEED = 2F;
    private final static float FORWARD_SPEED = 16F;


    private final Node node;
    private final Node walls;

    private Vector3f nonCollidingTranslation;
    private Quaternion nonCollidingRotation;

    private final GameControlManager manager;
    private float vAngle = 0F;
    private float hAngle = 0F;


    public TankController(Node node, Node walls) {
        this.node = node;
        this.walls = walls;

        this.manager = new GameControlManager();

        //create all actions
        for (CubeAction action : CubeAction.values()) {
            manager.addControl(action.name());
        }

        //bind keys
        bindKey(EXIT, KEY_X);
        bindKey(UP, KEY_UP);
        bindKey(DOWN, KEY_DOWN);
        bindKey(LEFT, KEY_LEFT);
        bindKey(RIGHT, KEY_RIGHT);
        bindKey(FAST, KEY_LSHIFT);

        //bind mouse buttons
        bindMouseButton(LEFT, LEFT_BUTTON);
        bindMouseButton(RIGHT, RIGHT_BUTTON);
    }


    private void bindKey(CubeAction action, int... keys) {
        final GameControl control = manager.getControl(action.name());
        for (int key : keys) {
          control.addBinding(new KeyboardBinding(key));
        }
    }


    private void bindMouseButton(CubeAction action, int mouseButton) {
        final GameControl control = manager.getControl(action.name());
        control.addBinding(new MouseButtonBinding(mouseButton));
    }


    private float value(CubeAction action) {
        return manager.getControl(action.name()).getValue();
    }


    @Override
    public void update(float time) {
        Vector3f origTranslation = node.getLocalTranslation();
        Quaternion origRotation = node.getLocalRotation();

        if (node.hasCollision(walls, true)) {
            logger.info("Oh dear... We're already colliding. What chance do we stand!?");
            node.setLocalTranslation(nonCollidingTranslation);
            node.setLocalRotation(nonCollidingRotation);
            node.updateWorldBound();
        }

        if (value(EXIT) > 0) {
            DarkstarUpdater.getInstance().serverLogout();
            System.exit(0); //OK, this is just a demo...
        }

        hAngle += TURN_SPEED * time * (value(LEFT) - value(RIGHT));
        node.getLocalRotation().fromAngles(vAngle, hAngle, 0f);

        Vector3f newLocation = node.getLocalTranslation().clone();

        // I don't get this next line. What is getRotationColumn? value must be
        // between 0 and 2. I think 0=x, 1=y and 2=z... But I is baffled!
        Vector3f direction = new Vector3f(node.getLocalRotation().getRotationColumn(2));
       
        float speed = time * FORWARD_SPEED * (value(DOWN) - value(UP));
        speed *= 1 + (2 * value(FAST)); // Double the speed if the fast button is being pressed.
        newLocation.addLocal(direction.mult(speed));

        node.setLocalTranslation(newLocation);

        node.updateWorldBound();

        if (node.hasCollision(walls, true)) {
            node.setLocalTranslation(origTranslation);
            node.setLocalRotation(origRotation);
            node.updateWorldBound();
        }

        if (!node.hasCollision(walls, true)) {
            logger.info("Location is not colliding. Storing it away.");
            nonCollidingTranslation = node.getLocalTranslation().clone();
            nonCollidingRotation = node.getLocalRotation().clone();
        }
    }
}



You'll see that it has a number of "updateWorldBound()" going on... Doesn't seem to make a difference !

Cheers
Richard

Ok, in order to test it better I reproduced the problem. The sollution is that you have to call updateWorldVectors() before calling updateWorldBound() after each change of translation/rotation as the bounding-center is set by the worldtranslation which is not automatically updated. (Actually I thought SimpleGame would do that in the main-loop but it seems not to be true).



EDIT: Both steps are done by: 

player.updateGeometricState(0,true);





Here my test so you can see how I did it (you can steer the box wiht "uhjk"-keys):



import java.util.Date;
import java.util.Random;

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.bounding.BoundingSphere;
import com.jme.bounding.OrientedBoundingBox;
import com.jme.input.InputHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.input.NodeHandler;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.shape.Box;
import com.jme.scene.state.MaterialState;

public class CollisionTest extends SimpleGame {

   private Box wall;
   private Box player;
   private InputHandler nodeHandler;
   private KeyBindingManager keybm = KeyBindingManager.getKeyBindingManager();
   
   @Override
   protected void simpleInitGame() {
      
      wall = new Box("b",new Vector3f(-10,0,0),new Vector3f(10,1,10));
      MaterialState ms = display.getRenderer().createMaterialState();
      ms.setAmbient(ColorRGBA.orange);
      wall.setRenderState(ms);
      wall.setModelBound(new OrientedBoundingBox());
      wall.updateModelBound();
      wall.updateRenderState();
      rootNode.attachChild(wall);
      
      player = new Box("player", Vector3f.ZERO,Vector3f.UNIT_XYZ);
      player.setModelBound(new BoundingSphere());
      player.updateModelBound();
      player.getLocalTranslation().addLocal(0,3,0);
      rootNode.attachChild(player);
   
      
      keybm.set("left", KeyInput.KEY_H);
      keybm.set("right", KeyInput.KEY_K);
      keybm.set("up", KeyInput.KEY_U);
      keybm.set("down", KeyInput.KEY_J);
   }
   
   
   

   @Override
   protected void simpleUpdate() {
      super.simpleUpdate();
      
      Vector3f saveLocation = player.getLocalTranslation().clone();
      
      if (keybm.isValidCommand("left",true))
      {
         player.getLocalTranslation().addLocal(new Vector3f(-tpf,0,0));
         player.updateGeometricState(0,true);
      }
      if (keybm.isValidCommand("right",true))
      {
         player.getLocalTranslation().addLocal(new Vector3f(tpf,0,0));
         player.updateGeometricState(0,true);
      }
      if (keybm.isValidCommand("up",true))
      {
         player.getLocalTranslation().addLocal(new Vector3f(0,tpf,0));
         player.updateGeometricState(0,true);
      }
      if (keybm.isValidCommand("down",true))
      {
         player.getLocalTranslation().addLocal(new Vector3f(0,-tpf,0));
         player.updateGeometricState(0,true);
      }
      

      if (wall.hasCollision(player, true))
      {
         player.getLocalTranslation().set(saveLocation);
         player.updateGeometricState(0,true);
      }
   }


   @Override
   protected void simpleRender() {
      super.simpleRender();
      
   }




   public static void main(String args[])
   {
      new CollisionTest().start();
   }
   
}



Hope that helps.

And did it finally work? Some feedback is always nice  :x, especially for people that find this thread later on, if the result finally helped, or not.