Mouse drag picking--mass selection--How to implement?

Hello all,



  This isn’t a JME problem per se, but I thought I’d try to get anyones input.  I’m writing an RTS using JME, and am at the unit selection section.  I can already slect a single unit no problem.  I’d like to be able to drag a window and select any units under that window, like you can in most RTS games.  I grab the real world coordinates of the mouse click and unlick.

  The kicker is, you can rotate your camera, so I can’t do something simple like I was thinking originally, simply cycling through the units and seeing if they’re coordinates are bounded by the two points. (see screenshots)











In these two scenerios the two real-world coordinates I grab are similiar, but the bounding window is different.





I’ve considered implementing a “shotgun” ray test in the window, selecting with 10x10 rays or whatever, but not only is this sloppy and inefficient, it would still miss units when zoomed out. 



Do you guys have any ideas how this can be or has been done?



Brenden

Perhaps you could do some king of bounding volume based on the selection and do bounding collision checks?

it looks as though you are creating your picking area based on the screen and not the terrain.



Have you tried doing it on the terrain

  1. Where the user clicks, cast a ray to get the start of the selection (xyz) on the actual heightmap.
  2. Get the area your intersted in when the button is released
  3. You now just have to test for units in the node of the heightmap. This can be done using instanceof

either build something similar to the camera frustum, using the 4 points a z depth of "zero" and 4 points at depth "farplane" and then test your objects boundingboxes with that…

or, test the objects screencoords(3d->2d) against your 2d recangle…or did you say that didnt work?

Thanks for stopping by and thinking about my problem :slight_smile:



I saw renanses' solution first and it made the most sense to me.  Yes kidneybean I was grabing the "real world" coordinates from the terrain, not the ortho screen. 



Using the four points of the dragged window, I generated a trimesh and extrude it 200 units high, to insure we don't miss any units on a hill.  I then use hasCollision to detect if the unit is inside the generated polygon.  I've had mixed results.  I've run into two seperate issues:


  1. If I use bounding detection, I run into an issue where a thin strip of selection gets a huge bounding box if done diagonally:



    I freehand sketched what I think JME is setting the bounding box to in black.


  2. I would prefer to use trianglecollision detection.  When I do, and my units intersect with the trimesh, all is well.  However, if they are totally enclosed in it, as in this screenshot, they are missed.







    Do you guys have any suggestions on either how to set the correct bounding box, or select units who, while missing the triangles of the target polygon, are enclosed in it?



    Brenden

Ok so the solution I came up with is to shoot a ray up from each unit and check to see if it colides with the trimesh.  Sounded good, but I’m getting strange problems.





while it does detect collision under most circumstonaces, not all.  And the collision points it is finding are way off.  They’re always on the face in this picture.  To help myself, I drew a line from the unit location to the target location.  I expected them to always be on the top face.



Can anyone help me with this part?  I’m strugling to understand if my trimesh is mis-oriented, or what.  I know the vertices need to be in a certian order for the normals to work, but that shouldn’t matter for collision detection, right?  Do the faces need to be added in any kind of order?  Heres the trimesh creation code:



Vector3f[] vertexes = { new Vector3f(draggingx, 0, draggingy), new Vector3f(thirdcornerx, 0, thirdcornery), new Vector3f(fourthcornerx, 0, fourthcornery), new Vector3f(x, 0, y),new Vector3f(draggingx, 250, draggingy), new Vector3f(thirdcornerx, 250, thirdcornery), new Vector3f(fourthcornerx, 250, fourthcornery), new Vector3f(x, 250, y)};
         int[] indexes = {
                      0,1,2,1,3,2,   //bottom
                     1,3,7,1,7,5,   //front
                     4,5,6,5,7,6,   //top
                     0,1,5,0,5,4,   //left
                     2,0,4,2,4,6,   //back
                     3,2,6,3,6,7      //right
                     };
         selectionCube.reconstruct(BufferUtils.createFloatBuffer(vertexes), null, null,
         null,BufferUtils.createIntBuffer(indexes));



I realize those vertex variable names might be cryptic, but basicly, I'm trying to form it on this pattern:



Oh, and heres the ray generation code:


 tris = new ArrayList<Integer>();
               Ray testRay = new Ray(new Vector3f(units[loop].x,50,units[loop].y),new Vector3f(units[loop].x,300,units[loop].y));
               selectionCube.findTrianglePick(testRay, tris,0);
               if (tris.size()>0)
               {
                  selectionCube.getTriangle( (tris.get(0)), indices);
                  selectionCube.getTriangle(tris.get(0),verticies);
                  Vector3f target = new Vector3f();
                  testRay.intersectWhere(verticies[0], verticies[1], verticies[2], target);

sounds pretty complex…why didnt it work to just loop through the units and do:



cam.getScreenCoordinates( unit.getLocalTranslation(), screenPos );

if ( screenPos.x > selectionMinX && screenPos.x < selectionMaxX && screenPos.y > selectionMinY && screenPos.y < selectionMaxY ) {

  unit.setSelected(true);

}



???

yes…



i working on the same problem, and share my reason soon, if i found a easy way to implement it. :wink:

beamanbr said:

1) If I use bounding detection, I run into an issue where a thin strip of selection gets a huge bounding box if done diagonally:
...
I freehand sketched what I think JME is setting the bounding box to in black.


Maybe you should use an OrientedBoundingBox instead of the normal one. You can then do triangle intersection..

though even a oriented boundingbox will either miss units far away or select to many up close…



(as i have said i would either do a 3d->2d test or do 6 planetests just like the camera frustum culling code)

You might ask the oddlabs guys what they did in the case of tribal trouble. they reply for technical questions in their forums. just to get a hint.

I think this may have been said in another way, but, if you are just doing items on a terrain, knowing the 4 corners of intersection with the terrain would give you minX,maxX and minZ,maxZ (height, or Y should be irrelevant) and you could simply test your pieces world translation to see if they fall inside those min and max sets.

renanse said:

I think this may have been said in another way, but, if you are just doing items on a terrain, knowing the 4 corners of intersection with the terrain would give you minX,maxX and minZ,maxZ (height, or Y should be irrelevant) and you could simply test your pieces world translation to see if they fall inside those min and max sets.



that can give wrong selections if the terrain is hilly...

I didn’t know it was so simple to convert 3d->2d.  Thats only 100x simpler, so I’ll have to think about it  :roll:





Seriously, that works flawlessly and cuts about 100 lines of dense calculation code out of my program.  Thanks for the wake-up call Mr.Coder.



Take a looksee if you’d like to see the current state:



http://www.bdogstudios.kattare.com/webstart/Lesson8.jnlp

(WASD strafes camera, mouse roll zooms, hold mouse roller button for mouse-look.  +/- alters day cycle speed)





Brenden

no prob! did a quick test doing like that and i had no problems…





Well, cool, can you share the code here for others then?  :slight_smile:

Oh sure, but the tricky part Mr.Coder already described in a previous post.  Heres my implementation:



for (int loop=0;loop<numunits;loop++)
{
   if (units[loop].node!=null)  //unit is actually created
   {
      Vector3f screenPos2=new Vector3f(0,0,0);
      x = units[loop].x;
      y = units[loop].z;
          //get top left and bottom right of the selection box based on first mouse click, (orthoMouseDragx/y) and current hotspot position
          //this code seems redundant, but insures the coordinates are equivilent no matter which direction you drag from
      float selectionBoxX1 = FastMath.abs(am.getHotSpotPosition().x+orthoMouseDragx)/2-FastMath.abs(orthoMouseDragx-am.getHotSpotPosition().x)/2;
      float selectionBoxY1 = FastMath.abs(am.getHotSpotPosition().y+orthoMouseDragy)/2-FastMath.abs(orthoMouseDragy-am.getHotSpotPosition().y)/2;
      float selectionBoxX2 = FastMath.abs(am.getHotSpotPosition().x+orthoMouseDragx)/2+FastMath.abs(orthoMouseDragx-am.getHotSpotPosition().x)/2;
      float selectionBoxY2 = FastMath.abs(am.getHotSpotPosition().y+orthoMouseDragy)/2+FastMath.abs(orthoMouseDragy-am.getHotSpotPosition().y)/2;

          // Mr.Coders code to grab the 2D coordinates of unit on the screen.  The tb.getheight()+15 sets the y (height) coordinate to aproxamately center of the mesh
      cam.getScreenCoordinates(new Vector3f(x,tb.getHeight(x,y)+15,y),screenPos2);
      if ((screenPos2.x>selectionBoxX1)&&(screenPos2.x<selectionBoxX2)&&(screenPos2.y>selectionBoxY1)&&(screenPos2.y<selectionBoxY2))
         selectUnit(loop,true);
   }
}

Ok, i'm finished my implementation


import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;

import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.MouseInput;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

public class ObjectSelectionManager {
   
   private HashSet<ObjectPickListener> listeners = new HashSet<ObjectPickListener>();
   
   /* Multiselection */
   private int               mouseButton = 0;
   private boolean           drag = false;
   private Vector2f         mouseFirstDownPos;
   private Vector2f          screenPos = null;
   private ArrayList<Node>  selectableObjects;
   private ArrayList<Node>  selection;
   
   private String           displayTex = "textures/unitselection.png";
   private DisplaySystem     display;
   
   /**
    * @param mouseButton mousebutton for action
    */
   public ObjectSelectionManager(int mouseButton) {
      this.mouseButton = mouseButton;
      
      selectableObjects = new ArrayList<Node>();
      selection = new ArrayList<Node>();
      display = DisplaySystem.getDisplaySystem();
   }
   
   private Spatial createSelectionPointer() {
      
      Quad selectionIndikator = new Quad("ID_INDIKATOR_DISPLAY",1f,1f);
      selectionIndikator.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
      selectionIndikator.setLocalTranslation(new Vector3f(0f,0f,0f));
      selectionIndikator.lookAt(selectionIndikator.getWorldTranslation(), new Vector3f(0f,0f,1f) );
      selectionIndikator.setLocalRotation(new Quaternion(new float[]{-FastMath.DEG_TO_RAD*90,0f,0f}));
      
      TextureState ts = display.getRenderer().createTextureState();
      ts.setTexture(TextureManager.loadTexture(getClass().getClassLoader()
            .getResource(displayTex), Texture.MM_LINEAR,Texture.FM_LINEAR,0.0f, true));
      ts.setEnabled(true);
      selectionIndikator.setRenderState(ts);
      
      AlphaState as = display.getRenderer().createAlphaState();
      as.setBlendEnabled(true);
      as.setSrcFunction(AlphaState.SB_SRC_ALPHA);
      as.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
      as.setTestEnabled(true);
      as.setTestFunction(AlphaState.TF_GREATER);
      selectionIndikator.setRenderState(as);
      
      return selectionIndikator;
   }
   
   public void add(Node spatial) {
      
      // add to selectable objects id posible
      selectableObjects.add(spatial);
   }
   
   public void remove(Node spatial) {
      
      Spatial i = spatial.getChild("ID_INDIKATOR_DISPLAY");
      if(i!=null) {
         spatial.detachChild(i);
      } else {
         System.out.println("ID not found");
      }
      
      // remove from selectable
      selectableObjects.remove(spatial);
      
   }
   
   public ArrayList<Node> findInRange(Vector2f fromScreenPoint, Vector2f toScreenPoint) {
      
      if(fromScreenPoint.equals(toScreenPoint)) {
         fromScreenPoint.x += 50;
         fromScreenPoint.y -= 50;
         toScreenPoint.x   -= 50;
         toScreenPoint.y   += 50;
        }
      
      Camera cam = DisplaySystem.getDisplaySystem().getRenderer().getCamera();
      ArrayList<Node> ret = new ArrayList<Node>();
      
      Vector3f screenPos=new Vector3f(0,0,0);
      float x,y,h;
      
      float x1 = fromScreenPoint.x;
      float y1 = fromScreenPoint.y;
      float x2 = toScreenPoint.x;
      float y2 = toScreenPoint.y;
      
      /* check for any direction of range*/
      if (x1 > x2) {
         x2 = fromScreenPoint.x;
         x1 = toScreenPoint.x;
      }
      if (y1 < y2) {
         y2 = fromScreenPoint.y;
         y1 = toScreenPoint.y;
      }
      
      for (Iterator<Node> iter = selectableObjects.iterator(); iter.hasNext();) {
         Node s = iter.next();
         x = s.getWorldTranslation().x;
         y = s.getWorldTranslation().z;
         h = s.getWorldTranslation().y;
         
         cam.getScreenCoordinates(new Vector3f(x,h,y),screenPos);
         
         if ((screenPos.x>x1)&&(screenPos.x<x2)&&(screenPos.y>y2)&&(screenPos.y<y1)) {
            ret.add(s);
         }
      }
      
      return ret;
   }
   
   public ArrayList<Node> getSelection() {
      return selection;
   }
   
   public void clearSelection() {
      
      for (Iterator<Node> iter = selection.iterator(); iter.hasNext();) {
         removeIndikator(iter.next());
      }
      
      selection.clear();
   }
   
   public void removeIndikator(Node unit) {
      Spatial i = unit.getChild("ID_INDIKATOR_DISPLAY");
      if(i!=null) {
         unit.detachChild(i);
      }
   }
   
   private void setSelection(ArrayList<Node> selection) {
      
      clearSelection();
      
      float h = 1f;
      
      for (Iterator<Node> iter = selection.iterator(); iter.hasNext();) {
         
         h += 0.025f;
         Node s = iter.next();
         Spatial ss = createSelectionPointer();
         
         ss.setLocalTranslation(new Vector3f(0f,h,0f));
         ss.setLocalScale(new Vector3f(((BoundingBox)s.getWorldBound()).xExtent+3,((BoundingBox)s.getWorldBound()).xExtent+3,1f));
         s.attachChild( ss );
         
         s.updateRenderState();
         
      }
      
      this.selection = selection;
   }
   
   @SuppressWarnings("unchecked")
   public void addListener(ObjectPickListener listener) {
      this.listeners.add(listener);
   }
   
   public void removeListener(ObjectPickListener listener) {
      this.listeners.remove(listener);
   }
   
   public void update(float tpf) {
      
      /* left mouse button klicked */
      if ( MouseInput.get().isButtonDown(this.mouseButton) ) {
         screenPos = new Vector2f();
         screenPos.set(MouseInput.get().getXAbsolute(), MouseInput.get().getYAbsolute());
         
         /* hold mouse down start position */
         if (!drag) {
            mouseFirstDownPos = screenPos;
            drag = true;
         }
         
      } else {
         /* obmit the last dragposition and reset dragging */
         if (drag) {
            drag = false;
            setSelection(findInRange(mouseFirstDownPos, screenPos));
            fireRangeSelectEvent(mouseFirstDownPos, screenPos);
            mouseFirstDownPos = null;
         }
         
      }
      
      if(drag) {
         fireRangeSelectEvent(mouseFirstDownPos, screenPos);
      }
      
   }
   
   protected void fireRangeSelectEvent(Vector2f start, Vector2f end) {
      for (Iterator<ObjectPickListener> iter = listeners.iterator(); iter.hasNext();) {
         if(drag) {
            iter.next().onSelect( new SelectionEvent(start, end, drag, null));
         }else {
            iter.next().onSelect( new SelectionEvent(start, end, drag, getSelection()));
         }
      }
   }
   
}



the listener

public interface ObjectPickListener {
    void onSelect(SelectionEvent event);
}



the event-class

import java.util.ArrayList;

import com.jme.math.Vector2f;
import com.jme.scene.Node;

public class SelectionEvent {

   ArrayList<Node> selectedObjects;
   Vector2f startsFrom;
   Vector2f endAt;
   boolean  drag;
   
   public SelectionEvent(Vector2f startsFrom, Vector2f endAt, boolean isDrag, ArrayList<Node> selectedObjects) {
      super();
      this.startsFrom = startsFrom;
      this.endAt      = endAt;
      this.drag       = isDrag;
      this.selectedObjects = selectedObjects;
   }

   public Vector2f getEndAt() {
      return endAt;
   }

   public Vector2f getStartsFrom() {
      return startsFrom;
   }

   public boolean isDrag() {
      return drag;
   }

   public ArrayList<Node> getSelectedObjects() {
      return selectedObjects;
   }

}



simple usage in a simplegame



private ObjectSelectionManager objectSelecter;

...

protected void simpleInitGame() {

...
     objectSelecter = new ObjectSelectionManager( 0 ); //left mousebutton for select-action
      {
         objectSelecter.addListener(this);
      }

      objectSelecter.add( ... object for selection ... );
      objectSelecter.add( ... object for selection ... );
      objectSelecter.add( ... object for selection ... );
      objectSelecter.add( ... object for selection ... );
....

...
}

protected void simpleUpdate() {
     if (objectSelecter!=null)
   objectSelecter.update(tpf);
}

/**from ObjectPickListener interface*/
public void onSelect(SelectionEvent event) {

      Vector2f start = event.getStartsFrom();
      Vector2f end = event.getEndAt();
      
      if(event.isDrag()) {
         
         if(selectionHud==null) {
            selectionHud = new SelectionQuad();
         }
         
         if(hudNode.getChildIndex(selectionHud)<0) {
            hudNode.attachChild(selectionHud);
         }
         
         selectionHud.setRange(start, end);
         
      } else {
         hudNode.detachChild(selectionHud);
      }
       
      if(event.getSelectedObjects()!=null) {
         // do something with selected objects
      }

   }



selectionHud is a quad 1*1 in ortho
hudNode is the node for the selectionHud. Used for add and remove the selectionHud-Quad.

selectionHud.setRange(start, end); translate/scale the quad

use differend instances of the manager to manage multiple selectionarrays...  :-o

good look ;)
http://jxg-tech.mi-studios.de/