From 0cd0a1f5848507f0c877f86b51e8575b528e9b35 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Thu, 29 Jul 2010 20:39:23 +0000 Subject: [PATCH] # Add auto-attacking of nearby enemies. Add general range-detection code. Avoid unnecessarily computing 3D entity positions. This was SVN commit r7817. --- .../mods/public/gui/session_new/session.xml | 7 +- .../simulation/components/GuiInterface.js | 7 + .../public/simulation/components/UnitAI.js | 107 +++- .../public/simulation/components/Vision.js | 5 + .../mods/public/simulation/helpers/FSM.js | 17 +- source/maths/FixedVector2D.h | 46 ++ source/scriptinterface/ScriptConversions.cpp | 51 +- source/simulation2/MessageTypes.h | 48 +- source/simulation2/Simulation2.cpp | 1 + source/simulation2/TypeList.h | 4 + .../simulation2/components/CCmpFootprint.cpp | 8 +- .../components/CCmpObstruction.cpp | 6 +- .../simulation2/components/CCmpPosition.cpp | 18 +- .../components/CCmpRangeManager.cpp | 527 ++++++++++++++++++ .../simulation2/components/CCmpUnitMotion.cpp | 23 +- source/simulation2/components/ICmpPosition.h | 7 + .../components/ICmpRangeManager.cpp | 32 ++ .../simulation2/components/ICmpRangeManager.h | 122 ++++ source/simulation2/helpers/Render.cpp | 10 +- .../scripting/MessageTypeConversions.cpp | 16 + source/simulation2/system/InterfaceScripted.h | 6 + 21 files changed, 997 insertions(+), 71 deletions(-) create mode 100644 source/simulation2/components/CCmpRangeManager.cpp create mode 100644 source/simulation2/components/ICmpRangeManager.cpp create mode 100644 source/simulation2/components/ICmpRangeManager.h diff --git a/binaries/data/mods/public/gui/session_new/session.xml b/binaries/data/mods/public/gui/session_new/session.xml index 9065564840..2ef335d7d0 100644 --- a/binaries/data/mods/public/gui/session_new/session.xml +++ b/binaries/data/mods/public/gui/session_new/session.xml @@ -64,7 +64,7 @@ /> - diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index 6dbf881e46..433cb83177 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -248,6 +248,12 @@ GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) } }; +GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) +{ + var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + cmpRangeManager.SetDebugOverlay(enabled); +}; + // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is @@ -262,6 +268,7 @@ var exposedFunctions = { "SetPathfinderDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, + "SetRangeDebugOverlay": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index a1d928d277..4ae79d98f3 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -19,6 +19,10 @@ var UnitFsmSpec = { // ignore uninteresting construction messages }, + "LosRangeUpdate": function(msg) { + // ignore newly-seen units by default + }, + "Attacked": function(msg) { // Default behaviour: attack back at our attacker if (this.CanAttack(msg.data.attacker)) @@ -30,11 +34,35 @@ var UnitFsmSpec = { "IDLE": { "enter": function() { + // If we entered the idle state we must have nothing better to do, + // so immediately check whether there's anybody nearby to attack. + // (If anyone approaches later, it'll be handled via LosRangeUpdate.) + if (this.losRangeQuery) + { + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var ents = rangeMan.ResetActiveQuery(this.losRangeQuery); + if (this.AttackVisibleEntity(ents)) + return true; + } + + // Nobody to attack - switch to idle this.SelectAnimation("idle"); + return false; + }, + + "leave": function() { + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + rangeMan.DisableActiveQuery(this.losRangeQuery); + }, + + "LosRangeUpdate": function(msg) { + // TODO: implement stances (ignore this message if hold-fire stance) + + // Start attacking one of the newly-seen enemy (if any) + this.AttackVisibleEntity(msg.data.added); }, }, - "Order.Walk": function(msg) { var ok; if (this.order.data.target) @@ -328,14 +356,56 @@ UnitAI.prototype.Init = function() this.order = undefined; // always == this.orderQueue[0] }; - -//// FSM linkage functions //// - UnitAI.prototype.OnCreate = function() { UnitFsm.Init(this, "INDIVIDUAL.IDLE"); }; +UnitAI.prototype.OnOwnershipChanged = function(msg) +{ + this.SetupRangeQuery(msg.to); +}; + +UnitAI.prototype.OnDestroy = function() +{ + // Clean up any timers that are now obsolete + this.StopTimer(); + + // Clean up range queries + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + if (this.losRangeQuery) + rangeMan.DestroyActiveQuery(this.losRangeQuery); +}; + +// Set up a range query for all enemy units within LOS range +// which can be attacked. +// This should be called whenever our ownership changes. +UnitAI.prototype.SetupRangeQuery = function(owner) +{ + var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); + if (!cmpVision) + return; + + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + + if (this.losRangeQuery) + rangeMan.DestroyActiveQuery(this.losRangeQuery); + + var range = cmpVision.GetRange(); + + // Find all enemy players (i.e. exclude Gaia and ourselves) + var players = []; + for (var i = 1; i < playerMan.GetNumPlayers(); ++i) + if (i != owner) + players.push(i); + + this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range, players, IID_DamageReceiver); + rangeMan.EnableActiveQuery(this.losRangeQuery); +}; + +//// FSM linkage functions //// + UnitAI.prototype.SetNextState = function(state) { UnitFsm.SetNextState(this, state); @@ -438,12 +508,6 @@ UnitAI.prototype.StopTimer = function() //// Message handlers ///// -UnitAI.prototype.OnDestroy = function() -{ - // Clean up any timers that are now obsolete - this.StopTimer(); -}; - UnitAI.prototype.OnMotionChanged = function(msg) { if (!msg.speed) @@ -463,6 +527,12 @@ UnitAI.prototype.OnAttacked = function(msg) UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; +UnitAI.prototype.OnRangeUpdate = function(msg) +{ + if (msg.tag == this.losRangeQuery) + UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); +}; + //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() @@ -562,6 +632,23 @@ UnitAI.prototype.GetBestAttack = function() return cmpAttack.GetBestAttack(); }; +/** + * Try to find one of the given entities which can be attacked, + * and start attacking it. + * Returns true if it found something to attack. + */ +UnitAI.prototype.AttackVisibleEntity = function(ents) +{ + for each (var target in ents) + { + if (this.CanAttack(target)) + { + this.PushOrderFront("Attack", { "target": target }); + return true; + } + } + return false; +}; //// External interface functions //// diff --git a/binaries/data/mods/public/simulation/components/Vision.js b/binaries/data/mods/public/simulation/components/Vision.js index c650f2bc6a..1dd9329f85 100644 --- a/binaries/data/mods/public/simulation/components/Vision.js +++ b/binaries/data/mods/public/simulation/components/Vision.js @@ -14,4 +14,9 @@ Vision.prototype.Schema = * TODO: this all needs to be designed and implemented */ +Vision.prototype.GetRange = function() +{ + return +this.template.Range; +}; + Engine.RegisterComponentType(IID_Vision, "Vision", Vision); diff --git a/binaries/data/mods/public/simulation/helpers/FSM.js b/binaries/data/mods/public/simulation/helpers/FSM.js index 64115238cd..20784b6412 100644 --- a/binaries/data/mods/public/simulation/helpers/FSM.js +++ b/binaries/data/mods/public/simulation/helpers/FSM.js @@ -220,6 +220,11 @@ FSM.prototype.SwitchToNextState = function(obj, nextStateName) if (!toState) error("Tried to change to non-existent state '" + nextState + "'"); + // Find the set of states in the hierarchy tree to leave then enter, + // to traverse from the old state to the new one. + // If any enter/leave function returns true then abort the process + // (this lets them intercept the transition and start a new transition) + for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix) { } @@ -228,14 +233,22 @@ FSM.prototype.SwitchToNextState = function(obj, nextStateName) { var leave = this.states[fromState[i]].leave; if (leave) - leave.apply(obj); + { + obj.fsmStateName = fromState[i]; + if (leave.apply(obj)) + return; + } } for (var i = equalPrefix; i < toState.length; ++i) { var enter = this.states[toState[i]].enter; if (enter) - enter.apply(obj); + { + obj.fsmStateName = toState[i]; + if (enter.apply(obj)) + return; + } } obj.fsmStateName = nextStateName; diff --git a/source/maths/FixedVector2D.h b/source/maths/FixedVector2D.h index 1723a4a7f3..ac42b490bc 100644 --- a/source/maths/FixedVector2D.h +++ b/source/maths/FixedVector2D.h @@ -111,6 +111,52 @@ public: return r; } + /** + * Returns -1, 0, +1 depending on whether length is less/equal/greater + * than the argument. + * Avoids sqrting and overflowing. + */ + int CompareLength(fixed cmp) const + { + i64 x = (i64)X.GetInternalValue(); // abs(x) <= 2^31 + i64 y = (i64)Y.GetInternalValue(); + u64 xx = (u64)(x * x); // xx <= 2^62 + u64 yy = (u64)(y * y); + u64 d2 = xx + yy; // d2 <= 2^63 (no overflow) + + i64 c = (i64)cmp.GetInternalValue(); + u64 c2 = (u64)(c * c); + if (d2 < c2) + return -1; + else if (d2 > c2) + return +1; + else + return 0; + } + + /** + * Returns -1, 0, +1 depending on whether length is less/equal/greater + * than the argument's length. + * Avoids sqrting and overflowing. + */ + int CompareLength(const CFixedVector2D& other) const + { + i64 x = (i64)X.GetInternalValue(); + i64 y = (i64)Y.GetInternalValue(); + u64 d2 = (u64)(x * x) + (u64)(y * y); + + i64 ox = (i64)other.X.GetInternalValue(); + i64 oy = (i64)other.Y.GetInternalValue(); + u64 od2 = (u64)(ox * ox) + (u64)(oy * oy); + + if (d2 < od2) + return -1; + else if (d2 > od2) + return +1; + else + return 0; + } + bool IsZero() const { return (X.IsZero() && Y.IsZero()); diff --git a/source/scriptinterface/ScriptConversions.cpp b/source/scriptinterface/ScriptConversions.cpp index d55165ce5b..0a6f18393a 100644 --- a/source/scriptinterface/ScriptConversions.cpp +++ b/source/scriptinterface/ScriptConversions.cpp @@ -117,30 +117,6 @@ template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, return true; } -/* -template bool ScriptInterface::FromJSVal >(JSContext* cx, jsval v, std::vector& out) -{ - JSObject* obj; - if (!JS_ValueToObject(cx, v, &obj) || obj == NULL || !JS_IsArrayObject(cx, obj)) - FAIL("Argument must be an array"); - jsuint length; - if (!JS_GetArrayLength(cx, obj, &length)) - FAIL("Failed to get array length"); - out.reserve(length); - for (jsuint i = 0; i < length; ++i) - { - jsval el; - if (!JS_GetElement(cx, obj, i, &el)) - FAIL("Failed to read array element"); - T el2; - if (!FromJSVal::Convert(cx, el, el2)) - return false; - out.push_back(el2); - } - return true; -} -*/ - //////////////////////////////////////////////////////////////// // Primitive types: @@ -235,6 +211,28 @@ template static jsval ToJSVal_vector(JSContext* cx, const std::vecto return OBJECT_TO_JSVAL(obj); } +template static bool FromJSVal_vector(JSContext* cx, jsval v, std::vector& out) +{ + JSObject* obj; + if (!JS_ValueToObject(cx, v, &obj) || obj == NULL || !JS_IsArrayObject(cx, obj)) + FAIL("Argument must be an array"); + jsuint length; + if (!JS_GetArrayLength(cx, obj, &length)) + FAIL("Failed to get array length"); + out.reserve(length); + for (jsuint i = 0; i < length; ++i) + { + jsval el; + if (!JS_GetElement(cx, obj, i, &el)) + FAIL("Failed to read array element"); + T el2; + if (!ScriptInterface::FromJSVal(cx, el, el2)) + return false; + out.push_back(el2); + } + return true; +} + template<> jsval ScriptInterface::ToJSVal >(JSContext* cx, const std::vector& val) { return ToJSVal_vector(cx, val); @@ -249,3 +247,8 @@ template<> jsval ScriptInterface::ToJSVal >(JSContext* { return ToJSVal_vector(cx, val); } + +template<> bool ScriptInterface::FromJSVal >(JSContext* cx, jsval v, std::vector& out) +{ + return FromJSVal_vector(cx, v, out); +} diff --git a/source/simulation2/MessageTypes.h b/source/simulation2/MessageTypes.h index 2e944cf355..53ad55a23c 100644 --- a/source/simulation2/MessageTypes.h +++ b/source/simulation2/MessageTypes.h @@ -87,7 +87,7 @@ public: }; /** - * This is send immediately after a new entity's components have all be created + * This is sent immediately after a new entity's components have all been created * and initialised. */ class CMessageCreate : public CMessage @@ -149,11 +149,12 @@ class CMessagePositionChanged : public CMessage public: DEFAULT_MESSAGE_IMPL(PositionChanged) - CMessagePositionChanged(bool inWorld, entity_pos_t x, entity_pos_t z, entity_angle_t a) : - inWorld(inWorld), x(x), z(z), a(a) + CMessagePositionChanged(entity_id_t entity, bool inWorld, entity_pos_t x, entity_pos_t z, entity_angle_t a) : + entity(entity), inWorld(inWorld), x(x), z(z), a(a) { } + entity_id_t entity; bool inWorld; entity_pos_t x, z; entity_angle_t a; @@ -192,4 +193,45 @@ public: ssize_t i0, j0, i1, j1; // inclusive lower bound, exclusive upper bound, in tiles }; +/** + * Sent by CCmpRangeManager at most once per turn, when an active range query + * has had matching units enter/leave the range since the last RangeUpdate. + */ +class CMessageRangeUpdate : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(RangeUpdate) + + CMessageRangeUpdate(u32 tag, const std::vector& added, const std::vector& removed) : + tag(tag), added(added), removed(removed) + { + } + + u32 tag; + std::vector added; + std::vector removed; + + // CCmpRangeManager wants to store a vector of messages and wants to + // swap vectors instead of copying (to save on memory allocations), + // so add some constructors for it: + + CMessageRangeUpdate(u32 tag) : + tag(tag) + { + } + + CMessageRangeUpdate(const CMessageRangeUpdate& other) : + CMessage(), tag(other.tag), added(other.added), removed(other.removed) + { + } + + CMessageRangeUpdate& operator=(const CMessageRangeUpdate& other) + { + tag = other.tag; + added = other.added; + removed = other.removed; + return *this; + } +}; + #endif // INCLUDED_MESSAGETYPES diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index a77053bc20..14108e8b79 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -95,6 +95,7 @@ public: m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ObstructionManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, LoadXML(L"special/pathfinder.xml").GetChild("Pathfinder")); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam); + m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_RangeManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_SoundManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam); diff --git a/source/simulation2/TypeList.h b/source/simulation2/TypeList.h index 9338e77f98..c3de6bc174 100644 --- a/source/simulation2/TypeList.h +++ b/source/simulation2/TypeList.h @@ -39,6 +39,7 @@ MESSAGE(Destroy) MESSAGE(OwnershipChanged) MESSAGE(PositionChanged) MESSAGE(MotionChanged) +MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) // TemplateManager must come before all other (non-test) components, @@ -92,6 +93,9 @@ COMPONENT(Position) // must be before VisualActor INTERFACE(ProjectileManager) COMPONENT(ProjectileManager) +INTERFACE(RangeManager) +COMPONENT(RangeManager) + INTERFACE(Selectable) COMPONENT(Selectable) diff --git a/source/simulation2/components/CCmpFootprint.cpp b/source/simulation2/components/CCmpFootprint.cpp index 6d2649a798..3f2fc525e5 100644 --- a/source/simulation2/components/CCmpFootprint.cpp +++ b/source/simulation2/components/CCmpFootprint.cpp @@ -148,7 +148,7 @@ public: // The spawn point should be far enough from this footprint to fit the unit, plus a little gap entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2); - CFixedVector3D initialPos = cmpPosition->GetPosition(); + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); entity_angle_t initialAngle = cmpPosition->GetRotation().Y; if (m_Shape == CIRCLE) @@ -164,7 +164,7 @@ public: fixed s, c; sincos_approx(angle, s, c); - CFixedVector3D pos (initialPos.X + s.Multiply(radius), fixed::Zero(), initialPos.Z + c.Multiply(radius)); + CFixedVector3D pos (initialPos.X + s.Multiply(radius), fixed::Zero(), initialPos.Y + c.Multiply(radius)); SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity if (!cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Z, spawnedRadius)) @@ -207,9 +207,7 @@ public: sy = m_Size1; break; } - CFixedVector2D center; - center.X = initialPos.X + (-dir.Y).Multiply(sy/2 + clearance); - center.Y = initialPos.Z + dir.X.Multiply(sy/2 + clearance); + CFixedVector2D center = initialPos - dir.Perpendicular().Multiply(sy/2 + clearance); dir = dir.Multiply((sx + clearance*2) / (int)(numPoints-1)); for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] diff --git a/source/simulation2/components/CCmpObstruction.cpp b/source/simulation2/components/CCmpObstruction.cpp index 733968a929..7c638147f9 100644 --- a/source/simulation2/components/CCmpObstruction.cpp +++ b/source/simulation2/components/CCmpObstruction.cpp @@ -208,16 +208,16 @@ public: if (cmpPosition.null()) return false; - CFixedVector3D pos = cmpPosition->GetPosition(); + CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); SkipTagObstructionFilter filter(m_Tag); // ignore collisions with self if (m_Type == STATIC) - return cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Z, cmpPosition->GetRotation().Y, m_Size0, m_Size1); + return cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1); else - return cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Z, m_Size0); + return cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Size0); } }; diff --git a/source/simulation2/components/CCmpPosition.cpp b/source/simulation2/components/CCmpPosition.cpp index 2c5921ceb6..7624d7713d 100644 --- a/source/simulation2/components/CCmpPosition.cpp +++ b/source/simulation2/components/CCmpPosition.cpp @@ -246,12 +246,20 @@ public: baseY = std::max(baseY, cmpWaterMan->GetWaterLevel(m_X, m_Z)); } - // NOTE: most callers don't actually care about Y; if this is a performance - // issue then we could add a new method that simply returns X/Z - return CFixedVector3D(m_X, baseY + m_YOffset, m_Z); } + virtual CFixedVector2D GetPosition2D() + { + if (!m_InWorld) + { + LOGERROR(L"CCmpPosition::GetPosition2D called on entity when IsInWorld is false"); + return CFixedVector2D(); + } + + return CFixedVector2D(m_X, m_Z); + } + virtual void TurnTo(entity_angle_t y) { m_RotY = y; @@ -381,12 +389,12 @@ private: { if (m_InWorld) { - CMessagePositionChanged msg(true, m_X, m_Z, m_RotY); + CMessagePositionChanged msg(GetEntityId(), true, m_X, m_Z, m_RotY); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } else { - CMessagePositionChanged msg(false, entity_pos_t(), entity_pos_t(), entity_angle_t()); + CMessagePositionChanged msg(GetEntityId(), false, entity_pos_t::Zero(), entity_pos_t::Zero(), entity_angle_t::Zero()); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } } diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp new file mode 100644 index 0000000000..9d9a496074 --- /dev/null +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -0,0 +1,527 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "simulation2/system/Component.h" +#include "ICmpRangeManager.h" + +#include "ICmpPosition.h" +#include "simulation2/MessageTypes.h" +#include "simulation2/helpers/Render.h" + +#include "graphics/Overlay.h" +#include "maths/FixedVector2D.h" +#include "ps/CLogger.h" +#include "ps/Overlay.h" +#include "ps/Profile.h" +#include "renderer/Scene.h" + +/** + * Representation of a range query. + */ +struct Query +{ + bool enabled; + entity_id_t source; + entity_pos_t maxRange; + u32 ownersMask; + int interface; + std::vector lastMatch; +}; + +/** + * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) + * into a 31-bit mask for quick set-membership tests. + */ +static u32 CalcOwnerMask(i32 owner) +{ + if (owner >= -1 && owner < 30) + return 1 << (1+owner); + else + return 0; // owner was invalid +} + +/** + * Representation of an entity, with the data needed for queries. + */ +struct EntityData +{ + EntityData() : ownerMask(CalcOwnerMask(-1)), inWorld(0) { } + entity_pos_t x, z; + u32 ownerMask : 31; + u32 inWorld : 1; +}; + +cassert(sizeof(EntityData) == 12); + +/** + * Functor for sorting entities by distance from a source point. + * It must only be passed entities that has a Position component + * and are currently in the world. + */ +struct EntityDistanceOrdering +{ + EntityDistanceOrdering(const CSimContext& context, const CFixedVector2D& source) : + context(context), source(source) + { + } + + bool operator()(entity_id_t a, entity_id_t b) + { + CmpPtr cmpPositionA(context, a); + CmpPtr cmpPositionB(context, b); + // (these positions will be valid and in the world, else ExecuteQuery wouldn't have returned it) + CFixedVector2D vecA = cmpPositionA->GetPosition2D() - source; + CFixedVector2D vecB = cmpPositionB->GetPosition2D() - source; + return (vecA.CompareLength(vecB) < 0); + } + + const CSimContext& context; + CFixedVector2D source; +}; + +/** + * Basic range manager implementation. + * Maintains a list of all entities (and their positions and owners), which is used for + * queries. + * + * TODO: Ideally this would use a quadtree or something for more efficient spatial queries, + * since it's about O(n^2) in the total number of entities on the map. + */ +class CCmpRangeManager : public ICmpRangeManager +{ +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeGloballyToMessageType(MT_Create); + componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); + componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); + componentManager.SubscribeGloballyToMessageType(MT_Destroy); + + componentManager.SubscribeToMessageType(MT_Update); + + componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays + } + + DEFAULT_COMPONENT_ALLOCATOR(RangeManager) + + bool m_DebugOverlayEnabled; + bool m_DebugOverlayDirty; + std::vector m_DebugOverlayLines; + + tag_t m_QueryNext; // next allocated id + std::map m_Queries; + std::map m_EntityData; + + static std::string GetSchema() + { + return ""; + } + + virtual void Init(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode)) + { + m_QueryNext = 1; + + m_DebugOverlayEnabled = false; + m_DebugOverlayDirty = true; + } + + virtual void Deinit(const CSimContext& UNUSED(context)) + { + } + + virtual void Serialize(ISerializer& UNUSED(serialize)) + { + // TODO + } + + virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { + Init(context, paramNode); + } + + virtual void HandleMessage(const CSimContext& UNUSED(context), const CMessage& msg, bool UNUSED(global)) + { + switch (msg.GetType()) + { + case MT_Create: + { + const CMessageCreate& msgData = static_cast (msg); + entity_id_t ent = msgData.entity; + + // Ignore local entities - we shouldn't let them influence anything + if (ENTITY_IS_LOCAL(ent)) + break; + + // Ignore non-positional entities + CmpPtr cmpPosition(GetSimContext(), ent); + if (cmpPosition.null()) + break; + + // The newly-created entity will have owner -1 and position out-of-world + // (any initialisation of those values will happen later), so we can just + // use the default-constructed EntityData here + EntityData entdata; + + // Remember this entity + m_EntityData.insert(std::make_pair(ent, entdata)); + + break; + } + case MT_PositionChanged: + { + const CMessagePositionChanged& msgData = static_cast (msg); + entity_id_t ent = msgData.entity; + + std::map::iterator it = m_EntityData.find(ent); + + // Ignore if we're not already tracking this entity + if (it == m_EntityData.end()) + break; + + if (msgData.inWorld) + { + it->second.inWorld = 1; + it->second.x = msgData.x; + it->second.z = msgData.z; + } + else + { + it->second.inWorld = 0; + it->second.x = entity_pos_t::Zero(); + it->second.z = entity_pos_t::Zero(); + } + + break; + } + case MT_OwnershipChanged: + { + const CMessageOwnershipChanged& msgData = static_cast (msg); + entity_id_t ent = msgData.entity; + + std::map::iterator it = m_EntityData.find(ent); + + // Ignore if we're not already tracking this entity + if (it == m_EntityData.end()) + break; + + it->second.ownerMask = CalcOwnerMask(msgData.to); + + break; + } + case MT_Destroy: + { + const CMessageDestroy& msgData = static_cast (msg); + entity_id_t ent = msgData.entity; + + m_EntityData.erase(ent); + + break; + } + case MT_Update: + { + m_DebugOverlayDirty = true; + ExecuteActiveQueries(); + break; + } + case MT_RenderSubmit: + { + const CMessageRenderSubmit& msgData = static_cast (msg); + RenderSubmit(msgData.collector); + break; + } + } + } + + + virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t maxRange, + std::vector owners, int requiredInterface) + { + size_t id = m_QueryNext++; + m_Queries[id] = ConstructQuery(source, maxRange, owners, requiredInterface); + + return id; + } + + virtual void DestroyActiveQuery(tag_t tag) + { + if (m_Queries.find(tag) == m_Queries.end()) + { + LOGERROR(L"CCmpRangeManager: DestroyActiveQuery called with invalid tag %d", tag); + return; + } + + m_Queries.erase(tag); + } + + virtual void EnableActiveQuery(tag_t tag) + { + std::map::iterator it = m_Queries.find(tag); + if (it == m_Queries.end()) + { + LOGERROR(L"CCmpRangeManager: EnableActiveQuery called with invalid tag %d", tag); + return; + } + + Query& q = it->second; + q.enabled = true; + } + + virtual void DisableActiveQuery(tag_t tag) + { + std::map::iterator it = m_Queries.find(tag); + if (it == m_Queries.end()) + { + LOGERROR(L"CCmpRangeManager: DisableActiveQuery called with invalid tag %d", tag); + return; + } + + Query& q = it->second; + q.enabled = false; + } + + virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t maxRange, + std::vector owners, int requiredInterface) + { + PROFILE("ExecuteQuery"); + + Query q = ConstructQuery(source, maxRange, owners, requiredInterface); + + std::vector r; + + CmpPtr cmpSourcePosition(GetSimContext(), q.source); + if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) + { + // If the source doesn't have a position, then the result is just the empty list + return r; + } + + PerformQuery(q, r); + + // Return the list sorted by distance from the entity + CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); + std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(GetSimContext(), pos)); + + return r; + } + + virtual std::vector ResetActiveQuery(tag_t tag) + { + PROFILE("ResetActiveQuery"); + + std::vector r; + + std::map::iterator it = m_Queries.find(tag); + if (it == m_Queries.end()) + { + LOGERROR(L"CCmpRangeManager: ResetActiveQuery called with invalid tag %d", tag); + return r; + } + + Query& q = it->second; + q.enabled = true; + + CmpPtr cmpSourcePosition(GetSimContext(), q.source); + if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) + { + // If the source doesn't have a position, then the result is just the empty list + q.lastMatch = r; + return r; + } + + PerformQuery(q, r); + + q.lastMatch = r; + + // Return the list sorted by distance from the entity + CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); + std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(GetSimContext(), pos)); + + return r; + } + + virtual void SetDebugOverlay(bool enabled) + { + m_DebugOverlayEnabled = enabled; + m_DebugOverlayDirty = true; + if (!enabled) + m_DebugOverlayLines.clear(); + } + +private: + + /** + * Update all currently-enabled active queries. + */ + void ExecuteActiveQueries() + { + PROFILE("ExecuteActiveQueries"); + + // Store a queue of all messages before sending any, so we can assume + // no entities will move until we've finished checking all the ranges + std::vector > messages; + + for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) + { + Query& q = it->second; + + if (!q.enabled) + continue; + + CmpPtr cmpSourcePosition(GetSimContext(), q.source); + if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) + continue; + + std::vector r; + r.reserve(q.lastMatch.size()); + + PerformQuery(q, r); + + // Compute the changes vs the last match + std::vector added; + std::vector removed; + std::set_difference(r.begin(), r.end(), q.lastMatch.begin(), q.lastMatch.end(), std::back_inserter(added)); + std::set_difference(q.lastMatch.begin(), q.lastMatch.end(), r.begin(), r.end(), std::back_inserter(removed)); + + if (added.empty() && removed.empty()) + continue; + + // 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) + CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); + std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(GetSimContext(), pos)); + + messages.push_back(std::make_pair(q.source, CMessageRangeUpdate(it->first))); + messages.back().second.added.swap(added); + messages.back().second.removed.swap(removed); + + it->second.lastMatch.swap(r); + } + + for (size_t i = 0; i < messages.size(); ++i) + GetSimContext().GetComponentManager().PostMessage(messages[i].first, messages[i].second); + } + + /** + * Returns a list of distinct entity IDs that match the given query, sorted by ID. + */ + void PerformQuery(const Query& q, std::vector& r) + { + CmpPtr cmpSourcePosition(GetSimContext(), q.source); + if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) + return; + CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); + + for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) + { + // Quick filter to ignore entities with the wrong owner + if (!(it->second.ownerMask & q.ownersMask)) + continue; + + // Restrict based on location + // (TODO: this bit ought to use a quadtree or something) + if (!it->second.inWorld) + continue; + int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); + if (distVsMax > 0) + continue; + + // Ignore self + if (it->first == q.source) + continue; + + // Ignore if it's missing the required interface + if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(it->first, q.interface)) + continue; + + r.push_back(it->first); + } + } + + Query ConstructQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) + { + Query q; + q.enabled = false; + q.source = source; + q.maxRange = maxRange; + + q.ownersMask = 0; + for (size_t i = 0; i < owners.size(); ++i) + q.ownersMask |= CalcOwnerMask(owners[i]); + + q.interface = requiredInterface; + + return q; + } + + void RenderSubmit(SceneCollector& collector) + { + if (!m_DebugOverlayEnabled) + return; + + CColor enabledRingColour(0, 1, 0, 1); + CColor disabledRingColour(1, 0, 0, 1); + CColor rayColour(1, 1, 0, 0.2); + + if (m_DebugOverlayDirty) + { + m_DebugOverlayLines.clear(); + + for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) + { + Query& q = it->second; + + CmpPtr cmpSourcePosition(GetSimContext(), q.source); + if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) + continue; + CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); + + // Draw the range circle + m_DebugOverlayLines.push_back(SOverlayLine()); + m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); + SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToDouble(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); + + // Draw a ray from the source to each matched entity + for (size_t i = 0; i < q.lastMatch.size(); ++i) + { + CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); + if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) + continue; + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + + std::vector coords; + coords.push_back(pos.X.ToFloat()); + coords.push_back(pos.Y.ToFloat()); + coords.push_back(targetPos.X.ToFloat()); + coords.push_back(targetPos.Y.ToFloat()); + + m_DebugOverlayLines.push_back(SOverlayLine()); + m_DebugOverlayLines.back().m_Color = rayColour; + SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); + } + } + + m_DebugOverlayDirty = false; + } + + for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) + collector.Submit(&m_DebugOverlayLines[i]); + } +}; + +REGISTER_COMPONENT_TYPE(RangeManager) diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp index a0e0a86934..a2d0398831 100644 --- a/source/simulation2/components/CCmpUnitMotion.cpp +++ b/source/simulation2/components/CCmpUnitMotion.cpp @@ -354,8 +354,7 @@ void CCmpUnitMotion::Move(fixed dt) if (cmpPosition.null()) return; - CFixedVector3D pos3 = cmpPosition->GetPosition(); - CFixedVector2D pos (pos3.X, pos3.Z); + CFixedVector2D pos = cmpPosition->GetPosition2D(); // We want to move (at most) m_Speed*dt units from pos towards the next waypoint @@ -491,8 +490,7 @@ bool CCmpUnitMotion::MoveToPoint(entity_pos_t x, entity_pos_t z) if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; - CFixedVector3D pos3 = cmpPosition->GetPosition(); - CFixedVector2D pos (pos3.X, pos3.Z); + CFixedVector2D pos = cmpPosition->GetPosition2D(); // Reset any current movement m_HasTarget = false; @@ -560,8 +558,7 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; - CFixedVector3D pos3 = cmpPosition->GetPosition(); - CFixedVector2D pos (pos3.X, pos3.Z); + CFixedVector2D pos = cmpPosition->GetPosition2D(); // Reset any current movement m_HasTarget = false; @@ -690,9 +687,9 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) return false; - CFixedVector3D targetPos = cmpTargetPosition->GetPosition(); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); - return MoveToPointRange(targetPos.X, targetPos.Z, minRange, maxRange); + return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); } } @@ -704,8 +701,7 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; - CFixedVector3D pos3 = cmpPosition->GetPosition(); - CFixedVector2D pos (pos3.X, pos3.Z); + CFixedVector2D pos = cmpPosition->GetPosition2D(); entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); @@ -750,8 +746,7 @@ bool CCmpUnitMotion::IsInAttackRange(entity_id_t target, entity_pos_t minRange, if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; - CFixedVector3D pos3 = cmpPosition->GetPosition(); - CFixedVector2D pos (pos3.X, pos3.Z); + CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) @@ -801,9 +796,9 @@ bool CCmpUnitMotion::IsInAttackRange(entity_id_t target, entity_pos_t minRange, if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) return false; - CFixedVector3D targetPos = cmpTargetPosition->GetPosition(); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); - entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length(); + entity_pos_t distance = (pos - targetPos).Length(); if (minRange <= distance && distance <= maxRange) return true; diff --git a/source/simulation2/components/ICmpPosition.h b/source/simulation2/components/ICmpPosition.h index 52ba59de53..43518b987a 100644 --- a/source/simulation2/components/ICmpPosition.h +++ b/source/simulation2/components/ICmpPosition.h @@ -22,6 +22,7 @@ #include "simulation2/helpers/Position.h" #include "maths/FixedVector3D.h" +#include "maths/FixedVector2D.h" class CMatrix3D; @@ -95,6 +96,12 @@ public: */ virtual CFixedVector3D GetPosition() = 0; + /** + * Returns the current x,z position (no interpolation). + * Must not be called unless IsInWorld is true. + */ + virtual CFixedVector2D GetPosition2D() = 0; + /** * Rotate smoothly to the given angle around the upwards axis. * @param y clockwise radians from the +Z axis. diff --git a/source/simulation2/components/ICmpRangeManager.cpp b/source/simulation2/components/ICmpRangeManager.cpp new file mode 100644 index 0000000000..309248f642 --- /dev/null +++ b/source/simulation2/components/ICmpRangeManager.cpp @@ -0,0 +1,32 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpRangeManager.h" + +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(RangeManager) +DEFINE_INTERFACE_METHOD_4("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, std::vector, int) +DEFINE_INTERFACE_METHOD_4("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, std::vector, int) +DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) +DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) +DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) +DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t) +DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) +END_INTERFACE_WRAPPER(RangeManager) diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h new file mode 100644 index 0000000000..926006c4ac --- /dev/null +++ b/source/simulation2/components/ICmpRangeManager.h @@ -0,0 +1,122 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPRANGEMANAGER +#define INCLUDED_ICMPRANGEMANAGER + +#include "simulation2/system/Interface.h" + +#include "simulation2/helpers/Position.h" + +/** + * Provides efficient range-based queries of the game world. + * + * Possible use cases: + * - combat units need to detect targetable enemies entering LOS, so they can choose + * to auto-attack. + * - auras let a unit have some effect on all units (or those of the same player, or of enemies) + * within a certain range. + * - capturable animals need to detect when a player-owned unit is nearby and no units of other + * players are in range. + * - scenario triggers may want to detect when units enter a given area. + * - units gathering from a resource that is exhausted need to find a new resource of the + * same type, near the old one and reachable. + * - projectile weapons with splash damage need to find all units within some distance + * of the target point. + * - ... + * + * In most cases the users are event-based and want notifications when something + * has entered or left the range, and the query can be set up once and rarely changed. + * These queries have to be fast. It's fine to approximate an entity as a point. + * + * Current design: + * + * This class handles just the most common parts of range queries: + * distance, target interface, and player ownership. + * The caller can then apply any more complex filtering that it needs. + * + * There are two types of query: + * Passive queries are performed by ExecuteQuery and immediately return the matching entities. + * Active queries are set up by CreateActiveQuery, and then a CMessageRangeUpdate message will be + * sent to the entity once per turn if anybody has entered or left the range since the last RangeUpdate. + * Queries can be disabled, in which case no message will be sent. + */ +class ICmpRangeManager : public IComponent +{ +public: + /** + * External identifiers for active queries. + */ + typedef u32 tag_t; + + /** + * Execute a passive query. + * @param source the entity around which the range will be computed. + * @param maxRange maximum distance in metres (inclusive). + * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. + * @param requiredInterface if non-zero, an interface ID that matching entities must implement. + * @return list of entities matching the query, ordered by increasing distance from the source entity. + */ + virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0; + + /** + * Construct an active query. The query will be disabled by default. + * @param source the entity around which the range will be computed. + * @param maxRange maximum distance in metres (inclusive). + * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. + * @param requiredInterface if non-zero, an interface ID that matching entities must implement. + * @return unique non-zero identifier of query. + */ + virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0; + + /** + * Destroy a query and clean up resources. This must be called when an entity no longer needs its + * query (e.g. when the entity is destroyed). + * @param tag identifier of query. + */ + virtual void DestroyActiveQuery(tag_t tag) = 0; + + /** + * Re-enable the processing of a query. + * @param tag identifier of query. + */ + virtual void EnableActiveQuery(tag_t tag) = 0; + + /** + * Disable the processing of a query (no RangeUpdate messages will be sent). + * @param tag identifier of query. + */ + virtual void DisableActiveQuery(tag_t tag) = 0; + + /** + * Immediately execute a query, and re-enable it if disabled. + * The next RangeUpdate message will say who has entered/left since this call, + * so you won't miss any notifications. + * @param tag identifier of query. + * @return list of entities matching the query, ordered by increasing distance from the source entity. + */ + virtual std::vector ResetActiveQuery(tag_t tag) = 0; + + /** + * Toggle the rendering of debug info. + */ + virtual void SetDebugOverlay(bool enabled) = 0; + + DECLARE_INTERFACE_TYPE(RangeManager) +}; + +#endif // INCLUDED_ICMPRANGEMANAGER diff --git a/source/simulation2/helpers/Render.cpp b/source/simulation2/helpers/Render.cpp index ada8370b34..7af59b4622 100644 --- a/source/simulation2/helpers/Render.cpp +++ b/source/simulation2/helpers/Render.cpp @@ -26,7 +26,6 @@ #include "graphics/Terrain.h" #include "maths/MathUtil.h" -static const size_t RENDER_CIRCLE_POINTS = 16; static const float RENDER_HEIGHT_DELTA = 0.25f; // distance above terrain void SimRender::ConstructLineOnGround(const CSimContext& context, std::vector xz, @@ -79,11 +78,14 @@ void SimRender::ConstructCircleOnGround(const CSimContext& context, float x, flo water = cmpWaterMan->GetExactWaterLevel(x, z); } - overlay.m_Coords.reserve((RENDER_CIRCLE_POINTS + 1) * 3); + // Adapt the circle resolution to look reasonable for small and largeish radiuses + size_t numPoints = clamp((size_t)(radius*4.0f), (size_t)12, (size_t)48); - for (size_t i = 0; i <= RENDER_CIRCLE_POINTS; ++i) // use '<=' so it's a closed loop + overlay.m_Coords.reserve((numPoints + 1) * 3); + + for (size_t i = 0; i <= numPoints; ++i) // use '<=' so it's a closed loop { - float a = i * 2 * (float)M_PI / RENDER_CIRCLE_POINTS; + float a = i * 2 * (float)M_PI / numPoints; float px = x + radius * sin(a); float pz = z + radius * cos(a); float py = std::max(water, cmpTerrain->GetExactGroundLevel(px, pz)) + RENDER_HEIGHT_DELTA; diff --git a/source/simulation2/scripting/MessageTypeConversions.cpp b/source/simulation2/scripting/MessageTypeConversions.cpp index 15eebc6e3a..b4192eb49c 100644 --- a/source/simulation2/scripting/MessageTypeConversions.cpp +++ b/source/simulation2/scripting/MessageTypeConversions.cpp @@ -200,6 +200,22 @@ CMessage* CMessageTerrainChanged::FromJSVal(ScriptInterface& UNUSED(scriptInterf return NULL; } +//////////////////////////////// + +jsval CMessageRangeUpdate::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(tag); + SET_MSG_PROPERTY(added); + SET_MSG_PROPERTY(removed); + return OBJECT_TO_JSVAL(obj); +} + +CMessage* CMessageRangeUpdate::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) +{ + return NULL; +} + //////////////////////////////////////////////////////////////// CMessage* CMessageFromJSVal(int mtid, ScriptInterface& scriptingInterface, jsval val) diff --git a/source/simulation2/system/InterfaceScripted.h b/source/simulation2/system/InterfaceScripted.h index 9e3c7e190c..d90b0061cd 100644 --- a/source/simulation2/system/InterfaceScripted.h +++ b/source/simulation2/system/InterfaceScripted.h @@ -73,4 +73,10 @@ 4, \ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT, 0 }, +#define DEFINE_INTERFACE_METHOD_5(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5) \ + { scriptname, \ + ScriptInterface::callMethod, \ + 5, \ + JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT, 0 }, + #endif // INCLUDED_INTERFACE_SCRIPTED