diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index 5a566aa817..197bf6ca19 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -36,6 +36,8 @@ var placementSupport = new PlacementSupport(); 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; @@ -495,7 +497,7 @@ function determineAction(x, y, fromMinimap) var targets = []; var target = undefined; if (!fromMinimap) - targets = Engine.PickEntitiesAtPoint(x, y); + targets = Engine.PickEntitiesAtPoint(x, y, SELECTION_SEARCH_RADIUS); if (targets.length) target = targets[0]; @@ -1108,7 +1110,7 @@ function handleInputAfterGui(ev) { case "mousemotion": // Highlight the first hovered entity (if any) - var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); + var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); if (ents.length) g_Selection.setHighlightList([ents[0]]); else @@ -1162,7 +1164,7 @@ function handleInputAfterGui(ev) { case "mousemotion": // Highlight the first hovered entity (if any) - var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); + var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); if (ents.length) g_Selection.setHighlightList([ents[0]]); else @@ -1212,14 +1214,14 @@ function handleInputAfterGui(ev) return false; } - var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); + var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { - var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); + var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS); if (!ents.length) { if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml index 48ba31fb31..f65afe3d7c 100644 --- a/binaries/data/mods/public/gui/session/session.xml +++ b/binaries/data/mods/public/gui/session/session.xml @@ -293,7 +293,14 @@ Restrict camera - Engine.GameView_SetConstrainCameraEnabled(this.checked); + + 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; + Reveal map diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp index b417e95c1a..6f60277a85 100644 --- a/source/gui/scripting/ScriptFunctions.cpp +++ b/source/gui/scripting/ScriptFunctions.cpp @@ -148,9 +148,9 @@ void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, CScriptVal cmd) cmpCommandQueue->PostNetworkCommand(cmd2); } -std::vector PickEntitiesAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y) +std::vector PickEntitiesAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y, int range) { - return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false); + return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false, range); } std::vector PickFriendlyEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player) @@ -804,7 +804,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface) scriptInterface.RegisterFunction("PostNetworkCommand"); // Entity picking - scriptInterface.RegisterFunction, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint"); + scriptInterface.RegisterFunction, int, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint"); 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/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp index 1daafcdfca..49590e814f 100644 --- a/source/simulation2/components/CCmpRangeManager.cpp +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -97,17 +97,17 @@ static u32 CalcSharedLosMask(std::vector players) * Checks whether v is in a parabolic range of (0,0,0) * The highest point of the paraboloid is (0,range/2,0) * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid - * + * * Avoids sqrting and overflowing. */ -static bool InParabolicRange(CFixedVector3D v, fixed range) +static bool InParabolicRange(CFixedVector3D v, fixed range) { i32 x = v.X.GetInternalValue(); // abs(x) <= 2^31 i32 z = v.Z.GetInternalValue(); u64 xx = (u64)FIXED_MUL_I64_I32_I32(x, x); // xx <= 2^62 u64 zz = (u64)FIXED_MUL_I64_I32_I32(z, z); i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) - + i32 y = v.Y.GetInternalValue(); i32 c = range.GetInternalValue(); i32 c_2 = c >> 1; @@ -327,7 +327,7 @@ public: // will get confused when trying to run from enemies m_LosRevealAll.resize(MAX_LOS_PLAYER_ID+2,false); m_SharedLosMasks.resize(MAX_LOS_PLAYER_ID+2,0); - + m_LosCircular = false; m_TerrainVerticesPerSide = 0; @@ -601,6 +601,11 @@ public: debug_warn(L"inconsistent subdivs"); } + SpatialSubdivision* GetSubdivision() + { + return & m_Subdivision; + } + // Reinitialise subdivisions and LOS data, based on entity data void ResetDerivedData(bool skipLosState) { @@ -844,7 +849,7 @@ public: if (!query.enabled) continue; - CmpPtr cmpSourcePosition(query.source); + CmpPtr cmpSourcePosition(query.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; @@ -858,9 +863,9 @@ public: removed.clear(); // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) - std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(), + std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(), std::back_inserter(added)); - std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(), + std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; @@ -927,7 +932,7 @@ public: } } // Not the entire world, so check a parabolic range, or a regular range - else if (q.parabolic) + else if (q.parabolic) { // elevationBonus is part of the 3D position, as the source is really that much heigher CmpPtr cmpSourcePosition(q.source); @@ -944,7 +949,7 @@ public: if (!TestEntityQuery(q, it->first, it->second)) continue; - + CmpPtr cmpSecondPosition(GetSimContext(), ents[i]); if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) continue; @@ -952,8 +957,8 @@ public: // Restrict based on precise distance if (!InParabolicRange( - CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - - pos3d, + CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) + - pos3d, q.maxRange)) continue; @@ -968,12 +973,12 @@ public: } } // check a regular range (i.e. not the entire world, and not parabolic) - else + else { // Get a quick list of entities that are potentially in range SpatialQueryArray ents; m_Subdivision.GetNear(ents, pos, q.maxRange); - + for (int i = 0; i < ents.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(ents[i]); @@ -1002,7 +1007,7 @@ public: virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) { entity_pos_t r = entity_pos_t::Zero() ; - + pos.Y += elevationBonus; entity_pos_t orientation = rot.Y; @@ -1022,26 +1027,26 @@ public: { r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; } - + return r; - + } - virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) + virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) { - + // angle = 0 goes in the positive Z direction entity_pos_t precision = entity_pos_t::FromInt((int)TERRAIN_TILE_SIZE)/8; std::vector r; - + CmpPtr cmpTerrain(GetSystemEntity()); CmpPtr cmpWaterManager(GetSystemEntity()); entity_pos_t waterLevel = cmpWaterManager->GetWaterLevel(pos.X,pos.Z); entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; - if (cmpTerrain) + if (cmpTerrain) { for (int i = 0; i < numberOfSteps; i++) { @@ -1064,7 +1069,7 @@ public: r.push_back(maxVector.Y); continue; } - + // Loop until vectors come close enough while ((maxVector - minVector).CompareLength(precision) > 0) { @@ -1083,26 +1088,26 @@ public: minVector = newVector; minDistance = newDistance; } - else + else { // new vector is out parabolic range, so this is a new maxVector maxVector = newVector; maxDistance = newDistance; } - + } r.push_back(maxVector.X); r.push_back(maxVector.Y); - + } r.push_back(r[0]); - r.push_back(r[1]); + r.push_back(r[1]); } return r; } - + Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask) @@ -1150,7 +1155,7 @@ public: return; static CColor disabledRingColour(1, 0, 0, 1); // red static CColor enabledRingColour(0, 1, 0, 1); // green - static CColor subdivColour(0, 0, 1, 1); // blue + static CColor subdivColour(0, 0, 1, 1); // blue static CColor rayColour(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) @@ -1173,26 +1178,26 @@ public: m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); } - else - { + else + { // elevation bonus is part of the 3D position. As if the unit is really that much higher - CFixedVector3D pos = cmpSourcePosition->GetPosition(); + CFixedVector3D pos = cmpSourcePosition->GetPosition(); pos.Y += q.elevationBonus; std::vector coords; - + // Get the outline from cache if possible if (ParabolicRangesOutlines.find(q.source.GetId()) != ParabolicRangesOutlines.end()) { EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source.GetId()]; - if (e.position == pos && e.range == q.maxRange) + if (e.position == pos && e.range == q.maxRange) { // outline is cached correctly, use it - coords = e.outline; + coords = e.outline; } else { - // outline was cached, but important parameters changed + // outline was cached, but important parameters changed // (position, elevation, range) // update it coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); @@ -1202,9 +1207,9 @@ public: ParabolicRangesOutlines[q.source.GetId()] = e; } } - else + else { - // outline wasn't cached (first time you enable the range overlay + // outline wasn't cached (first time you enable the range overlay // or you created a new entiy) // cache a new outline coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); @@ -1215,10 +1220,10 @@ public: e.outline = coords; ParabolicRangesOutlines[q.source.GetId()] = e; } - + CColor thiscolor = q.enabled ? enabledRingColour : disabledRingColour; - - // draw the outline (piece by piece) + + // draw the outline (piece by piece) for (size_t i = 3; i < coords.size(); i += 2) { std::vector c; @@ -1268,7 +1273,7 @@ public: { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = subdivColour; - + float xpos = x*divSize + divSize/2; float zpos = y*divSize + divSize/2; SimRender::ConstructSquareOnGround(GetSimContext(), xpos, zpos, divSize, divSize, 0.0f, diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h index ea41e147b4..61584343a2 100644 --- a/source/simulation2/components/ICmpRangeManager.h +++ b/source/simulation2/components/ICmpRangeManager.h @@ -24,6 +24,7 @@ #include "simulation2/system/Interface.h" #include "simulation2/helpers/Position.h" #include "simulation2/helpers/Player.h" +#include "simulation2/helpers/Spatial.h" #include "graphics/Terrain.h" // for TERRAIN_TILE_SIZE @@ -72,6 +73,12 @@ public: */ typedef u32 tag_t; + /** + * Access the spatial subdivision kept by the range manager. + * @return pointer to spatial subdivision structure. + */ + virtual SpatialSubdivision* GetSubdivision() = 0; + /** * Set the bounds of the world. * Entities should not be outside the bounds (else efficiency will suffer). @@ -118,12 +125,12 @@ public: entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface, u8 flags) = 0; /** - * Construct an active query of a paraboloic form around the unit. + * Construct an active query of a paraboloic form around the unit. * The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. - * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; - * or -1.0 to ignore distance. + * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; + * or -1.0 to ignore distance. * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them * @param elevationBonus extra bonus so the source can be placed higher and shoot further * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. diff --git a/source/simulation2/helpers/Selection.cpp b/source/simulation2/helpers/Selection.cpp index b6d0a0ba7c..4cc7a7ebbc 100644 --- a/source/simulation2/helpers/Selection.cpp +++ b/source/simulation2/helpers/Selection.cpp @@ -27,26 +27,43 @@ #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpVisual.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) +std::vector EntitySelection::PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables, int range) { + PROFILE2("PickEntitiesAtPoint"); CVector3D origin, dir; camera.BuildCameraRay(screenX, screenY, origin, dir); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); - std::vector > hits; // (dist^2, entity) pairs + /* 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)); - const CSimulation2::InterfaceListUnordered& ents = simulation.GetEntitiesWithInterfaceUnordered(IID_Selectable); - for (CSimulation2::InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) + // Get a rough group of entities using our approximated origin. + SpatialQueryArray 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 (int i = 0; i < ents.size(); ++i) { - entity_id_t ent = it->first; - CEntityHandle handle = it->second->GetEntityHandle(); + CmpPtr cmpSelectable(simulation, ents[i]); + if (!cmpSelectable) + continue; + + CEntityHandle handle = cmpSelectable->GetEntityHandle(); // Check if this entity is only selectable in Atlas - if (!allowEditorSelectables && static_cast(it->second)->IsEditorOnly()) + if (!allowEditorSelectables && cmpSelectable->IsEditorOnly()) continue; // Ignore entities hidden by LOS (or otherwise hidden, e.g. when not IsInWorld) @@ -86,12 +103,11 @@ std::vector EntitySelection::PickEntitiesAtPoint(CSimulation2& simu } // 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, ent)); + hits.push_back(std::make_pair(dist2, ents[i])); } // Sort hits by distance @@ -107,6 +123,7 @@ std::vector EntitySelection::PickEntitiesAtPoint(CSimulation2& simu std::vector EntitySelection::PickEntitiesInRect(CSimulation2& simulation, const CCamera& camera, int sx0, int sy0, int sx1, int sy1, player_id_t owner, bool allowEditorSelectables) { + PROFILE2("PickEntitiesInRect"); // Make sure sx0 <= sx1, and sy0 <= sy1 if (sx0 > sx1) std::swap(sx0, sx1); @@ -148,7 +165,6 @@ std::vector EntitySelection::PickEntitiesInRect(CSimulation2& simul CVector3D position = cmpVisual->GetPosition(); // Reject if it's not on-screen (e.g. it's behind the camera) - if (!camera.GetFrustum().IsPointVisible(position)) continue; @@ -168,6 +184,7 @@ std::vector EntitySelection::PickSimilarEntities(CSimulation2& simu const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables, bool allowFoundations) { + PROFILE2("PickSimilarEntities"); CmpPtr cmpTemplateManager(simulation, SYSTEM_ENTITY); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); diff --git a/source/simulation2/helpers/Selection.h b/source/simulation2/helpers/Selection.h index 484ee085be..908727bb63 100644 --- a/source/simulation2/helpers/Selection.h +++ b/source/simulation2/helpers/Selection.h @@ -43,10 +43,11 @@ namespace EntitySelection * this value is ignored as the whole map is revealed. * @param allowEditorSelectables if true, all entities with the ICmpSelectable interface * will be selected (including decorative actors), else only those selectable ingame. + * @param range Approximate range to check for entity in. * * @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); +std::vector PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables, int range = 200); /** * Finds all selectable entities within the given screen coordinate rectangle, diff --git a/source/simulation2/helpers/Spatial.h b/source/simulation2/helpers/Spatial.h index bd1540d66c..cd3b8d8a40 100644 --- a/source/simulation2/helpers/Spatial.h +++ b/source/simulation2/helpers/Spatial.h @@ -18,6 +18,7 @@ #ifndef INCLUDED_SPATIAL #define INCLUDED_SPATIAL +#include "simulation2/system/Component.h" #include "simulation2/serialization/SerializeTemplates.h" #include "ps/CLogger.h"