# 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:
parent
913404e4b2
commit
0cd0a1f584
@ -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>
|
||||
|
||||
<!-- ================================ ================================ -->
|
||||
|
@ -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)
|
||||
|
@ -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,10 +34,34 @@ 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;
|
||||
@ -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 ////
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
527
source/simulation2/components/CCmpRangeManager.cpp
Normal file
527
source/simulation2/components/CCmpRangeManager.cpp
Normal 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)
|
@ -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;
|
||||
|
@ -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.
|
||||
|
32
source/simulation2/components/ICmpRangeManager.cpp
Normal file
32
source/simulation2/components/ICmpRangeManager.cpp
Normal 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)
|
122
source/simulation2/components/ICmpRangeManager.h
Normal file
122
source/simulation2/components/ICmpRangeManager.h
Normal 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
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user