Input handler for strategy games

Original:

In my own project I do not make use of SimpleGame, but StandardGame which is a bit different from your examples. So I modified the example code that comes with your library. The modification was easy but I still made a Wiki page for this also… I hope you do not mind. The setupTerrain method probably does not have to be executed entirely in OpenGL thread but I was just after a quick solution.



Bloody awsome code though. Loved if from the first 2 seconds :slight_smile: I would certainly like to see it as part of jME core, but as I am a noob I might not understand why it is not such a great idea…



Edit 1:

I also have a question concerning integration with FengGUI. The problem occurs with your software cursor in combination with FengGUI buttons. As you have discussed on the FengGUI forum in this thread , FengGUI keeps reseting the cursor to hardware… and StrategicHandler back… and the result is that I get lots and lots of warning messages :slight_smile: I tried setting the FengGUI CursorFactory to null in the binding, but there is no setter method. Since you seemed to be satisfied with their answers on the FengGUI forum, it appears that you might have a solution for this problem. At least that is what I am hoping…

Integration with FengGUI works with software mouse out of the box. I'm sure with some more effort and maybe a light patch to FengGUI it would be possible to make it work with the hardware mouse as well.



Here is a working example with the software mouse. Make sure you never call MouseInput.get().setMouseVisible(true). Because that will activate hardware mouse in FengGUI.


import java.net.URISyntaxException;

import org.fenggui.ComboBox;
import org.fenggui.TextEditor;
import org.fenggui.composites.Window;
import org.fenggui.event.ISelectionChangedListener;
import org.fenggui.event.SelectionChangedEvent;
import org.fenggui.layout.StaticLayout;
import org.lex.input.integration.FengJmeInputHandler;
import org.lex.input.mouse.Cursor;
import org.lex.input.mouse.MouseManager;
import org.lex.input.mouse.component.SoftwareMouse;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GLContext;

import com.jme.app.BaseGame;
import com.jme.image.Texture;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.light.PointLight;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.shape.Box;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.system.JmeException;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jme.util.lwjgl.LWJGLTimer;
import com.jme.util.resource.ResourceLocatorTool;
import com.jme.util.resource.SimpleResourceLocator;
 
/**
 * FengJME - A test class for integrating FengGUI and jME.
 *
 * @author Josh (updated by neebie)
 * @author lex (Aleksey Nikiforov) some changes to make it work with mouseManger
 *
 */
public class FengJMEMouseManager extends BaseGame
{
   Camera cam; // Camera for jME
   Node rootNode; // The root node for the jME scene
   PointLight light; // Changeable light
   FengJmeInputHandler input;
   Timer timer;
 
   Box box; // A box
 
   org.fenggui.Display disp; // FengGUI's display

   MouseManager mouseManager;
   KeyBindingManager keyboard;
   
   private String cmdToggleNative = "toggleNative";
   private String cmdToggleCursor = "toggleCursor";
   private String cmdToggleVisible = "toggleVisible";
   
   private String defaultCursor = "default";
   private String spinningCursor = "spinning";
 
 
   /* (non-Javadoc)
    * @see com.jme.app.BaseGame#cleanup()
    */
   @Override
   protected void cleanup()
   {
      MouseInput.destroyIfInitalized();
      KeyInput.destroyIfInitalized();
   }
 
 
   /* (non-Javadoc)
    * @see com.jme.app.BaseGame#initGame()
    */
   @Override
   protected void initGame()
   {
      // Create our root node
      rootNode = new Node("rootNode");
      // Going to enable z-buffering
      ZBufferState buf = display.getRenderer().createZBufferState();
      buf.setEnabled(true);
      buf.setFunction(ZBufferState.CF_LEQUAL);
      // ... and set the z-buffer on our root node
      rootNode.setRenderState(buf);
 
      // Create a white light and enable it
      light = new PointLight();
      light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
      light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
      light.setLocation(new Vector3f(100, 100, 100));
      light.setEnabled(true);
      /** Attach the light to a lightState and the lightState to rootNode. */
      LightState lightState = display.getRenderer().createLightState();
      lightState.setEnabled(true);
      lightState.attach(light);
      rootNode.setRenderState(lightState);
 
      // Create our box
      box = new Box("The Box", new Vector3f(-1, -1, -1), new Vector3f(1, 1, 1));
      box.updateRenderState();
      // Rotate the box 25 degrees along the x and y axes.
      Quaternion rot = new Quaternion();
      rot.fromAngles(FastMath.DEG_TO_RAD * 25, FastMath.DEG_TO_RAD * 25, 0.0f);
      box.setLocalRotation(rot);
      
      try {
         ResourceLocatorTool.addResourceLocator(
               ResourceLocatorTool.TYPE_TEXTURE,
               new SimpleResourceLocator(
                     this.getClass().getClassLoader().getResource(
                           "jmetest/data/images/Monkey.jpg")));
      } catch (URISyntaxException e) {
         e.printStackTrace();
      }
      
      Texture tex = TextureManager.loadTexture(
            "Monkey.png",
            Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, 1.0f, true);
      float scale = 0.5f;
      tex.setScale(new Vector3f(scale, scale, scale));
      TextureState ts = display.getRenderer().createTextureState();
      ts.setTexture(tex);
      box.setRenderState(ts);
      
      // Attach the box to the root node
      rootNode.attachChild(box);
 
      // Update our root node
      rootNode.updateGeometricState(0.0f, true);
      rootNode.updateRenderState();
 
      // Create the GUI
      initGUI();
   }
 
 
   /**
    * Create our GUI.  FengGUI init code goes in here
    *
    */
   protected void initGUI()
   {
      // Grab a display using an LWJGL binding
      //      (obviously, since jME uses LWJGL)
      disp = new org.fenggui.Display(new org.fenggui.render.lwjgl.LWJGLBinding());
 
      input = new FengJmeInputHandler(disp);
      
      // *** Setup for mouseManager with FengGUI *****************************
      mouseManager = new MouseManager(
            new SoftwareMouse("Mouse", display.getRenderer()));
      mouseManager.registerWithInputHandler(input);
      mouseManager.addListener(input.getMouseListener());
      rootNode.attachChild(mouseManager.getMouse().getMouseSpatial());
      mouseManager.setCursor(spinningCursor, Cursor.load(
            this.getClass().getClassLoader().getResource(
                  "org/lex/test/goldenarrow.car"),
            "spinning.cursor"));
      mouseManager.useCursor(defaultCursor);
      // *** End Setup *******************************************************
 
      //    Create a dialog and set it to some location on the screen
      Window frame = new Window();
      disp.addWidget(frame);
      frame.setX(20);
      frame.setY(350);
      frame.setSize(200, 100);
      frame.setShrinkable(false);
      //frame.setExpandable(true);
      frame.setTitle("Pick a color");
      frame.getContentContainer().setLayoutManager(new StaticLayout());
 
      // Create a combobox with some random values in it
      //   we'll change these values to something more useful later on.
      ComboBox<String> list = new ComboBox<String>();
      frame.addWidget(list);
      list.setSize(150, list.getMinHeight());
      list.setShrinkable(false);
      list.setX(25);
      list.setY(25);
      list.addItem("White");
      list.addItem("Green");
      list.addItem("Blue");
      list.addItem("Red");
 
      list.addSelectionChangedListener(new CBListener());
 
      //try to add TextArea here but get OpenGLException
      TextEditor ta = new TextEditor(false);
      disp.addWidget(ta);
      ta.setText("Hallo Text");
      ta.setX(40);
      ta.setY(50);
      //ta.setSize(100, ta.getAppearance().getFont().get)
      ta.setSizeToMinSize();
 
      // Update the display with the newly added components
      disp.layout();
   }
 
 
   /* (non-Javadoc)
    * @see com.jme.app.BaseGame#initSystem()
    */
   @Override
   protected void initSystem()
   {
      try
      {
         // Initialize our jME display system
         display = DisplaySystem.getDisplaySystem(properties.getRenderer());
         display.createWindow(properties.getWidth(), properties.getHeight(), properties.getDepth(), properties
               .getFreq(), properties.getFullscreen());
 
         // Get a camera based on the window settings
         cam = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
      }
      catch (JmeException ex)
      {
         ex.printStackTrace();
         System.exit(1);
      }
 
      /** Set a black background.*/
      display.getRenderer().setBackgroundColor(ColorRGBA.black);
      /** Set up how our camera sees. */
      cam.setFrustumPerspective(45.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);
      Vector3f loc = new Vector3f(0.0f, 0.0f, 15.0f);
      Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
      Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
      Vector3f dir = new Vector3f(0.0f, 0f, -1.0f);
      /** Move our camera to a correct place and orientation. */
      cam.setFrame(loc, left, up, dir);
      /** Signal that we've changed our camera's location/frustum. */
      cam.update();
      /** Assign the camera to this renderer.*/
      display.getRenderer().setCamera(cam);
      
      // Bind the Escape key to kill our test app
      KeyBindingManager.getKeyBindingManager().set("quit", KeyInput.KEY_ESCAPE);
      
      keyboard = KeyBindingManager.getKeyBindingManager();
      keyboard.set(cmdToggleNative, KeyInput.KEY_H);
      keyboard.set(cmdToggleCursor, KeyInput.KEY_C);
      keyboard.set(cmdToggleVisible, KeyInput.KEY_V);
 
      // Create our timer
      timer = new LWJGLTimer();
   }
 
 
   @Override
   protected void reinit()
   {
   }
 
 
   @Override
   protected void render(float interpolation)
   {
      display.getRenderer().clearBuffers();
      display.getRenderer().draw(rootNode);
      
      GL11.glMatrixMode(GL11.GL_TEXTURE);
      GL11.glLoadIdentity();
      if (GLContext.getCapabilities().GL_ARB_shader_objects) {
         GL20.glUseProgram(0);
      }
      
      disp.layout();
      disp.display();
   }
 
 
   /* (non-Javadoc)
    * @see com.jme.app.BaseGame#update(float)
    */
   @Override
   protected void update(float interpolation)
   {
      timer.update();
      float tpf = timer.getTimePerFrame();
      input.update(tpf);
      if (!input.wasKeyHandled())
      {
         // Check to see if Escape was pressed
         if (KeyBindingManager.getKeyBindingManager().isValidCommand("quit")) finish();
         
         if (keyboard.isValidCommand(cmdToggleNative, false)) {
            mouseManager.setNativeMousePreferred(
                  !mouseManager.isNativeMousePreferred());
         } else if (keyboard.isValidCommand(cmdToggleCursor, false)) {
            if (defaultCursor.equals(mouseManager.getCurrentCursor())) {
               mouseManager.useCursor(spinningCursor);
            } else {
               mouseManager.useCursor(defaultCursor);
            }
         } else if (keyboard.isValidCommand(cmdToggleVisible, false)) {
            mouseManager.getMouse().setCursorVisible(
                  !mouseManager.getMouse().isCursorVisible());
         }
      }
      
   }
 
 
   /**
    * @param args
    */
   public static void main(String[] args)
   {
      FengJMEMouseManager app = new FengJMEMouseManager();
      app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }
 
   private class CBListener implements ISelectionChangedListener
   {
      public void selectionChanged(SelectionChangedEvent selectionChangedEvent)
      {
         if (!selectionChangedEvent.isSelected()) return;
         String value = selectionChangedEvent.getToggableWidget().getText();
         if ("White".equals(value)) light.setDiffuse(ColorRGBA.white);
         if ("Red".equals(value)) light.setDiffuse(ColorRGBA.red);
         if ("Blue".equals(value)) light.setDiffuse(ColorRGBA.blue);
         if ("Green".equals(value)) light.setDiffuse(ColorRGBA.green);
      }
 
   }
   
}

Judging by the error log, looks like the problem occurs because of locking the terrain node. Locking the mesh creates display lists for that mesh. Display lists are faster to render, but some video cards (especially older or integrated video cards) might not support display lists.



Try disabling terrain node locking by commenting out the lines:


       terrainNode.lock();
       terrainNode.lockBranch();



Alternatively you can lock a model in one of the jme test by calling node.lock() and see if that causes the same error.

That fixed the problem.  :)  Put the ATI x1800XT on the list of cards that don't like that operation!



Thanks lex!

Hmm, x1800XT should handle it fine.  sounds like a driver issue to me.

Yeah, I would think so too…the card isn't exactly old, hehe.  I had the 8.3 catalyst drivers and went back to 8.2 and 8.1 with no difference.  Weird.  :confused:

Out of curiosity, is your x1800 integrated into the MoBo?  Perhaps with shared memory?





Also, is this being done before the OpenGL thread starts?  If not, is it being done IN the OpenGL thread?

The card occupies my PCI-e x16 slot on my EPoX 9npa+ ultra motherboard.  The code is being called from simpleInitGame in a class that extends SimpleGame.

Great, that setup should rock alrighty.



but


Also, is this being done before the OpenGL thread starts?  If not, is it being done IN the OpenGL thread?

I was able to reapply the input handler code after using a OpenGL queue I wrote to execute OpenGL actions in its thread.  And yes, basixs, at that point the locks were being performed outside of the thread after it had been started.  Again, I thank you for your help.  :slight_smile:

No problemo :slight_smile:

Mindgamer said:

Original:
In my own project I do not make use of SimpleGame, but StandardGame which is a bit different from your examples. So I modified the example code that comes with your library. The modification was easy but I still made a Wiki page for this also...


Begging your pardon, but I believe you forgot to set the lightState in the example on the wiki.  Shouldn't this be added to the bottom of setupTerrain()?


      terrainNode.setRenderState(lightState);
      terrainNode.updateRenderState();

cbratton said:

Mindgamer said:

Original:
In my own project I do not make use of SimpleGame, but StandardGame which is a bit different from your examples. So I modified the example code that comes with your library. The modification was easy but I still made a Wiki page for this also...


Begging your pardon, but I believe you forgot to set the lightState in the example on the wiki.  Shouldn't this be added to the bottom of setupTerrain()?


      terrainNode.setRenderState(lightState);
      terrainNode.updateRenderState();




Code modified according to this thread.

I have a question for lex. I have not tried it and would not know how to do it but - would strategic handler work over  the surface of a sphere? Or if it does not, do you think it can be implemented fairly easily?



I think so far everywhere the world orientation has been done with an 'up' vector. I guess it is reasonable to assume that same can be achieved with a down vector - and re-calculating that vector every frame, so that it is directed at a certain point - center of the sphere?



Sphere = planet in my mind :slight_smile:

Right now the view scrolling is locked to a plane with height offset, depending on the terrain. You can rewrite the plane class to map a sphere.

I have run into a slight problem. I am not sure if I am using the handler incorrectly or I am misunderstanding some key 3D concept. Basically I needed to change the orientation of the world. This meant overwriting the RotationSphere and ScrollPlane values for the scrolling to work correctly. I got this to work as I wanted.



However I also wanted to increase the VERTICAL_ROTATION_MAX value beyond 89.9f. This is where I ran into problems. In your original SimpleSetup demo with default values, the VERTICAL_ROTATION_MAX works correctly. However with my own world coordinates, whenever my vertical rotation reaches 90 degrees, my objects disappear from view. I copied my RotationSphere and ScrollPlane code into the SimpleSetup.setupHandler() method and it seemed to me that it had the same effect - stuff disappeared. When strategicHandler.getCamera().getDirection() reached 0.0 : 0.0 : -1.0 , all went blank.



To test it I added to init:

      Quad quad;
       quad = new Quad("quad", 100, 100);
       quad.setLocalTranslation(new Vector3f(0, 0, 0));
        quad.setModelBound(new BoundingBox());
        quad.updateModelBound();
        rootNode.attachChild(quad);



And to setupHandler right before 'input = strategicHandler;' I put:

final Vector3f DEFAULT_SCROLL_XVEC = Vector3f.UNIT_X;
final Vector3f DEFAULT_SCROLL_YVEC = Vector3f.UNIT_Y;
final Vector3f DEFAULT_WORLD_UP = Vector3f.UNIT_Z;
final Vector3f DEFAULT_DIRECTION = Vector3f.UNIT_Z;
final Vector3f DEFAULT_WORLD_LEFT = Vector3f.UNIT_X.negate();
final float DEFAULT_VERTICAL_ROTATION_MAX = 100f;
      
strategicHandler.getRotationSphere().setVectors(
   DEFAULT_DIRECTION,
   DEFAULT_WORLD_UP,
   DEFAULT_WORLD_LEFT);
      
strategicHandler.getScrollingPlane().setPlane(
   DEFAULT_SCROLL_XVEC,
   DEFAULT_SCROLL_YVEC,
   DEFAULT_WORLD_UP);
      
strategicHandler.getRotationSphere().getVertical().setMax(DEFAULT_VERTICAL_ROTATION_MAX);



Trying the same changes in your Demo application, stuff did not disappear. I looked at the code and added to my code:

camEffects = new StrategicCameraEffects(strategicHandler);
strategicHandler.setCameraEffects(camEffects);



After this change, my objects no longer disappeared, but the angle still never went over 90 degrees. While I am content with this solution (as 90 degrees was exactly what I wanted to achieve), I was wondering what the reason for this is and if it is intentional.

Edit: On second look it appears that the camera Effects modify the way the rotation works (Del and PgDn keys) - the scene is no longer rotated. So this solution still does not help me. Perhaps you can offer me some further insight?

Kind regards

Hmm, this may actually be an issue with OpenGL itself.  I remember that when I was creating my own pure OpenGL camera class I had to handle vertical rotations that went past 'straight-up' and 'straight-down', basically because of the  camera up vector there is a math issue.  What I did to work around it was to detect when the camera went past (or right at) the completely vertical point I would reverse the camera's up vector.



To debugg if this is what you are fighting maybe just print out all your camera angles, right when the change happens one or more of your vectors should suddenly switch, also if you don't have surrounding world stuff to maintain your bearings put a HUGE colored box in. 

Well, this behavior is one of the reason the vertical angles are locked at 89.9. Probably should be documented somewhere as well :slight_smile:



There are several problems with vertical angles at ±90 degrees:

  1. The scrolling direction is determined as a project of camera.direction onto the scrolling plane when scrolling up and a projection of camera.left onto the scrolling plane when scrolling left. When the camera hits 90 vertical angle, then camera direction is parallel to the word up vector, and orthogonal to the scrolling plane. So the projection of camera.direction onto the scrolling plane is zero. However due to round off errors you will get a non-zero vector and it's direction is unpredictable.
  2. Because WorldUp vector is the same, when you pass the 90 degree mark, the view is "rotated" 180 degress in horizontal axis (Imagine looking down at the clock that lie on the floor and then walking from 6 o'clock mark to 12 o'clock). Due to round off errors, this causes view to randomly flip many times a second, producing a rather confusing effect.
  3. When WorldUp is equal to Camera.direction, code that does camera orientation based on worldUp produces a zero vector which results in a black screen.



    The way the handler is written is under the assumption that WorldUp is consistent and your camera will not be hanging upside-down. To adapt the handler to you application you could:

    a) invert WorldUp, whenever you pass 90 degree mark to allow camera to hang upside-down (does not fix the black screen).

    b) jump from 89.9 to 90.1 avoiding the camera direction that is parallel to the worldUp vector. (You can also try 89.99 and 90.01 or even more precision, so that there is no noticeable difference to the user).



    a quick fix for -90 to 90 range is to replace


   public boolean rotateVertical(float degrees) {
      float newVal = vercitcal.getCurrent();
      return vertical.addToCurrent(degrees);
   }



with


   public boolean rotateVertical(float degrees) {
      boolean noLimit = vertical.addToCurrent(degrees);
      float newVal = vertical.getCurrent();
      
      float sv = Math.signum(newVal);
      newVal *= sv;
      
      if (newVal > POLE_MIN && newVal < POLE_MAX) {//POLE_MIN=89.99, POLE_MAX=90.01
         vertical.setCurrent(sv*POLE_MIN);
      }
      
      return noLimit;
   }



Some problems with this approach:
1) small rotation under [POLE_MAX - POLE_MIN] will be ingored, so when moving very slowly, the rotation will stop at -90 and 90
2) does not work for other ranges, such as 90 to 270 and -270 to -90.

It's possible to extend MinMaxCurrent class to automatically handle discontinuities with a give period. For example a discontinuity at 90 degrees with a repeating period of 180 degrees.

Actually the best solution would be to take full control of camera.direction, camera.up and camera.left instead of relying on auto-leveling code.



I will probably refactor the handler to fully control the camera when porting it to jme2.

OK, I think I understand. However I still do not get why the DEFAULT_VERTICAL_ROTATION_MAX does not affect the handler at all, when I set the world coordinates as described above. Although the max is set to 89.9, when in the new world coordinate system, everything is still rotated all the way to 90 and blanked out. All I have to do for this is to change the default world vectors in StrategicHandler.



At this point I would be quite happy with limiting my vertical rotation to 89.99 or whatever - as long as I could keep my world orientation.



Maybe I was not just sharp enough to get this from the previous posts, but do you have any advice on how I could get this working?