1
0
forked from 0ad/0ad

# Add auto-attacking of nearby enemies.

Add general range-detection code.
Avoid unnecessarily computing 3D entity positions.

This was SVN commit r7817.
This commit is contained in:
Ykkrosh 2010-07-29 20:39:23 +00:00
parent 913404e4b2
commit 0cd0a1f584
21 changed files with 997 additions and 71 deletions

View File

@ -64,7 +64,7 @@
/>
<!-- Dev/cheat commands -->
<object name="devCommands" size="100%-155 50 100%-16 130" z="200" type="image" sprite="devCommandsBackground"
<object name="devCommands" size="100%-155 50 100%-16 146" z="200" type="image" sprite="devCommandsBackground"
hidden="true" hotkey="session.devcommands.toggle">
<action on="Press">
this.hidden = !this.hidden;
@ -90,6 +90,11 @@
<object size="100%-16 64 100% 80" type="checkbox" style="wheatCrossBox">
<action on="Press">g_Selection.SetMotionDebugOverlay(this.checked);</action>
</object>
<object size="0 80 100%-18 96" type="text" style="devCommandsText">Range overlay</object>
<object size="100%-16 80 100% 96" type="checkbox" style="wheatCrossBox">
<action on="Press">Engine.GuiInterfaceCall("SetRangeDebugOverlay", this.checked);</action>
</object>
</object>
<!-- ================================ ================================ -->

View File

@ -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)

View File

@ -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 ////

View File

@ -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);

View File

@ -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;

View File

@ -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());

View File

@ -117,30 +117,6 @@ template<> bool ScriptInterface::FromJSVal<std::string>(JSContext* cx, jsval v,
return true;
}
/*
template<typename T> bool ScriptInterface::FromJSVal<std::vector<T> >(JSContext* cx, jsval v, std::vector<T>& 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<T>::Convert(cx, el, el2))
return false;
out.push_back(el2);
}
return true;
}
*/
////////////////////////////////////////////////////////////////
// Primitive types:
@ -235,6 +211,28 @@ template<typename T> static jsval ToJSVal_vector(JSContext* cx, const std::vecto
return OBJECT_TO_JSVAL(obj);
}
template<typename T> static bool FromJSVal_vector(JSContext* cx, jsval v, std::vector<T>& 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<T>(cx, el, el2))
return false;
out.push_back(el2);
}
return true;
}
template<> jsval ScriptInterface::ToJSVal<std::vector<int> >(JSContext* cx, const std::vector<int>& val)
{
return ToJSVal_vector(cx, val);
@ -249,3 +247,8 @@ template<> jsval ScriptInterface::ToJSVal<std::vector<std::wstring> >(JSContext*
{
return ToJSVal_vector(cx, val);
}
template<> bool ScriptInterface::FromJSVal<std::vector<int> >(JSContext* cx, jsval v, std::vector<int>& out)
{
return FromJSVal_vector(cx, v, out);
}

View File

@ -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<entity_id_t>& added, const std::vector<entity_id_t>& removed) :
tag(tag), added(added), removed(removed)
{
}
u32 tag;
std::vector<entity_id_t> added;
std::vector<entity_id_t> 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

View File

@ -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);

View File

@ -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)

View File

@ -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]

View File

@ -208,16 +208,16 @@ public:
if (cmpPosition.null())
return false;
CFixedVector3D pos = cmpPosition->GetPosition();
CFixedVector2D pos = cmpPosition->GetPosition2D();
CmpPtr<ICmpObstructionManager> 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);
}
};

View File

@ -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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<entity_id_t> 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<ICmpPosition> cmpPositionA(context, a);
CmpPtr<ICmpPosition> 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<SOverlayLine> m_DebugOverlayLines;
tag_t m_QueryNext; // next allocated id
std::map<tag_t, Query> m_Queries;
std::map<entity_id_t, EntityData> m_EntityData;
static std::string GetSchema()
{
return "<a:component type='system'/><empty/>";
}
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<const CMessageCreate&> (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<ICmpPosition> 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<const CMessagePositionChanged&> (msg);
entity_id_t ent = msgData.entity;
std::map<u32, EntityData>::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<const CMessageOwnershipChanged&> (msg);
entity_id_t ent = msgData.entity;
std::map<u32, EntityData>::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<const CMessageDestroy&> (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<const CMessageRenderSubmit&> (msg);
RenderSubmit(msgData.collector);
break;
}
}
}
virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t maxRange,
std::vector<int> 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<tag_t, Query>::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<tag_t, Query>::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<entity_id_t> ExecuteQuery(entity_id_t source, entity_pos_t maxRange,
std::vector<int> owners, int requiredInterface)
{
PROFILE("ExecuteQuery");
Query q = ConstructQuery(source, maxRange, owners, requiredInterface);
std::vector<entity_id_t> r;
CmpPtr<ICmpPosition> 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<entity_id_t> ResetActiveQuery(tag_t tag)
{
PROFILE("ResetActiveQuery");
std::vector<entity_id_t> r;
std::map<tag_t, Query>::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<ICmpPosition> 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<std::pair<entity_id_t, CMessageRangeUpdate> > messages;
for (std::map<tag_t, Query>::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it)
{
Query& q = it->second;
if (!q.enabled)
continue;
CmpPtr<ICmpPosition> cmpSourcePosition(GetSimContext(), q.source);
if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld())
continue;
std::vector<entity_id_t> r;
r.reserve(q.lastMatch.size());
PerformQuery(q, r);
// Compute the changes vs the last match
std::vector<entity_id_t> added;
std::vector<entity_id_t> 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<entity_id_t>& r)
{
CmpPtr<ICmpPosition> cmpSourcePosition(GetSimContext(), q.source);
if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld())
return;
CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
for (std::map<entity_id_t, EntityData>::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<int> 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<tag_t, Query>::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it)
{
Query& q = it->second;
CmpPtr<ICmpPosition> 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<ICmpPosition> cmpTargetPosition(GetSimContext(), q.lastMatch[i]);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
continue;
CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
std::vector<float> 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)

View File

@ -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<ICmpObstructionManager> 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;

View File

@ -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.

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ICmpRangeManager.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(RangeManager)
DEFINE_INTERFACE_METHOD_4("ExecuteQuery", std::vector<entity_id_t>, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, std::vector<int>, int)
DEFINE_INTERFACE_METHOD_4("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, std::vector<int>, 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<entity_id_t>, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(RangeManager)

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<entity_id_t> ExecuteQuery(entity_id_t source, entity_pos_t maxRange, std::vector<int> 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<int> 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<entity_id_t> ResetActiveQuery(tag_t tag) = 0;
/**
* Toggle the rendering of debug info.
*/
virtual void SetDebugOverlay(bool enabled) = 0;
DECLARE_INTERFACE_TYPE(RangeManager)
};
#endif // INCLUDED_ICMPRANGEMANAGER

View File

@ -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<float> 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;

View File

@ -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)

View File

@ -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<rettype, arg1, arg2, arg3, arg4, arg5, &class_##classname, classname, &classname::methodname>, \
5, \
JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT, 0 },
#endif // INCLUDED_INTERFACE_SCRIPTED