diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index bace666b88..769e0f4947 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -32,11 +32,11 @@ const INPUT_MASSTRIBUTING = 10;
var inputState = INPUT_NORMAL;
+const INVALID_ENTITY = 0;
+
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
-// Distance to search for a selatable entity in. Bigger numbers are slower.
-var SELECTION_SEARCH_RADIUS = 100;
// Number of pixels the mouse can move before the action is considered a drag
var maxDragDelta = 4;
@@ -278,13 +278,13 @@ function determineAction(x, y, fromMinimap)
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return undefined;
- var targets = [];
var target = undefined;
if (!fromMinimap)
- targets = Engine.PickEntitiesAtPoint(x, y, SELECTION_SEARCH_RADIUS);
-
- if (targets.length)
- target = targets[0];
+ {
+ var ent = Engine.PickEntityAtPoint(x, y);
+ if (ent != INVALID_ENTITY)
+ target = ent;
+ }
// decide between the following ordered actions
// if two actions are possible, the first one is taken
@@ -853,15 +853,13 @@ function handleInputAfterGui(ev)
{
g_ShowAllStatusBars = (ev.type == "hotkeydown");
recalculateStatusBarDisplay();
- }
-
- if (ev.hotkey == "session.highlightguarding")
+ }
+ else if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
-
- if (ev.hotkey == "session.highlightguarded")
+ else if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeydown");
updateAdditionalHighlight();
@@ -876,9 +874,9 @@ function handleInputAfterGui(ev)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
- var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
- if (ents.length)
- g_Selection.setHighlightList([ents[0]]);
+ var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
+ if (ent != INVALID_ENTITY)
+ g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
@@ -930,9 +928,9 @@ function handleInputAfterGui(ev)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
- var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
- if (ents.length)
- g_Selection.setHighlightList([ents[0]]);
+ var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
+ if (ent != INVALID_ENTITY)
+ g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
@@ -980,15 +978,19 @@ function handleInputAfterGui(ev)
return false;
}
- var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
- g_Selection.setHighlightList(ents);
+ var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
+ if (ent != INVALID_ENTITY)
+ g_Selection.setHighlightList([ent]);
+ else
+ g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
- var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
- if (!ents.length)
+ var ents = [];
+ var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
+ if (selectedEntity == INVALID_ENTITY)
{
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
@@ -999,7 +1001,6 @@ function handleInputAfterGui(ev)
return true;
}
- var selectedEntity = ents[0];
var now = new Date();
// If camera following and we select different unit, stop
@@ -1052,7 +1053,7 @@ function handleInputAfterGui(ev)
prevClickedEntity = selectedEntity;
// We only want to include the first picked unit in the selection
- ents = [ents[0]];
+ ents = [selectedEntity];
}
// Update the list of selected units
diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml
index 48d2a0f0a7..b9d253316d 100644
--- a/binaries/data/mods/public/gui/session/session.xml
+++ b/binaries/data/mods/public/gui/session/session.xml
@@ -112,11 +112,6 @@
diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
index 6c623333f0..1f7f3091df 100644
--- a/source/gui/scripting/ScriptFunctions.cpp
+++ b/source/gui/scripting/ScriptFunctions.cpp
@@ -173,9 +173,9 @@ void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, CScriptVal cmd1)
cmpCommandQueue->PostNetworkCommand(CScriptVal(cmd2));
}
-std::vector PickEntitiesAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y, int range)
+entity_id_t PickEntityAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y)
{
- return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false, range);
+ return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false);
}
std::vector PickFriendlyEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player)
@@ -959,7 +959,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction("PostNetworkCommand");
// Entity picking
- scriptInterface.RegisterFunction, int, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
+ scriptInterface.RegisterFunction("PickEntityAtPoint");
scriptInterface.RegisterFunction, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
scriptInterface.RegisterFunction, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
diff --git a/source/maths/BoundingSphere.h b/source/maths/BoundingSphere.h
index 533b35031f..2d6705427a 100644
--- a/source/maths/BoundingSphere.h
+++ b/source/maths/BoundingSphere.h
@@ -55,6 +55,23 @@ public:
{
return m_Radius;
}
+
+ /**
+ * Check if the ray, defined by an origin point and a direction unit vector
+ * interesects with the sphere
+ */
+ bool RayIntersect(const CVector3D& origin, const CVector3D& dir) const
+ {
+ CVector3D v = m_Center - origin; // Vector v from the origin of the ray to the center of the sphere
+ float pcLen = dir.Dot(v); // Length of the projection of v onto the direction vector of the ray
+ if(pcLen <= 0)
+ return false; // Sphere behind the ray
+ // Get the shortest distance from the center of the sphere to the ray
+ v = (dir * pcLen) - v;
+ if (v.LengthSquared() > m_Radius * m_Radius)
+ return false; // Distance to sphere center more than radius
+ return true;
+ }
};
#endif // INCLUDED_BOUNDINGSPHERE
diff --git a/source/simulation2/components/CCmpUnitRenderer.cpp b/source/simulation2/components/CCmpUnitRenderer.cpp
index 5620145e3a..b948fdb99e 100644
--- a/source/simulation2/components/CCmpUnitRenderer.cpp
+++ b/source/simulation2/components/CCmpUnitRenderer.cpp
@@ -26,6 +26,7 @@
#include "ICmpRangeManager.h"
#include "ICmpSelectable.h"
#include "ICmpVisibility.h"
+#include "ICmpVisual.h"
#include "graphics/Frustum.h"
#include "graphics/ModelAbstract.h"
@@ -264,6 +265,63 @@ public:
{
m_EnableDebugOverlays = enabled;
}
+
+ virtual void PickAllEntitiesAtPoint(std::vector >& outEntities, const CVector3D& origin, const CVector3D& dir, bool allowEditorSelectables)
+ {
+ // First, make a rough test with the worst-case bounding boxes to pick all
+ // entities/models that could possibly be hit by the ray.
+ std::vector candidates;
+ for (size_t i = 0; i < m_Units.size(); ++i)
+ {
+ SUnit& unit = m_Units[i];
+ if (!unit.actor)
+ continue;
+ if (unit.sweptBounds.RayIntersect(origin, dir))
+ candidates.push_back(&unit);
+ }
+
+ // Now make a more precise test to get rid of the remaining false positives
+ float tmin, tmax;
+ CVector3D center;
+ for (size_t i = 0; i< candidates.size(); ++i)
+ {
+ const SUnit& unit = *candidates[i];
+
+ CmpPtr cmpVisual(unit.entity);
+ if (!cmpVisual)
+ continue;
+
+ CBoundingBoxOriented selectionBox = cmpVisual->GetSelectionBox();
+ if (selectionBox.IsEmpty())
+ {
+ if (!allowEditorSelectables)
+ continue;
+
+ // Fall back to using old AABB selection method for decals
+ // see: http://trac.wildfiregames.com/ticket/1032
+ // Decals are flat objects without a selectionShape defined,
+ // but they should still be selectable in the editor to move them
+ // around or delete them after they are placed.
+ // Check campaigns/labels/ in the Actors tab of atlas for examples.
+ CBoundingBoxAligned aABBox = cmpVisual->GetBounds();
+ if (aABBox.IsEmpty())
+ continue;
+
+ if (!aABBox.RayIntersect(origin, dir, tmin, tmax))
+ continue;
+
+ aABBox.GetCentre(center);
+ }
+ else
+ {
+ if (!selectionBox.RayIntersect(origin, dir, tmin, tmax))
+ continue;
+
+ center = selectionBox.m_Center;
+ }
+ outEntities.push_back(std::make_pair(unit.entity, center));
+ }
+ }
};
void CCmpUnitRenderer::TurnStart()
diff --git a/source/simulation2/components/ICmpUnitRenderer.h b/source/simulation2/components/ICmpUnitRenderer.h
index e560705e90..a1f44abfe0 100644
--- a/source/simulation2/components/ICmpUnitRenderer.h
+++ b/source/simulation2/components/ICmpUnitRenderer.h
@@ -53,6 +53,19 @@ public:
virtual void UpdateUnit(tag_t tag, CUnit* unit, const CBoundingSphere& boundsApprox) = 0;
virtual void UpdateUnitPos(tag_t tag, bool inWorld, const CVector3D& pos0, const CVector3D& pos1) = 0;
+
+ /**
+ * Return a list of visual entities along with their center point.
+ * Visual means they have an associated actor and a visual component,
+ * but they could still be hiden in the fog of war for a specific player,
+ * for example.
+ * NOTE: It's generally faster to do a lot of ray intersection tests than
+ * querying a lot of entities for component interfaces and doing these types
+ * of tests first.
+ */
+ virtual void PickAllEntitiesAtPoint(std::vector >& outEntities,
+ const CVector3D& origin, const CVector3D& dir,
+ bool allowEditorSelectables) = 0;
/**
* Returns the frame offset from the last Interpolate message.
diff --git a/source/simulation2/helpers/Selection.cpp b/source/simulation2/helpers/Selection.cpp
index f462c64070..fd24ae20d0 100644
--- a/source/simulation2/helpers/Selection.cpp
+++ b/source/simulation2/helpers/Selection.cpp
@@ -27,41 +27,54 @@
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpVisual.h"
+#include "simulation2/components/ICmpUnitRenderer.h"
#include "simulation2/helpers/Spatial.h"
#include "ps/CLogger.h"
#include "ps/Profiler2.h"
-std::vector EntitySelection::PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables, int range)
-{
- PROFILE2("PickEntitiesAtPoint");
+entity_id_t EntitySelection::PickEntityAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables)
+{
+ PROFILE2("PickEntityAtPoint");
CVector3D origin, dir;
camera.BuildCameraRay(screenX, screenY, origin, dir);
+
+ CmpPtr cmpUnitRenderer(simulation.GetSimContext().GetSystemEntity());
+ ENSURE(cmpUnitRenderer);
+ std::vector > entities;
+ cmpUnitRenderer->PickAllEntitiesAtPoint(entities, origin, dir, allowEditorSelectables);
+ if (entities.empty())
+ return INVALID_ENTITY;
+
+ // Filter for relevent entities in the list of candidates (all entities below the mouse)
+ std::vector > hits; // (dist^2, entity) pairs
+ for (size_t i = 0; i < entities.size(); ++i)
+ {
+ // Find the perpendicular distance from the object's centre to the picker ray
+ float dist2;
+ const CVector3D center = entities[i].second;
+ CVector3D closest = origin + dir * (center - origin).Dot(dir);
+ dist2 = (closest - center).LengthSquared();
+ hits.push_back(std::make_pair(dist2, entities[i].first));
+ }
+
+ // Sort hits by distance
+ struct SortFun {
+ bool operator() ( std::pair i, std::pair j) { return (i.first cmpRangeManager(simulation, SYSTEM_ENTITY);
ENSURE(cmpRangeManager);
-
- /* We try to approximate where the mouse is hovering by drawing a ray from
- * the center of the camera and through the mouse then taking the position
- * at which the ray intersects the terrain. */
- // TODO: Do this smarter without being slow.
- CVector3D pos3d = camera.GetWorldCoordinates(screenX, screenY, true);
- // Change the position to 2D by removing the terrain height.
- CFixedVector2D pos(fixed::FromFloat(pos3d.X), fixed::FromFloat(pos3d.Z));
-
- // Get a rough group of entities using our approximated origin.
- std::vector ents;
- cmpRangeManager->GetSubdivision()->GetNear(ents, pos, entity_pos_t::FromInt(range));
-
- // Filter for relevent entities and calculate precise distances.
- std::vector > hits; // (dist^2, entity) pairs
- for (size_t i = 0; i < ents.size(); ++i)
+
+ for (size_t i = 0; i < hits.size(); ++i)
{
- CmpPtr cmpSelectable(simulation, ents[i]);
+ const CEntityHandle& handle = hits[i].second;
+
+ CmpPtr cmpSelectable(handle);
if (!cmpSelectable)
continue;
- CEntityHandle handle = cmpSelectable->GetEntityHandle();
-
// Check if this entity is only selectable in Atlas
if (!allowEditorSelectables && cmpSelectable->IsEditorOnly())
continue;
@@ -69,56 +82,10 @@ std::vector EntitySelection::PickEntitiesAtPoint(CSimulation2& simu
// Ignore entities hidden by LOS (or otherwise hidden, e.g. when not IsInWorld)
if (cmpRangeManager->GetLosVisibility(handle, player) == ICmpRangeManager::VIS_HIDDEN)
continue;
-
- CmpPtr cmpVisual(handle);
- if (!cmpVisual)
- continue;
-
- CVector3D center;
- float tmin, tmax;
-
- CBoundingBoxOriented selectionBox = cmpVisual->GetSelectionBox();
- if (selectionBox.IsEmpty())
- {
- if (!allowEditorSelectables)
- continue;
-
- // Fall back to using old AABB selection method for decals
- // see: http://trac.wildfiregames.com/ticket/1032
- CBoundingBoxAligned aABBox = cmpVisual->GetBounds();
- if (aABBox.IsEmpty())
- continue;
-
- if (!aABBox.RayIntersect(origin, dir, tmin, tmax))
- continue;
-
- aABBox.GetCentre(center);
- }
- else
- {
- if (!selectionBox.RayIntersect(origin, dir, tmin, tmax))
- continue;
-
- center = selectionBox.m_Center;
- }
-
- // Find the perpendicular distance from the object's centre to the picker ray
- float dist2;
- CVector3D closest = origin + dir * (center - origin).Dot(dir);
- dist2 = (closest - center).LengthSquared();
-
- hits.push_back(std::make_pair(dist2, ents[i]));
+
+ return handle.GetId();
}
-
- // Sort hits by distance
- std::sort(hits.begin(), hits.end()); // lexicographic comparison
-
- // Extract the entity IDs
- std::vector hitEnts;
- hitEnts.reserve(hits.size());
- for (size_t i = 0; i < hits.size(); ++i)
- hitEnts.push_back(hits[i].second);
- return hitEnts;
+ return INVALID_ENTITY;
}
/**
diff --git a/source/simulation2/helpers/Selection.h b/source/simulation2/helpers/Selection.h
index 908727bb63..7cefddc391 100644
--- a/source/simulation2/helpers/Selection.h
+++ b/source/simulation2/helpers/Selection.h
@@ -47,7 +47,7 @@ namespace EntitySelection
*
* @return ordered list of selected entities with the closest first.
*/
-std::vector PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables, int range = 200);
+entity_id_t PickEntityAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables);
/**
* Finds all selectable entities within the given screen coordinate rectangle,
diff --git a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp
index f00ae61a00..e90b09d093 100644
--- a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp
+++ b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp
@@ -622,17 +622,16 @@ QUERYHANDLER(PickObject)
// Normally this function would be called with a player ID to check LOS,
// but in Atlas the entire map is revealed, so just pass INVALID_PLAYER
- std::vector ents = EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, INVALID_PLAYER, msg->selectActors);
+ entity_id_t ent = EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, INVALID_PLAYER, msg->selectActors);;
- // Multiple entities may have been picked, but they are sorted by distance,
- // so only take the first one
- if (!ents.empty())
+ if (ent == INVALID_ENTITY)
+ msg->id = INVALID_ENTITY;
+ else
{
- msg->id = ents[0];
-
+ msg->id = ent;
// Calculate offset of object from original mouse click position
// so it gets moved by that offset
- CmpPtr cmpPosition(*g_Game->GetSimulation2(), (entity_id_t)ents[0]);
+ CmpPtr cmpPosition(*g_Game->GetSimulation2(), ent);
if (!cmpPosition || !cmpPosition->IsInWorld())
{
// error
@@ -650,11 +649,6 @@ QUERYHANDLER(PickObject)
msg->offsety = (int)(cy - y);
}
}
- else
- {
- // No entity picked
- msg->id = INVALID_ENTITY;
- }
}