Create a movement system to move characters around the map

I have searched the forums and no one seems to have discussed in detail how to complete the movement system without using the physics engine.

This is one of the few discussions of movement systems that I’ve found.

Maybe I’m using the wrong keyword to search :sweat_smile:

If anyone would like to tell me more about the movement system please reply to me in this post

Thank you very much :smiling_face_with_three_hearts:

How do you want it to behave?

You say “no physics” but you probably still want it to interact with the world somehow? What should it do?

Edit: also, what are the issues with physics that you are trying to avoid in this approach?

2 Likes

Maybe there was a translation problem,
What I’m trying to say is avoid using minie and bullets to control character movement
Rather than not using physics
I don’t seem to find the relevant solutions in the forums.

yaRnMcDonuts gave me a bit of inspiration, using ray to control the characters

But what kind of physics interaction do you want?

Either:

  1. characters hit physical objects and stop but nothing else happens
  2. characters hit physical objects and the objects might go flying away and character doesn’t stop.
  3. characters are physical objects and both can be moved by collisions.

If a boulder goes rolling into a player, does the player move? Or does the boulder just stop like the player was a concrete post?

If you literally don’t want physics you’d just move the camera

I just want the character to move or jump over the terrain,

I’m now trying yaRnMcDonuts’s solution of ray to implement mobile

If you have a better suggestion please let me know :grinning:

Thank you very much for your reply with

Yes long ago I noticed it is not possible to use BetterCharacterControl or CharacterControl for large scale games because the framerate begins to drop at extreme rates once you have more then 15-30 character controls in the scene. This is alright for most games especially ones like platformers that benefit from as realistic physics as possible, but for things like RPGs and RTS you usually need to support many more NPCs, and using real physics quickly becomes overkill and tanks the framerate.

I have a good implementation but unfortunately it is mixed in with a lot of my networking and game specific code so its never been something that I can easily share the code for.

If I have the time I’d like to eventually make a minimal implementation to share with the JME community and will probably call it somthing like LightweightCharacterControl, although right now that’s a lower priority since I’m still working on my game and there doesn’t seem to be a high demand for such a thing in the jme community since most games can usually get by using bullet/minie physics for character movement and rarely need more than 20 npcs.


But I can explain the general idea behind my lightweight character movement system and hopefully that can help you come up with something similar.

I use a minimum of 2 ray casts every frame for each character. (and sometimes every other frame for far-away NPC if there are too many, but I haven’t had this issue yet).

The first ray cast is cast from the character’s head towards the ground, and I use that collision distnace along with the character’s height to position the character at the correct y location. If they are too high off the ground, I let gravity accumulate so they are falling. And if their feet are clipping under the terrain, the characer gets pushed up accordingly.

The second ray cast is cast forward every frame in the direction the player is about to move. If a collision with a wall is detected close enough, then it pushes the player away based on the direction the player tried moving as well as the contact normal of the object they collided with.

For determining collisions between multiple npcs and players, you could also use a ray cast and handle it similar to hitting a wall. Or you can just use each npc’s radius and distance between one another to simulate less realistic pushy-physics so that characters can overlap but will push each other away. I still am experimenting with different options in this area so I can’t say much more on whats best yet.

You can also add rays casting to the left and right of each npc if you want more realistic movement collision, but this is another thing I haven’t done yet because it only prevents visual clipping when going through tight spaces, and right now thats not an important bug imo and isn’t worth the extra ray casts haha. But I plan on working on this more in the future, and will probably experiment with a few extra ray casts per npc when they’re close to the player so it operates a bit more realistically while still minimizing ray casts, since the ray casts are where the real performance hits will happen.

Hopefully this is helpful, and if you have anymore questions I’m glad to help whenever I can.

5 Likes

There is in Unity a None Physics Character Controller with Jump and Ground Check Only and There is a RigidBodyController

The Character Controller from Unity Manual

The Character Controller is a component you can add to your player. Its function is to move the player according to the environment (the colliders).
It doesn’t respond nor uses physics in any way.
On top of that, the Character Controller comes with a Capsule Collider.

I tried using something similar but I faced obstacle with intersect method which returns every vertices collided with each other whereas I just want the name of two object that collided

1 Like

Do you mean a waypoint system like this ?

I think you might need to implement it by your own.

I made a simple move with the ray but I kept having problems with the Y-axis can you briefly tell me how you made the Y-axis fit the ground without shaking

movedemo.jar

/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package mygame;


import camera.TerrainChaseCamera;

import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.audio.AudioListenerState;
import com.jme3.collision.CollisionResults;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;


/**
 *
 * @author Icyboxs
 */
public class movedemo extends SimpleApplication implements ActionListener{
     private TerrainChaseCamera chaseCam;
     private CollisionResults results = new CollisionResults();
     public Node model = new Node("model");
     public Node camNode = new Node("cam");
     public Node terrainNode = new Node("terrainNode");
     public boolean left = false, right = false, up = false, down = false, v = false,isPresseds=false;
    public final static String BUTTON_LEFT = "	BUTTON_LEFT";
    public final static String DEBUG = "DEBUG";
    public final static String DEBUG1 = "DEBUG1";
    public final static String FORWARD = "forward";
    public final static String BACKWARD = "backward";
    public final static String LEFT = "left";
    public final static String RIGHT = "right";
    public final static String V = "v";
    public final static String JUMP = "jump";
    public Vector3f camDir = new Vector3f();
    public Vector3f camLeft = new Vector3f();
    private Ray ray=new Ray();
  public movedemo(){
        super(  
              new StatsAppState(), 
              new AudioListenerState(),
              new DebugKeysAppState()


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

    @Override
    public void simpleInitApp() {
        Box b = new Box(10, 10, 10);
        Geometry geom = new Geometry("Box", b);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);
        geom.move(0, 10, 0);
        
        Box b2 = new Box(10, 10, 10);
        Geometry geom2 = new Geometry("Box", b);
        Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat2.setColor("Color", ColorRGBA.Green);
        geom2.setMaterial(mat2);
        rootNode.attachChild(geom2);
        Box box = new Box(500, 2, 500);
        Geometry geom1 = new Geometry("Box", box);
        Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat1.setColor("Color", ColorRGBA.White);
        geom1.setMaterial(mat1);
        camNode.setLocalTranslation(0, 5, 0);
        model.attachChild(geom);
        model.attachChild(camNode);
        System.err.println(terrainNode.getChildren());
        terrainNode.attachChild(geom1);
       rootNode.attachChild(terrainNode);
        rootNode.attachChild(model);
        initKeys();
        TerrainCollisionCamera();
    }
       
    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
        terrainNode.collideWith(RayCollision(), results);
        CameraCollision(results);
        RoleMove();
    }
    
    public void TerrainCollisionCamera() {
        chaseCam = new TerrainChaseCamera(cam, model.getChild("cam"), inputManager,this);
        chaseCam.setDebugFrustum(false);
       // chaseCam.createCameraFrustum();// 开启DEBUG 相机
        chaseCam.setCameraCollisionDetection(true, "terrainNode");//开启地形碰撞设置地形名称
        chaseCam.setDragToRotate(false); //把鼠标锁定在窗口里
        chaseCam.setCursorVisible(false);//设置光标不可见
        chaseCam.setInvertVerticalAxis(true);//反转鼠标的垂直轴移动
        chaseCam.setMinDistance(1f);
        chaseCam.setMaxDistance(1000f);
        chaseCam.setDefaultVerticalRotation(0);
        chaseCam.setMinVerticalRotation(-1.57f);
    }
    
       private void initKeys() {
        inputManager.addMapping(BUTTON_LEFT, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addMapping(DEBUG, new KeyTrigger(KeyInput.KEY_F1));
        inputManager.addMapping(DEBUG1, new KeyTrigger(KeyInput.KEY_F2));
        inputManager.addMapping(LEFT, new KeyTrigger(KeyInput.KEY_A));
        inputManager.addMapping(RIGHT, new KeyTrigger(KeyInput.KEY_D));
        inputManager.addMapping(FORWARD, new KeyTrigger(KeyInput.KEY_W));
        inputManager.addMapping(BACKWARD, new KeyTrigger(KeyInput.KEY_S));
        inputManager.addMapping(JUMP, new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addMapping(V, new KeyTrigger(KeyInput.KEY_V));
        inputManager.addListener(this, BUTTON_LEFT, DEBUG, DEBUG1, LEFT, RIGHT, FORWARD, BACKWARD, JUMP, V);
    }
       
        @Override
    public void onAction(String name, boolean bln, float f) {
        if (FORWARD.equals(name)){
          if(bln){
          System.err.println("按下W");
          up=true;
          }else if(!bln){
          System.err.println("弹起W");
          up=false;
          }
        }else if (BACKWARD.equals(name)){
          if(bln){
          System.err.println("按下S");
          down=true;
          }else if(!bln){
          System.err.println("弹起S");
          down=false;
          }
        }else if (LEFT.equals(name)){
        if(bln){
            System.err.println("按下A");
            left = true;
        }else if(!bln){
            System.err.println("弹起A");
            left = false;
        }
        }else if (RIGHT.equals(name)){
                if(bln){
            System.err.println("按下D");
            right = true;
        }else if(!bln){
            System.err.println("弹起D");
            right = false;
        }     
        }else if (V.equals(name)){
         if(bln){
            System.err.println("按下V");
           model.move(0,0,0);
        }else if(!bln){
            System.err.println("弹起V");
            
        }     
        }
    }
    
       public void RoleMove() {
        camDir.set(cam.getDirection()).multLocal(0.5f);
        camLeft.set(cam.getLeft()).multLocal(0.5f);

        System.err.println(camDir.y+","+camLeft.y);

        if(up){
        model.move(camDir);
        }else if(down){
        model.move(camDir.negate());
       
        }else if(left){
             model.move(camLeft);
         
        }else if(right){
            model.move(camLeft.negate());
        }

   }
       
           public Ray RayCollision() {
        
         ray.setOrigin(model.getChild("cam").getWorldTranslation());
         ray.setDirection(model.getChild("cam").getLocalTranslation().normalize().negate());
         ray.setLimit((float)100);
    return ray;
    }
           
       private void CameraCollision(CollisionResults results) {
        System.err.println(results.size());
        /**
         * 判断检测结果 Judgment ray result
         */
        if (results.size() > 0) {


            // 离射线原点最近的交点
            Vector3f closest = results.getClosestCollision().getContactPoint();
            // 离射线原点最远的交点
            Vector3f farthest = results.getFarthestCollision().getContactPoint();
           
            // 离射线原点最近的距离
            float ClosestDist = results.getClosestCollision().getDistance();
            float FarthestDist = results.getFarthestCollision().getDistance();

                if(ClosestDist<10){
//                    camDir.y=(float) 1.0;
//                    camLeft.y=(float) 1.0;
                }else if(ClosestDist>10){
//                    camDir.y=(float) -1.0;
//                    camLeft.y=(float) -1.0;
               }
                System.err.println("最近点(Nearest point),"+ClosestDist);
                System.err.println("最远点(Farthest point),"+FarthestDist);
           results.clear();
        }else{

        }
    }
}

TerrainChaseCamera.jar

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package camera;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetManager;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.input.CameraInput;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.input.ChaseCamera;
import com.jme3.input.InputManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.WireFrustum;
import com.jme3.shadow.ShadowUtil;
import java.util.Objects;
import java.util.logging.Logger;
/**
 *
 * @author Icyboxs
 */
public class TerrainChaseCamera extends ChaseCamera {
private static Logger log =Logger.getLogger(TerrainChaseCamera.class.toString());

private float maxDistances=10f;
private Quaternion quaternion= new Quaternion();
private Vector3f afterVector= new Vector3f();
protected  boolean DebugFrustum= false;
protected boolean CameraCollisionDetection= false;
private SimpleApplication simpleApp;
private CollisionResults results = new CollisionResults();
private String ChildName;
private boolean CursorVisible =true;
  // 射线
private Ray ray=new Ray();

    public TerrainChaseCamera(Camera cam, Spatial target, InputManager inputManager,SimpleApplication simpleApp) {
        super(cam, target, inputManager);
        this.simpleApp=simpleApp;
    }
    
   public TerrainChaseCamera(Camera cam, Spatial target, InputManager inputManager) {
        super(cam, target, inputManager);
    }


    /**
     * 使用射线检测,判断离摄像机最近的点。
     */
    public Ray RayCollision() {
         ray.setOrigin(targetLocation);
         ray.setDirection(cam.getLocation().subtract(targetLocation).normalize());
        // System.err.println(ray.getDirection());
    return ray;
    }
    
    public Camera getcam(){
    
        
        return cam;
    
    }
    
     /**
     * Set whether the mouse cursor should be visible or not.
     *
     * @param visible whether the mouse cursor should be visible or not.
     */
    public boolean getCursorVisible(){
    return this.CursorVisible;
}
public boolean setCursorVisible(boolean CursorVisible){
    return this.CursorVisible=CursorVisible;
}
    
     /**
     * 是否开启DEBUG相机
     *
     * 
     */ 
public boolean getDebugFrustum(){
    return this.DebugFrustum;
}
public boolean setDebugFrustum(boolean DebugFrustum){
    if(DebugFrustum){
    createCameraFrustum();
    }
    
    return this.DebugFrustum=DebugFrustum;
}
    
     /**
     * 相机地形碰撞状态
     *
     * 
     */ 
public boolean getCameraCollisionDetection(){
    return this.CameraCollisionDetection;
}
     /**
     * 开启相机地形碰撞 设置开启和碰撞地形名称
     */ 
public boolean setCameraCollisionDetection(boolean CameraCollisionDetection,String ChildName){
    if(CameraCollisionDetection){
    this.ChildName=ChildName;
    return this.CameraCollisionDetection=CameraCollisionDetection;
    }else{
    this.ChildName=ChildName;
    return this.CameraCollisionDetection=CameraCollisionDetection;
   
    }
    
}


    /**
     * update the camera control, should only be used internally
     *
     * @param tpf time per frame (in seconds)
     */
    @Override
    public void update(float tpf) {
        updateCamera(tpf);
        updateCameraExtension(tpf);
    }
/**
 *追逐相机扩展功能
 *@param tpf time per frame (in seconds)
 **/
     public void updateCameraExtension(float tpf) {
       inputManager.setCursorVisible(CursorVisible);
       if(DebugFrustum){
       simpleApp.getRootNode().getChild("Viewing.Frustum").setLocalTranslation(cam.getLocation());
       simpleApp.getRootNode().getChild("Viewing.Frustum").setLocalRotation(cam.getRotation());
       }
       if(CameraCollisionDetection){
      //射线检测
      if(ChildName == null){
       
       }else{
        simpleApp.getRootNode().getChild(ChildName).collideWith(RayCollision(), results);
        CameraCollision(results);
      }

       }
     }
    
    
public  void createCameraFrustum() {
	//this.DebugFrustum=true;
       Vector3f[]  points = new Vector3f[8];
	for (int i = 0; i < 8; i++) {
		points[i] = new Vector3f();
	}
	
	Camera frustumCam = cam.clone();
        frustumCam.setLocation(new Vector3f(0, 0, 0));
        frustumCam.lookAt(Vector3f.UNIT_Z, Vector3f.ZERO);
	ShadowUtil.updateFrustumPoints2(frustumCam, points);
	Mesh mesh = WireFrustum.makeFrustum(points);
	
	Geometry frustumGeo = new Geometry("Viewing.Frustum", mesh);
	Material mat = new Material( simpleApp.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
	mat.setColor("Color", ColorRGBA.Red);
	frustumGeo.setMaterial(mat);
	frustumGeo.setCullHint(Spatial.CullHint.Never);
	frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);
      
        simpleApp.getRootNode().attachChild(frustumGeo);
}




    /**
     * 控制相机碰撞地形缩放
     * 
     * @param results
     */
    private void CameraCollision(CollisionResults results) {

//        System.err.println("碰撞结果:" + results.size());
//        System.err.println("射线:" + ray);

        /**
         * 判断检测结果
         */
        if (results.size() > 0) {
            // 从近到远,打印出射线途径的所有交点。
//            for (int i = 0; i < results.size(); i++) {
//                CollisionResult result = results.getCollision(i);
//
//                float dist = result.getDistance();
//                Vector3f point = result.getContactPoint();
//                Vector3f normal = result.getContactNormal();
//                Geometry geom = result.getGeometry();
//                System.err.printf("序号:%d, 距离:%.2f, 物体名称:%s, 交点:%s, 交点法线:%s\n", i, dist, geom.getName(), point, normal);
//            }

            // 离射线原点最近的交点
            Vector3f closest = results.getClosestCollision().getContactPoint();
            // 离射线原点最远的交点
            Vector3f farthest = results.getFarthestCollision().getContactPoint();
           
            // 离射线原点最近的距离
            float ClosestDist = results.getClosestCollision().getDistance();
            float FarthestDist = results.getFarthestCollision().getDistance();

                if(ClosestDist<=maxDistances){
                    
                    targetDistance=ClosestDist;
//                float incrimentDistance =  (-distance+dist1); // might need subtracted in opposite order 
//               
//                 zoomCamera(incrimentDistance);

                }else{
                   
               }
          //System.err.printf("最近点:%s, 最远点:%s\n", closest, farthest);
         //    System.err.printf("离射线原点最近的距离:%s\n", dist);
             
           results.clear();
        }else{
           
//setMinDistance(10f);
//setMaxDistance(10f);
           // zoomCamera(10f);
           // zoomCamera(10);getMaxDistance()
           targetDistance=maxDistances;
         //  System.err.printf("getDistanceToTarget:%s\n", getDistanceToTarget());
           
        }
//      System.err.printf("maxDistances:%s\n", maxDistances);
//      System.err.printf("minDistance:%s\n", minDistance);
//       System.err.println(distance);
    }
    
        @Override
    public void onAnalog(String name, float value, float tpf) {
        if (name.equals(CameraInput.CHASECAM_MOVELEFT)) {
            rotateCamera(-value);
        } else if (name.equals(CameraInput.CHASECAM_MOVERIGHT)) {
            rotateCamera(value);
        } else if (name.equals(CameraInput.CHASECAM_UP)) {
            vRotateCamera(value);
        } else if (name.equals(CameraInput.CHASECAM_DOWN)) {
            vRotateCamera(-value);
        } else if (name.equals(CameraInput.CHASECAM_ZOOMIN)) {
            zoomCamera(-value);
            maxDistances += -value * zoomSensitivity;
            if(maxDistances<minDistance){
            maxDistances=minDistance;
            }
            if (zoomin == false) {
                distanceLerpFactor = 0;
            }
            zoomin = true;
        } else if (name.equals(CameraInput.CHASECAM_ZOOMOUT)) {
            maxDistances += value * zoomSensitivity;
            if(maxDistances>maxDistance){
            maxDistances=maxDistance;
            }
            zoomCamera(+value);
            if (zoomin == true) {
                distanceLerpFactor = 0;
            }
            zoomin = false;
        }
    }
    
}

I commented out ray’s judgment on the Y-axis.
Please point out if there is any problem with the above code.
Let me know if you have any ideas.

       private void CameraCollision(CollisionResults results) {
        System.err.println(results.size());
        /**
         * 判断检测结果 Judgment ray result
         */
        if (results.size() > 0) {


            // 离射线原点最近的交点
            Vector3f closest = results.getClosestCollision().getContactPoint();
            // 离射线原点最远的交点
            Vector3f farthest = results.getFarthestCollision().getContactPoint();
           
            // 离射线原点最近的距离
            float ClosestDist = results.getClosestCollision().getDistance();
            float FarthestDist = results.getFarthestCollision().getDistance();

                if(ClosestDist<10){
//                    camDir.y=(float) 1.0;
//                    camLeft.y=(float) 1.0;
                }else if(ClosestDist>10){
//                    camDir.y=(float) -1.0;
//                    camLeft.y=(float) -1.0;
               }
                System.err.println("最近点(Nearest point),"+ClosestDist);
                System.err.println("最远点(Farthest point),"+FarthestDist);
           results.clear();
        }else{

        }
    }

This control ray detects the distance between the blue box mounted camNode and the ground

My idea is very simple. If the ray that we’re looking down detects that the current position intersects the ray below a certain value, we increase the Y-axis value Otherwise vice versa.

But there are still a lot of problems in the actual operation,
It’s not as smooth as in your video

For this part you need to account for the character’s height and the collisionDistance in order to determine the exact difference that the character needs moved vertically to remain with feet smoothly on the terrain.

I personally like to use the yExtent value of a character’s worldBounds to determine the character’s height, since it is an accurate and consistent measurement of a character’s height based on its model.

so you would want to do something like :

float height = character.getWorldBound().getYExtent();
float collisionDist = closest.distance(rayOrigin);

float yAmountToMove = height - collisinDist;

characterNode.move(0, yAmountToMove, 0);

This code also assumes that your rayOrigin for the down ray is being cast from the top of the character’s head.

And also you’ll want to add code for accumulating downward velocity if the player is determined to be in the air, since this code I posted as an example will automatically warp the player back to the ground even if they are 1000 feet in the air. Instead you’d want to add gravity * tpf to the character’s downward velocity every frame.

And of course you will eventually want to add more code to do extra little things depending on the requirements of your game, like you may want to limit the character’s step height, since by default it will be letting them step as high as their height. And you may also want to eventually add some code for acceleration / deceleration, or add some code for ignoring gravity if the character is considered to be swimming or flying with magic, etc etc.

1 Like

Thank you for your reply. It looks much better now

1 Like