Moving a "tank"

I'm just trying to learn this physics stuff for a game my group is making for our college course (I'm also just learning jME).  In our game the player controls a tank.  I originally started the code with a simple box whose location was updated every update cycle based on the input keys.



Then I realized to get collision detection I'd have to move the tank using forces.  I converted the tank (Box) to use a DynamicPhysicsNode and started applying a force in a constant direction whenever a key is pressed.  I also set masses, materials, and gravity, but friction seemed to make the box stick, and once it got going it became really unstable and would flip through the air (the box isn't that big).



Finally, I saw in a tutorial about the setSurfaceMotion() method of the material and that seems to work better.  So, is the only way to make a tank to create tread nodes, attach them to a body node and set their surface motion to either positive, negative, or opposite each other?  Is their an easier way to get the same basic effect?



Thanks, Dan

I think this is the easiest way, yes. There are more sophisticated ways, of course…

I've spent way to much time trying to get this working.  I know you've put a lot of time into jME Physics but the documentation is frustratingly lacking.



I'm still trying to get a box moving and rotating.  I want the camera to follow behind the box so I've made a camera that calculates and moves to a position "behind" the box every update cycle.  What surface motion vector do I need to use in order to get the box to move "forward"?  If I use a constant direction vector, the actual direction of movement seems unpredictable when the box is rotated (see below).  If I use sin/cos (using node.getLocalRotation().toAngles( null )[1] as the angle)  to assign the magnitude to the x and z components then it always moves in a constant direction no matter how its rotated.



I applied torque for rotation, but that only seems to effect the box when its moving, sometimes not even then.  I need to set a high value (15) or I never see any rotation, but then when the box does rotate it just goes haywire.

More tutorials would be great, of course. Unfortunately few people take the time to make one after they accomplished a task. Still, modeling physical behavior is hard, no documentation or framework will change that. You always need to figure out the way for your project.



Regarding surface motion: I tested modeling a simple "tank" based on Lesson 8 and figured that there was a bug in applying the motion (fixed in CVS).



The following code kind of works (rotate with [insert] and [pg up]). But the friction somewhat kills the desired effect at most angles. This could be fixed by allowing to specify friction in movement direction vs. friction orthogonal to the movement direction… but that's not supported by jME Physics, yet… the "tank" keeps complicated… what about using wheels? :roll:


/*
 * Copyright (c) 2005-2007 jME Physics 2
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 *  * Neither the name of 'jME Physics 2' nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jmetest.physicstut;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme.input.InputHandler;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.input.util.SyntheticButton;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.shape.Box;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.MaterialState;
import com.jmex.physics.DynamicPhysicsNode;
import com.jmex.physics.PhysicsSpace;
import com.jmex.physics.PhysicsUpdateCallback;
import com.jmex.physics.StaticPhysicsNode;
import com.jmex.physics.PhysicsCollisionGeometry;
import com.jmex.physics.contact.ContactInfo;
import com.jmex.physics.material.Material;
import com.jmex.physics.util.SimplePhysicsGame;
import com.jmex.physics.util.PhysicsPicker;

/**
 * Based on Lesson8 this shows a simple "tank" moving and rotating with surface motion on it's "chains".
 * @author Irrisor
 */
public class Lesson8 extends SimplePhysicsGame {

    protected void simpleInitGame() {
        // no magic here - just create a floor in that method
        createFloor();

        // second we create a box - as we create multiple boxes this time the code moved into a separate method
        player = createPlayer();
        player.setName( "player" );
        color( player, new ColorRGBA( 0, 0, 1, 1 ) );
        // the first box gets in the center above the floor
        player.getLocalTranslation().set( 8, 1, 0 );
        // move the center of mass down to let the box land on its 'feet'
        player.computeMass();
        player.setCenterOfMass( new Vector3f( 0, -0.5f, 0 ) );
        // this box keeps the default material

        // to move the player around we create a special material for it
        // and apply surface motion on it
        playerLeftPart.setMaterial( new Material( "player material left" ) );
        playerRightPart.setMaterial( new Material( "player material right" ) );
        // the actual motion is applied in the MoveAction (see below)

        // we map the MoveAction to the keys DELETE and PAGE DOWN for forward and backward
        input.addAction( new MoveAction( new Vector3f( -2, 0, 0 ), playerLeftPart ),
                InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_DELETE, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( -2, 0, 0 ), playerRightPart ),
                InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_DELETE, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( 2, 0, 0 ), playerLeftPart ),
                InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_PGDN, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( 2, 0, 0 ), playerRightPart ),
                InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_PGDN, InputHandler.AXIS_NONE, false );

        // now the player should be able to jump
        // we do this by applying a single force vector when the HOME key is pressed
        // this should happen only while the player is touching the floor (we determine that below)
        input.addAction( new InputAction() {
            public void performAction( InputActionEvent evt ) {
                if ( playerOnFloor && evt.getTriggerPressed() ) {
                    player.addForce( new Vector3f( 0, 500, 0 ) );
                }
            }
        }, InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_HOME, InputHandler.AXIS_NONE, false );

        // PGUP and INSERT should turn the node
        input.addAction( new MoveAction( new Vector3f( -2, 0, 0 ), playerLeftPart ), InputHandler.DEVICE_KEYBOARD,
                KeyInput.KEY_PGUP, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( 2, 0, 0 ), playerRightPart ), InputHandler.DEVICE_KEYBOARD,
                KeyInput.KEY_PGUP, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( 2, 0, 0 ), playerLeftPart ), InputHandler.DEVICE_KEYBOARD,
                KeyInput.KEY_INSERT, InputHandler.AXIS_NONE, false );
        input.addAction( new MoveAction( new Vector3f( -2, 0, 0 ), playerRightPart ), InputHandler.DEVICE_KEYBOARD,
                KeyInput.KEY_INSERT, InputHandler.AXIS_NONE, false );
        // Note that applying tourque needs to be done singular (like above: allowRepeats == false)
        // or it must be done _per step_, which would mean we need to move the action into an InputHandler which
        // is updated each physics step not each frame


        // ok finally detect when the player is touching the ground:
        // a simple way to do this is making a boolean variable (playerOnFloor) which is
        // set to true on collision with the floor and to false before each physics computation

        // collision events analoguous to Lesson6
        SyntheticButton playerCollisionEventHandler = player.getCollisionEventHandler();
        input.addAction( new InputAction() {
            public void performAction( InputActionEvent evt ) {
                ContactInfo contactInfo = (ContactInfo) evt.getTriggerData();
                if ( contactInfo.getNode1() == floor || contactInfo.getNode2() == floor ) {
                    playerOnFloor = true;
                }
            }
        }, playerCollisionEventHandler, false ); //TODO: this should be true when physics supports release events

        // and a very simple callback to set the variable to false before each step
        getPhysicsSpace().addToUpdateCallbacks( new PhysicsUpdateCallback() {
            public void beforeStep( PhysicsSpace space, float time ) {
                playerOnFloor = false;
            }

            public void afterStep( PhysicsSpace space, float time ) {

            }
        } );

        // finally print a key-binding message
        Text infoText = Text.createDefaultTextLabel( "key info", "[del] and [page down] to move, [home] to jump" );
        infoText.getLocalTranslation().set( 0, 20, 0 );
        fpsNode.attachChild( infoText );

        new PhysicsPicker(input, rootNode, getPhysicsSpace(), true ).setPickModeVisual( false );
        MouseInput.get().setCursorVisible( true );
    }

    private void createFloor() {
        // first we will create the floor like in Lesson3, but put into into a field
        floor = getPhysicsSpace().createStaticNode();
        rootNode.attachChild( floor );
        final Box visualFloorBox = new Box( "floor", new Vector3f(), 5, 0.25f, 5 );
        floor.attachChild( visualFloorBox );
        // and not that steep
        visualFloorBox.getLocalRotation().fromAngleNormalAxis( 0.1f, new Vector3f( 0, 0, -1 ) );
        final Box visualFloorBox2 = new Box( "floor", new Vector3f(), 5, 0.25f, 5 );
        floor.attachChild( visualFloorBox2 );
        visualFloorBox2.getLocalTranslation().set( 9.7f, -0.5f, 0 );
        // and another one a bit on the left
        final Box visualFloorBox3 = new Box( "floor", new Vector3f(), 5, 0.25f, 5 );
        floor.attachChild( visualFloorBox3 );
        visualFloorBox3.getLocalTranslation().set( -11, 0, 0 );
        floor.generatePhysicsGeometry();
    }

    /**
     * Action called on key input for applying movement of the player.
     */
    private class MoveAction extends InputAction {
        /**
         * store direction this action instance will move.
         */
        private Vector3f direction;
        private PhysicsCollisionGeometry geom;

        /**
         * @param direction direction this action instance will move
         * @param geometry where to apply surface motion
         */
        public MoveAction( Vector3f direction, PhysicsCollisionGeometry geometry ) {
            this.direction = direction;
            geom = geometry;
        }

        public void performAction( InputActionEvent evt ) {
            if ( evt.getTriggerPressed() ) {
                // key goes down - apply motion
                geom.getMaterial().setSurfaceMotion( direction );
            } else {
                // key goes up - stand still
                geom.getMaterial().setSurfaceMotion( ZERO );
                // note: for a game we usually won't want zero motion on key release but be able to combine
                //       keys in some way
            }
        }
    }

    /**
     * helper vector for zeroing motion.
     */
    private static final Vector3f ZERO = new Vector3f( 0, 0, 0 );

    /**
     * floor node - is a field to easily access it in the action and callback.
     */
    private StaticPhysicsNode floor;
    /**
     * player node - also a field to easily access it in the actions.
     */
    private DynamicPhysicsNode player;
    private PhysicsCollisionGeometry playerLeftPart;
    private PhysicsCollisionGeometry playerRightPart;

    /**
     * variable for detecting if the player is touching the floor.
     */
    private boolean playerOnFloor = false;

    /**
     * Little helper method to color a spatial.
     *
     * @param spatial the spatial to be colored
     * @param color   desired color
     */
    private void color( Spatial spatial, ColorRGBA color ) {
        final MaterialState materialState = display.getRenderer().createMaterialState();
        materialState.setDiffuse( color );
        if ( color.a < 1 ) {
            final AlphaState alphaState = display.getRenderer().createAlphaState();
            alphaState.setEnabled( true );
            alphaState.setBlendEnabled( true );
            alphaState.setSrcFunction( AlphaState.SB_SRC_ALPHA );
            alphaState.setDstFunction( AlphaState.DB_ONE_MINUS_SRC_ALPHA );
            spatial.setRenderState( alphaState );
            spatial.setRenderQueueMode( Renderer.QUEUE_TRANSPARENT );
        }
        spatial.setRenderState( materialState );
    }

    /**
     * Create a box like known from Lesson2.
     *
     * @return a physics node containing a box
     */
    private DynamicPhysicsNode createPlayer() {
        DynamicPhysicsNode dynamicNode = getPhysicsSpace().createDynamicNode();
        rootNode.attachChild( dynamicNode );
        dynamicNode.attachChild( new Box( "player", new Vector3f(), 0.5f, 0.5f, 0.5f ) );
        playerLeftPart = dynamicNode.createBox( "left" );
        playerLeftPart.getLocalTranslation().set(0,0,0.4f);
        playerLeftPart.getLocalScale().set(1,1,0.2f);
        playerRightPart = dynamicNode.createBox( "right" );
        playerRightPart.getLocalTranslation().set(0,0,-0.4f);
        playerRightPart.getLocalScale().set(1,1,0.2f);
        return dynamicNode;
    }

    @Override
    protected void simpleUpdate() {
        // move the cam where the player is
        cam.getLocation().x = player.getLocalTranslation().x;
        cam.update();
        cameraInputHandler.setEnabled( MouseInput.get().isButtonDown( 1 ) );
    }

    /**
     * The main method to allow starting this class as application.
     *
     * @param args command line arguments
     */
    public static void main( String[] args ) {
        Logger.getLogger( "" ).setLevel( Level.WARNING ); // to see the important stuff
        new Lesson8().start();
    }
}

/*
 * $log$
 */

i made a little example using GameControls and ActionRepeatController + FrictionCallback.

Steering is not done with Torque but directly with a RotationController.

You can Steer either with Mouse or A/D.

Accelerate / break with W/S.







import com.jme.bounding.BoundingBox;
import com.jme.input.InputHandler;
import com.jme.input.KeyInput;
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.MouseAxisBinding;
import com.jme.input.controls.controller.ActionRepeatController;
import com.jme.input.controls.controller.Axis;
import com.jme.input.controls.controller.RotationController;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.shape.Box;
import com.jme.scene.state.MaterialState;
import com.jmex.physics.DynamicPhysicsNode;
import com.jmex.physics.StaticPhysicsNode;
import com.jmex.physics.callback.FrictionCallback;
import com.jmex.physics.material.Material;
import com.jmex.physics.util.SimplePhysicsGame;


public class SimplePhysicsMoving extends SimplePhysicsGame {
   private StaticPhysicsNode envionment = null;

   private DynamicPhysicsNode player = null;
   private RotationController turnControl ;
   
   @Override
   protected void simpleInitGame() {
      input = new InputHandler();
      
      createPlayer();
      initPlayerInput();
      createEnvironment();

      cam.setLocation(new Vector3f(0, 50, 55));
      cam.lookAt(player.getLocalTranslation(), Vector3f.UNIT_Y);
      
   }

   private void createEnvironment() {
      int width = 50;
      int length = 50;
      
      envionment = getPhysicsSpace().createStaticNode();
      
      MaterialState ms = display.getRenderer().createMaterialState();
      ms.setDiffuse(ColorRGBA.cyan);
      
        Box visualFloor = new Box("vis floor", new Vector3f(), width, 1.0f, length);
        visualFloor.setModelBound(new BoundingBox());
        visualFloor.updateModelBound();
        visualFloor.setRenderState(ms);
        envionment.attachChild(visualFloor);
       
        Box backWall = new Box("back wall", new Vector3f(), width/2, 5, 1);
        backWall.setModelBound(new BoundingBox());
        backWall.updateModelBound();
        envionment.attachChild(backWall);
       
        envionment.generatePhysicsGeometry();
        envionment.setMaterial(Material.CONCRETE);
        envionment.setLocalTranslation(0, -1.5f, -length/2);
       
        rootNode.attachChild(envionment);
   }
   
   private void createPlayer() {
      player = getPhysicsSpace().createDynamicNode();
      
      MaterialState ms = display.getRenderer().createMaterialState();
      ms.setDiffuse(ColorRGBA.darkGray);
      ms.setAmbient(ColorRGBA.lightGray);
      ms.setShininess(128);
      ms.setSpecular(ColorRGBA.white);
      
      Box visualPlayer = new Box("player", new Vector3f(), 0.5f, 0.2f, 0.7f);
      visualPlayer.setModelBound(new BoundingBox());
      visualPlayer.updateModelBound();
      visualPlayer.setRenderState(ms);
      player.attachChild(visualPlayer);
      
      player.generatePhysicsGeometry();
      player.setMaterial(Material.ICE);
      player.computeMass();
      player.setLocalTranslation(0, 5, 0);
      player.setAffectedByGravity(true);
      
      rootNode.attachChild(player);
   }
   
   private void initPlayerInput() {
      // create a GameControl Manager
      GameControlManager manager = new GameControlManager();
      
      // set up GameControls for the rotation
      GameControl rotateLeft = manager.addControl("Rotate Left");
        rotateLeft.addBinding(new KeyboardBinding(KeyInput.KEY_A));
        rotateLeft.addBinding(new MouseAxisBinding(MouseAxisBinding.AXIS_X,
                true));

        GameControl rotateRight = manager.addControl("Rotate Right");
        rotateRight.addBinding(new KeyboardBinding(KeyInput.KEY_D));
        rotateRight.addBinding(new MouseAxisBinding(MouseAxisBinding.AXIS_X,
                false));
        // create a RotationController to rotate the player around the Y Axis
        turnControl = new RotationController(player, rotateLeft,
                rotateRight, 1, Axis.Y);
        player.addController(turnControl);
       
       
        // set up controls to
      GameControl speedUp = manager.addControl("Speed up");
        speedUp.addBinding(new KeyboardBinding(KeyInput.KEY_W));
       
        GameControl slowDown = manager.addControl("Slow down");
        slowDown.addBinding(new KeyboardBinding(KeyInput.KEY_S));
       
        ActionRepeatController speedUPAction = new ActionRepeatController(speedUp,
              10, new SpeedAction(player, 20, true));
        player.addController(speedUPAction);
       
        ActionRepeatController slowDownAction = new ActionRepeatController(slowDown,
              10, new SpeedAction(player, 20, false));
        player.addController(slowDownAction);
       
        FrictionCallback frictionCallback = new FrictionCallback();
        frictionCallback.add(player, 10, 10);
        getPhysicsSpace().addToUpdateCallbacks(frictionCallback);
   }
   

   @Override
   protected void simpleUpdate() {
      super.simpleUpdate();
   }
   
   class SpeedAction implements Runnable {
      private DynamicPhysicsNode player = null;
      private float invert = 1;
      private float speed = 20;
      
      public SpeedAction(DynamicPhysicsNode player, float speed, boolean forward) {
         this.player = player;
         if (forward) {
            invert = 1;
         } else {
            invert = -1;
         }
      }
      
      @Override
      public void run() {
         player.addForce(player.getLocalRotation().getRotationColumn(2).normalize().mult(speed*invert));
      }
   }
   
   
   /**
    * @param args
    */
   public static void main(String[] args) {
      new SimplePhysicsMoving().start();
   }
}

Wow…one of those approaches looks a crap-load easier to understand. 

sure, as Core-Dump stated himself, he didn't use proper physical modeling but simply rotated the tank directly. This allows penetrating walls, teleporting stuff which lies around, etc… but it's easy :slight_smile:

Actually, it's trivial to use physics forces instead with GameControls…I was referring more specifically to GameControls versus InputHandler…and more than anything just giving you a hard time.  }:-@

Well, the difference between InputHandler and GameControl is a matter of taste, I guess, as this

GameControl rotateRight = manager.addControl("Rotate Right");
        rotateRight.addBinding(new KeyboardBinding(KeyInput.KEY_D));
        rotateRight.addBinding(new MouseAxisBinding(MouseAxisBinding.AXIS_X,
                false));
        // create a RotationController to rotate the player around the Y Axis
        turnControl = new RotationController(player, rotateLeft,
                rotateRight, 1, Axis.Y);
        player.addController(turnControl);


is not very different from this


input.addAction( new MoveAction( new Vector3f( -2, 0, 0 ), playerLeftPart ),
                InputHandler.DEVICE_KEYBOARD, KeyInput.KEY_DELETE, InputHandler.AXIS_NONE, false );


Maybe more verbose, yes.

But the important point for dna5122 probably is, that Core-Dump gave an example for moving the tank with forces and dna5122 can get started with this more easily than with surface motion.

I also used ICE Material for the Tank, so he can slide over the surface (its actually a hover-tank ;)) and it seems my movement is not very smooth.

You need to try a few ways yourself and see what fits best.



There are always so many ways to achive something :slight_smile:

I like hover tanks. :slight_smile:

I've added a new feature for you, dna5122: mu orthogonal aligned to surface motion :slight_smile:



After updating from CVS you can add these lines after "playerRightPart.setMaterial( new Material( "player material right" ) );" in my example above:

        MutableContactInfo info = new MutableContactInfo();
        {
            info.setMu( 1 );
            info.setBounce( 0.4f );
            info.setMinimumBounceVelocity( 1f );
            info.setMuOrthogonal( 0.01f );
        }
        playerLeftPart.getMaterial().putContactHandlingDetails( null, info );
        playerRightPart.getMaterial().putContactHandlingDetails( null, info );



The box is then controlled tank-like, smoothly.

p.s. I've added the tank example as Lesson8b



If this works out for you, maybe you can put together some documentation you have missed before.

wow, less than 24hours and a 'custom' piece is added for a user; that is amazing. Simply amazing…

uh, it wasn't me… I ain't have time for nothing :stuck_out_tongue:

double negatives will get you every time

Still…tanks! I mean thanks!  That is amazing for under 24 hours, actualy 12 hours!



I'll check it out.  Again, thanks for all the suggestions.

What's the difference between attaching a com.jme.scene.shape.Box to a PhysicsNode and calling createBox(String)?  Is it just that former can be textured and the latter can't?

the latter is only the physical representation of the box, this os only visible when the PhysicsDebugger is enabled.



usually you attach a visual object like a Box or Sphere to a physics node and then call generatePhysicsGeometry() / computeMass() and let jme physics do the work.