RTS Camera control (9 posts)

  • Profile picture of abies abies34p said 1 year, 5 months ago:

    This is a class which allows you a rts-like camera control with keyboard. It assumes flat terrain, with up being y positive direction. Camera is looking at certain point from above. You can rotate camera around that point (Q and E keys), change tilt between directly from top to almost parallel to ground (R and F keys), zoom in and out (Z and X keys) and move the camera around changing the place you are looking at (WASD controls).

    setCenter method allows you to set starting point. You may want to put y coordinate slightly above the ground level to avoid too close zoom.

    You can control minimum/maximum tilt/zoom with setMinMaxValues method (don’t allow tilt to go to PI/2, as it reverses the camera at the very end). You can play with max speed/acceleration with setMaxSpeed methods. Certain degree of inertia is implemented, so you are not stopping/reversing immediately. Feel bit better that way, but can be annoying if you set acceleration time to be too long. In such case, it will probably make sense to deccelerate faster – should be simple extension.

    You can add it to your scenegraph with code like

                    final RtsCam rtsCam = new RtsCam(cam, rootNode);
    		rtsCam.registerWithInput(inputManager);
    		rtsCam.setCenter(new Vector3f(20,0.5f,20));
    

    possibly detaching default fly cam with inputManager.removeListener(flyCam);

    import java.io.IOException;
    
    import com.jme3.export.JmeExporter;
    import com.jme3.export.JmeImporter;
    import com.jme3.input.InputManager;
    import com.jme3.input.KeyInput;
    import com.jme3.input.controls.ActionListener;
    import com.jme3.input.controls.KeyTrigger;
    import com.jme3.math.Vector3f;
    import com.jme3.renderer.Camera;
    import com.jme3.renderer.RenderManager;
    import com.jme3.renderer.ViewPort;
    import com.jme3.scene.Spatial;
    import com.jme3.scene.control.Control;
    
    public class RtsCam implements Control, ActionListener {
    
    	public enum Degree {
    		SIDE,
    		FWD,
    		ROTATE,
    		TILT,
    		DISTANCE
    	}
    
    	private InputManager inputManager;
    	private final Camera cam;
    
    	private int[] direction = new int[5];
    	private float[] accelPeriod = new float[5];
    
    	private float[] maxSpeed = new float[5];
    	private float[] maxAccelPeriod = new float[5];
    	private float[] minValue = new float[5];
    	private float[] maxValue = new float[5];
    
    	private Vector3f position = new Vector3f();
    
    	private Vector3f center = new Vector3f();
    	private float tilt = (float)(Math.PI / 4);
    	private float rot = 0;
    	private float distance = 15;
    
    	private static final int SIDE = Degree.SIDE.ordinal();
    	private static final int FWD = Degree.FWD.ordinal();
    	private static final int ROTATE = Degree.ROTATE.ordinal();
    	private static final int TILT = Degree.TILT.ordinal();
    	private static final int DISTANCE = Degree.DISTANCE.ordinal();
    
    	public RtsCam(Camera cam, Spatial target) {
    		this.cam = cam;
    
    		setMinMaxValues(Degree.SIDE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
    		setMinMaxValues(Degree.FWD, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
    		setMinMaxValues(Degree.ROTATE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
    		setMinMaxValues(Degree.TILT, 0.2f, (float)(Math.PI / 2) - 0.001f);
    		setMinMaxValues(Degree.DISTANCE, 2, Float.POSITIVE_INFINITY);
    
    		setMaxSpeed(Degree.SIDE,10f,0.4f);
    		setMaxSpeed(Degree.FWD,10f,0.4f);
    		setMaxSpeed(Degree.ROTATE,2f,0.4f);
    		setMaxSpeed(Degree.TILT,1f,0.4f);
    		setMaxSpeed(Degree.DISTANCE,15f,0.4f);
                    target.addControl(this);
    	}
    
    	public void setMaxSpeed(Degree deg, float maxSpd, float accelTime) {
    		maxSpeed[deg.ordinal()] = maxSpd/accelTime;
    		maxAccelPeriod[deg.ordinal()] = accelTime;
    	}
    
    	public void registerWithInput(InputManager inputManager) {
    		this.inputManager = inputManager;
    
    		String[] mappings = new String[] { "+SIDE", "+FWD", "+ROTATE", "+TILT", "+DISTANCE",
    				"-SIDE", "-FWD", "-ROTATE", "-TILT", "-DISTANCE", };
    
    		inputManager.addMapping("-SIDE", new KeyTrigger(KeyInput.KEY_A));
    		inputManager.addMapping("+SIDE", new KeyTrigger(KeyInput.KEY_D));
    		inputManager.addMapping("+FWD", new KeyTrigger(KeyInput.KEY_S));
    		inputManager.addMapping("-FWD", new KeyTrigger(KeyInput.KEY_W));
    		inputManager.addMapping("+ROTATE", new KeyTrigger(KeyInput.KEY_Q));
    		inputManager.addMapping("-ROTATE", new KeyTrigger(KeyInput.KEY_E));
    		inputManager.addMapping("+TILT", new KeyTrigger(KeyInput.KEY_R));
    		inputManager.addMapping("-TILT", new KeyTrigger(KeyInput.KEY_F));
    		inputManager.addMapping("-DISTANCE", new KeyTrigger(KeyInput.KEY_Z));
    		inputManager.addMapping("+DISTANCE", new KeyTrigger(KeyInput.KEY_X));
    
    		inputManager.addListener(this, mappings);
    		inputManager.setCursorVisible(true);
    	}
    
    	public void write(JmeExporter ex) throws IOException {
    
    	}
    
    	public void read(JmeImporter im) throws IOException {
    
    	}
    
    	public Control cloneForSpatial(Spatial spatial) {
    		RtsCam other = new RtsCam(cam, spatial);
    		other.registerWithInput(inputManager);
    		return other;
    	}
    
    	public void setSpatial(Spatial spatial) {
    
    	}
    
    	public void setEnabled(boolean enabled) {
    
    	}
    
    	public boolean isEnabled() {
    
    		return true;
    	}
    
    	public void update(final float tpf) {
    
    		for (int i = 0; i < direction.length; i++) {
    			int dir = direction[i];
    			switch (dir) {
    			case -1:
    				accelPeriod[i] = clamp(-maxAccelPeriod[i],accelPeriod[i]-tpf,accelPeriod[i]);
    				break;
    			case 0:
    				if (accelPeriod[i] != 0) {
    					double oldSpeed = accelPeriod[i];
    					if (accelPeriod[i] > 0) {
    						accelPeriod[i] -= tpf;
    					} else {
    						accelPeriod[i] += tpf;
    					}
    					if (oldSpeed * accelPeriod[i] < 0) {
    						accelPeriod[i] = 0;
    					}
    				}
    				break;
    			case 1:
    				accelPeriod[i] = clamp(accelPeriod[i],accelPeriod[i]+tpf,maxAccelPeriod[i]);
    				break;
    			}
    
    		}
    
    		distance += maxSpeed[DISTANCE] * accelPeriod[DISTANCE] * tpf;
    		tilt += maxSpeed[TILT] * accelPeriod[TILT] * tpf;
    		rot += maxSpeed[ROTATE] * accelPeriod[ROTATE] * tpf;
    
    		distance = clamp(minValue[DISTANCE],distance,maxValue[DISTANCE]);
    		rot = clamp(minValue[ROTATE],rot,maxValue[ROTATE]);
    		tilt = clamp(minValue[TILT],tilt,maxValue[TILT]);
    
    		double offX = maxSpeed[SIDE] * accelPeriod[SIDE] * tpf;
    		double offZ = maxSpeed[FWD] * accelPeriod[FWD] * tpf;
    
    		center.x += offX * Math.cos(-rot) + offZ * Math.sin(rot);
    		center.z += offX * Math.sin(-rot) + offZ * Math.cos(rot);
    
    		position.x = center.x + (float)(distance * Math.cos(tilt) * Math.sin(rot));
    		position.y = center.y + (float)(distance * Math.sin(tilt));
    		position.z = center.z + (float)(distance * Math.cos(tilt) * Math.cos(rot));
    
    		cam.setLocation(position);
    		cam.lookAt(center, new Vector3f(0,1,0));
    
    	}
    
    	private static float clamp(float min, float value, float max) {
    		if ( value < min ) {
    			return min;
    		} else if ( value > max ) {
    			return max;
    		} else {
    			return value;
    		}
    	}
    
    	public float getMaxSpeed(Degree dg) {
    		return maxSpeed[dg.ordinal()];
    	}
    
    	public float getMinValue(Degree dg) {
    		return minValue[dg.ordinal()];
    	}
    
    	public float getMaxValue(Degree dg) {
    		return maxValue[dg.ordinal()];
    	}
    
    	// SIDE and FWD min/max values are ignored
    	public void setMinMaxValues(Degree dg, float min, float max) {
    		minValue[dg.ordinal()] = min;
    		maxValue[dg.ordinal()] = max;
    	}
    
    	public Vector3f getPosition() {
    		return position;
    	}
    
    	public void setCenter(Vector3f center) {
    		this.center.set(center);
    	}
    
    	public void render(RenderManager rm, ViewPort vp) {
    
    	}
    
    	public void onAction(String name, boolean isPressed, float tpf) {
    		int press = isPressed ? 1 : 0;
    
    		char sign = name.charAt(0);
    		if ( sign == '-') {
    			press = -press;
    		} else if (sign != '+') {
    			return;
    		}
    
    		Degree deg = Degree.valueOf(name.substring(1));
    		direction[deg.ordinal()] = press;
    	}
    }
    
  • Profile picture of damyon damyon said 7 months, 3 weeks ago:

    I just tried this and it works great. Thanks!

    - Damyon

  • Profile picture of tycal tycal said 6 months, 4 weeks ago:

    thanks for contribute your code! :D

  • Profile picture of kwando kwando80p said 5 months, 1 week ago:

    Can I use this in my game?

  • Profile picture of normen normen1271p said 5 months, 1 week ago:

    Sure, I guess thats why abies posted it here ;)

  • Profile picture of germanosk germanosk said 3 months, 3 weeks ago:

    Can anyone put a simple example using this code and the deafult models?
    Plz.

  • Profile picture of normen normen1271p said 3 months, 3 weeks ago:

    oO its shown the first lines of the post:

    final RtsCam rtsCam = new RtsCam(cam, rootNode);
    rtsCam.registerWithInput(inputManager);
    rtsCam.setCenter(new Vector3f(20,0.5f,20));
  • Profile picture of germanosk germanosk said 3 months, 3 weeks ago:

    @normen I was asking about something like that, but I already did by myself.

    /*
     * To change this template, choose Tools | Templates
     * and open the template in the editor.
     */
    package mygame;
    
    import com.jme3.animation.AnimChannel;
    import com.jme3.animation.AnimControl;
    import com.jme3.animation.AnimEventListener;
    import com.jme3.animation.LoopMode;
    import com.jme3.app.SimpleApplication;
    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.math.ColorRGBA;
    import com.jme3.math.Vector3f;
    import com.jme3.scene.Node;
    
    /** Sample 7 - how to load an OgreXML model and play an animation,
     * using channels, a controller, and an AnimEventListener. */
    public class RtsTest extends SimpleApplication
      implements AnimEventListener {
      private AnimChannel channel;
      private AnimControl control;
      Node player;
      public static void main(String[] args) {
        RtsTest app = new RtsTest();
        app.start();
      }
    
      @Override
      public void simpleInitApp() {
        viewPort.setBackgroundColor(ColorRGBA.LightGray);
        initKeys();
        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
        rootNode.addLight(dl);
        player = (Node) assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
        player.setLocalScale(new Vector3f(.05f,.05f,.05f));
        rootNode.attachChild(player);
        control = player.getControl(AnimControl.class);
        control.addListener(this);
        channel = control.createChannel();
        channel.setAnim("Stealth");
    
      final RtsCam rtsCam = new RtsCam(cam, rootNode);
        rtsCam.registerWithInput(inputManager);
        rtsCam.setCenter(new Vector3f(20,20,20));
    
      }
    
      public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
        if (animName.equals("Walk")) {
          channel.setAnim("Stealth", 0.50f);
          channel.setLoopMode(LoopMode.DontLoop);
          channel.setSpeed(1f);
        }
      }
    
      public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
        // unused
      }
    
      /** Custom Keybinding: Map named actions to inputs. */
      private void initKeys() {
        inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(actionListener, "Walk");
      }
      private ActionListener actionListener = new ActionListener() {
        public void onAction(String name, boolean keyPressed, float tpf) {
          if (name.equals("Walk") && !keyPressed) {
            if (!channel.getAnimationName().equals("Walk")) {
              channel.setAnim("Walk", 0.50f);
              channel.setLoopMode(LoopMode.Loop);
            }
          }
        }
      };
    }
  • Profile picture of germanosk germanosk said 3 months, 3 weeks ago:

    I almost forgot thanks a lot @abies you’re amazing and your code too!