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 @@ Engine.GameView_SetConstrainCameraEnabled(this.checked); - // Make selection more durable at the expense of speed if unchecked. - if (this.checked) - SELECTION_SEARCH_RADIUS -= 200; - else - SELECTION_SEARCH_RADIUS += 200; 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; - } }