1
0
forked from 0ad/0ad

# Added AI for chickens.

Add scripted HFSM system.
Add very basic animal AI.
Support script-only message types.
Add shift+D hotkey to toggle dev command panel.

This was SVN commit r7763.
This commit is contained in:
Ykkrosh 2010-07-18 15:19:49 +00:00
parent 5f366d798f
commit e19146cf25
20 changed files with 722 additions and 47 deletions

View File

@ -176,6 +176,7 @@ hotkey.resourcepool.toggle = "Shift+R" ; Toggle Resource Pool.
hotkey.grouppane.toggle = "Shift+G" ; Toggle Group Pane. hotkey.grouppane.toggle = "Shift+G" ; Toggle Group Pane.
hotkey.teamtray.toggle = "Shift+T" ; Toggle Team Tray. hotkey.teamtray.toggle = "Shift+T" ; Toggle Team Tray.
hotkey.session.ShowPlayersList = "Shift+P" ; Toggle Players List hotkey.session.ShowPlayersList = "Shift+P" ; Toggle Players List
hotkey.session.devcommands.toggle = "Shift+D"
; > SESSION ORIENTATION KEYS ; > SESSION ORIENTATION KEYS
hotkey.session.gui.flip = "Alt+G" ; Toggle GUI to top/bottom/left/right of screen. hotkey.session.gui.flip = "Alt+G" ; Toggle GUI to top/bottom/left/right of screen.

View File

@ -17,7 +17,7 @@
<Threshold>5</Threshold> <Threshold>5</Threshold>
<Decay>3</Decay> <Decay>3</Decay>
<Replacement>chicken_10.ogg</Replacement> <Replacement>chicken_10.ogg</Replacement>
<Path>/audio/actor/fauna/animal</Path> <Path>audio/actor/fauna/animal</Path>
<Sound>chicken_13.ogg</Sound> <Sound>chicken_13.ogg</Sound>
<Sound>chicken_10.ogg</Sound> <Sound>chicken_10.ogg</Sound>
<Sound>chicken_11.ogg</Sound> <Sound>chicken_11.ogg</Sound>

View File

@ -64,7 +64,12 @@
/> />
<!-- Dev/cheat commands --> <!-- Dev/cheat commands -->
<object z="200" size="100%-155 50 100%-16 130" type="image" name="devCommands" sprite="devCommandsBackground" hidden="true"> <object name="devCommands" size="100%-155 50 100%-16 130" z="200" type="image" sprite="devCommandsBackground"
hidden="true" hotkey="session.devcommands.toggle">
<action on="Press">
this.hidden = !this.hidden;
</action>
<object size="0 0 100%-18 16" type="text" style="devCommandsText">Control all units</object> <object size="0 0 100%-18 16" type="text" style="devCommandsText">Control all units</object>
<object size="100%-16 0 100% 16" type="checkbox" name="devControlAll" style="wheatCrossBox"/> <object size="100%-16 0 100% 16" type="checkbox" name="devControlAll" style="wheatCrossBox"/>

View File

@ -0,0 +1,241 @@
function AnimalAI() {}
AnimalAI.prototype.Schema =
"<a:example/>" +
"<element name='NaturalBehaviour' a:help='Behaviour of the unit in the absence of player commands (intended for animals)'>" +
"<choice>" +
"<value a:help='Will actively attack any unit it encounters, even if not threatened'>violent</value>" +
"<value a:help='Will attack nearby units if it feels threatened (if they linger within LOS for too long)'>aggressive</value>" +
"<value a:help='Will attack nearby units if attacked'>defensive</value>" +
"<value a:help='Will never attack units'>passive</value>" +
"<value a:help='Will never attack units. Will typically attempt to flee for short distances when units approach'>skittish</value>" +
"</choice>" +
"</element>";
var AnimalFsmSpec = {
"SKITTISH": {
"ResourceGather": function(msg) {
// If someone's carving chunks of meat off us, then run away
this.MoveAwayFrom(msg.gatherer, 12);
this.SetNextState("FLEEING");
this.PlaySound("panic");
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SelectAnimation("walk", false);
this.MoveRandomly();
// Set a random timer to switch to feeding state
this.StartTimer(RandomInt(2000, 8000));
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MoveStopped": function() {
this.MoveRandomly();
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("idle");
this.StopMoving();
this.StartTimer(RandomInt(1000, 4000));
},
"leave": function() {
this.StopTimer();
},
"MoveStopped": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": {
"enter": function() {
// Run quickly
this.SelectAnimation("run", false);
this.SetMoveSpeedFactor(6.0);
},
"leave": function() {
// Reset normal speed
this.SetMoveSpeedFactor(1.0);
},
"MoveStopped": function() {
// When we've run far enough, go back to the roaming state
this.SetNextState("ROAMING");
},
},
},
};
var AnimalFsm = new FSM(AnimalFsmSpec);
AnimalAI.prototype.Init = function()
{
this.messageQueue = [];
};
// FSM linkage functions:
AnimalAI.prototype.OnCreate = function()
{
AnimalFsm.Init(this, "SKITTISH.ROAMING");
};
AnimalAI.prototype.SetNextState = function(state)
{
AnimalFsm.SetNextState(this, state);
};
AnimalAI.prototype.DeferMessage = function(msg)
{
AnimalFsm.DeferMessage(this, msg);
};
AnimalAI.prototype.PushMessage = function(msg)
{
this.messageQueue.push(msg);
};
AnimalAI.prototype.OnUpdate = function()
{
var mq = this.messageQueue;
this.messageQueue = [];
for each (var msg in mq)
AnimalFsm.ProcessMessage(this, msg);
};
AnimalAI.prototype.OnMotionChanged = function(msg)
{
if (!msg.speed)
this.PushMessage({"type": "MoveStopped"});
};
AnimalAI.prototype.OnResourceGather = function(msg)
{
this.PushMessage({"type": "ResourceGather", "gatherer": msg.gatherer});
};
AnimalAI.prototype.TimerHandler = function(data, lateness)
{
this.PushMessage({"type": "Timer", "data": data, "lateness": lateness});
};
// Functions to be called by the FSM:
AnimalAI.prototype.PlaySound = function(name)
{
PlaySound(name, this.entity);
};
AnimalAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
var soundgroup;
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
// Set default values if unspecified
if (typeof once == "undefined")
once = false;
if (typeof speed == "undefined")
speed = 1.0;
if (typeof soundgroup == "undefined")
soundgroup = "";
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
AnimalAI.prototype.MoveRandomly = function()
{
// We want to walk in a random direction, but avoid getting stuck
// in obstacles or narrow spaces.
// So pick a circular range from approximately our current position,
// and move outwards to the nearest point on that circle, which will
// lead to us avoiding obstacles and moving towards free space.
// TODO: we probably ought to have a 'home' point, and drift towards
// that, so we don't spread out all across the whole map
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
if (!cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var distance = 4;
var jitter = 0.5;
// Randomly adjust the range's center a bit, so we tend to prefer
// moving in random directions (if there's nothing in the way)
var tx = pos.x + (2*Math.random()-1)*jitter;
var tz = pos.z + (2*Math.random()-1)*jitter;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
};
AnimalAI.prototype.MoveAwayFrom = function(ent, distance)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.MoveToAttackRange(ent, distance, distance);
};
AnimalAI.prototype.StopMoving = function()
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.StopMoving();
};
AnimalAI.prototype.SetMoveSpeedFactor = function(factor)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.SetSpeedFactor(factor);
};
AnimalAI.prototype.StartTimer = function(interval, data)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_AnimalAI, "TimerHandler", interval, data);
};
AnimalAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
Engine.RegisterComponentType(IID_AnimalAI, "AnimalAI", AnimalAI);

View File

@ -301,6 +301,9 @@ UnitAI.prototype.StartGather = function()
// Start the gather animation and sound // Start the gather animation and sound
this.SelectAnimation(typename, false, 1.0, typename); this.SelectAnimation(typename, false, 1.0, typename);
// Tell the target we're gathering from it
Engine.PostMessage(this.gatherTarget, MT_ResourceGather, { "entity": this.gatherTarget, "gatherer": this.entity });
}; };
UnitAI.prototype.CancelTimers = function() UnitAI.prototype.CancelTimers = function()

View File

@ -0,0 +1 @@
Engine.RegisterInterface("AnimalAI");

View File

@ -1 +1,5 @@
Engine.RegisterInterface("ResourceSupply"); Engine.RegisterInterface("ResourceSupply");
// Message sent from gatherers to ResourceSupply entities
// when beginning to gather
Engine.RegisterMessageType("ResourceGather");

View File

@ -0,0 +1,244 @@
// Hierarchical finite state machine implementation.
//
// FSMs are specified as a JS data structure;
// see e.g. AnimalAI.js for an example of the syntax.
//
// FSMs are implicitly linked with an external object.
// That object stores all FSM-related state.
// (This means we can serialise FSM-based components as
// plain old JS objects, with no need to serialise the complex
// FSM structure itself or to add custom serialisation code.)
function FSM(spec)
{
// The (relatively) human-readable FSM specification needs to get
// compiled into a more-efficient-to-execute version.
//
// In particular, message handling should require minimal
// property lookups in the common case (even when the FSM has
// a deeply nested hierarchy), and there should never be any
// string manipulation at run-time.
this.decompose = { "": [] };
/* 'decompose' will store:
{
"": [],
"A": ["A"],
"A.B": ["A", "A.B"],
"A.B.C": ["A", "A.B", "A.B.C"],
"A.B.D": ["A", "A.B", "A.B.D"],
...
};
This is used when switching between states in different branches
of the hierarchy, to determine the list of sub-states to leave/enter
*/
this.states = { };
/* 'states' will store:
{
...
"A": {
"_name": "A",
"_parent": "",
"_refs": { // local -> global name lookups (for SetNextState)
"B": "A.B",
"B.C": "A.B.C",
"B.D": "A.B.D",
},
},
"A.B": {
"_name": "A.B",
"_parent": "A",
"_refs": {
"C": "A.B.C",
"D": "A.B.D",
},
"MessageType": function(msg) { ... },
},
"A.B.C": {
"_name": "A.B.C",
"_parent": "A.B",
"_refs": {},
"enter": function() { ... },
"MessageType": function(msg) { ... },
},
"A.B.D": {
"_name": "A.B.D",
"_parent": "A.B",
"_refs": {},
"enter": function() { ... },
"leave": function() { ... },
"MessageType": function(msg) { ... },
},
...
}
*/
function process(fsm, node, path, handlers)
{
var state = {};
fsm.states[path.join(".")] = state;
var newhandlers = {};
for (var e in handlers)
newhandlers[e] = handlers[e];
state._name = path.join(".");
state._parent = path.slice(0, -1).join(".");
state._refs = {};
for (var key in node)
{
if (key === "enter" || key === "leave")
{
state[key] = node[key];
}
else if (key.match(/^[A-Z]+$/))
{
state._refs[key] = (state._name ? state._name + "." : "") + key;
// (the rest of this will be handled later once we've grabbed
// all the event handlers)
}
else
{
newhandlers[key] = node[key];
}
}
for (var e in newhandlers)
state[e] = newhandlers[e];
for (var key in node)
{
if (key.match(/^[A-Z]+$/))
{
var newpath = path.concat([key]);
var decomposed = [newpath[0]];
for (var i = 1; i < newpath.length; ++i)
decomposed.push(decomposed[i-1] + "." + newpath[i]);
fsm.decompose[newpath.join(".")] = decomposed;
var childstate = process(fsm, node[key], newpath, newhandlers);
for (var r in childstate._refs)
{
var cname = key + "." + r;
state._refs[cname] = childstate._refs[r];
}
}
}
return state;
}
process(this, spec, [], {});
}
FSM.prototype.Init = function(obj, initialState)
{
this.deferFromState = undefined;
obj.fsmStateName = "";
obj.fsmNextState = undefined;
this.SwitchToNextState(obj, initialState);
};
FSM.prototype.SetNextState = function(obj, state)
{
obj.fsmNextState = state;
};
FSM.prototype.ProcessMessage = function(obj, msg)
{
// print("ProcessMessage(obj, "+uneval(msg)+")\n");
var func = this.states[obj.fsmStateName][msg.type];
if (!func)
{
error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'");
return;
}
func.apply(obj, [msg]);
while (obj.fsmNextState)
{
var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState);
obj.fsmNextState = undefined;
if (nextStateName != obj.fsmStateName)
this.SwitchToNextState(obj, nextStateName);
}
};
FSM.prototype.DeferMessage = function(obj, msg)
{
// We need to work out which sub-state we were running the message handler from,
// and then try again in its parent state.
var old = this.deferFromState;
var from;
if (old) // if we're recursively deferring and saved the last used state, use that
from = old;
else // if this is the first defer then we must have last processed the message in the current FSM state
from = obj.fsmStateName;
// Find and save the parent, for use in recursive defers
this.deferFromState = this.states[from]._parent;
// Run the function from the parent state
var state = this.states[this.deferFromState];
var func = state[msg.type];
if (!func)
error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'");
func.apply(obj, [msg]);
// Restore the changes we made
this.deferFromState = old;
// TODO: if an inherited handler defers, it calls exactly the same handler
// on the parent state, which is probably useless and inefficient
// NOTE: this will break if two units try to execute AI at the same time;
// as long as AI messages are queue and processed asynchronously it should be fine
};
FSM.prototype.LookupState = function(currentStateName, stateName)
{
// print("LookupState("+currentStateName+", "+stateName+")\n");
for (var s = currentStateName; s; s = this.states[s]._parent)
if (stateName in this.states[s]._refs)
return this.states[s]._refs[stateName];
return stateName;
};
FSM.prototype.SwitchToNextState = function(obj, nextStateName)
{
var fromState = this.decompose[obj.fsmStateName];
var toState = this.decompose[nextStateName];
if (!toState)
error("Tried to change to non-existent state '" + nextState + "'");
for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix)
{
}
for (var i = fromState.length-1; i >= equalPrefix; --i)
{
var leave = this.states[fromState[i]].leave;
if (leave)
leave.apply(obj);
}
for (var i = equalPrefix; i < toState.length; ++i)
{
var enter = this.states[toState[i]].enter;
if (enter)
enter.apply(obj);
}
obj.fsmStateName = nextStateName;
}
Engine.RegisterGlobal("FSM", FSM);

View File

@ -0,0 +1,9 @@
/**
* Returns a random integer from min (inclusive) to max (exclusive)
*/
function RandomInt(min, max)
{
return Math.floor(min + Math.random() * (max-min))
}
Engine.RegisterGlobal("RandomInt", RandomInt);

View File

@ -15,4 +15,17 @@
<Circle radius="0.5"/> <Circle radius="0.5"/>
<Height>1.5</Height> <Height>1.5</Height>
</Footprint> </Footprint>
<AnimalAI>
<NaturalBehaviour>skittish</NaturalBehaviour>
</AnimalAI>
<UnitMotion>
<WalkSpeed>1</WalkSpeed>
<PassabilityClass>default</PassabilityClass>
<CostClass>default</CostClass>
</UnitMotion>
<Sound>
<SoundGroups>
<panic>actor/fauna/animal/chickens.xml</panic>
</SoundGroups>
</Sound>
</Entity> </Entity>

View File

@ -25,7 +25,7 @@
#include "simulation2/helpers/Position.h" #include "simulation2/helpers/Position.h"
#define DEFAULT_MESSAGE_IMPL(name) \ #define DEFAULT_MESSAGE_IMPL(name) \
virtual EMessageTypeId GetType() const { return MT_##name; } \ virtual int GetType() const { return MT_##name; } \
virtual const char* GetScriptHandlerName() const { return "On" #name; } \ virtual const char* GetScriptHandlerName() const { return "On" #name; } \
virtual const char* GetScriptGlobalHandlerName() const { return "OnGlobal" #name; } \ virtual const char* GetScriptGlobalHandlerName() const { return "OnGlobal" #name; } \
virtual jsval ToJSVal(ScriptInterface& scriptInterface) const; \ virtual jsval ToJSVal(ScriptInterface& scriptInterface) const; \
@ -86,6 +86,23 @@ public:
bool culling; bool culling;
}; };
/**
* This is send immediately after a new entity's components have all be created
* and initialised.
*/
class CMessageCreate : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Create)
CMessageCreate(entity_id_t entity) :
entity(entity)
{
}
entity_id_t entity;
};
/** /**
* This is sent immediately before a destroyed entity is flushed and really destroyed. * This is sent immediately before a destroyed entity is flushed and really destroyed.
* (That is, after CComponentManager::DestroyComponentsSoon and inside FlushDestroyedComponents). * (That is, after CComponentManager::DestroyComponentsSoon and inside FlushDestroyedComponents).

View File

@ -34,6 +34,7 @@ MESSAGE(TurnStart)
MESSAGE(Update) MESSAGE(Update)
MESSAGE(Interpolate) // non-deterministic (use with caution) MESSAGE(Interpolate) // non-deterministic (use with caution)
MESSAGE(RenderSubmit) // non-deterministic (use with caution) MESSAGE(RenderSubmit) // non-deterministic (use with caution)
MESSAGE(Create)
MESSAGE(Destroy) MESSAGE(Destroy)
MESSAGE(OwnershipChanged) MESSAGE(OwnershipChanged)
MESSAGE(PositionChanged) MESSAGE(PositionChanged)

View File

@ -59,7 +59,7 @@ public:
// Template state: // Template state:
fixed m_Speed; // in metres per second fixed m_WalkSpeed; // in metres per second
fixed m_RunSpeed; fixed m_RunSpeed;
entity_pos_t m_Radius; entity_pos_t m_Radius;
u8 m_PassClass; u8 m_PassClass;
@ -67,6 +67,7 @@ public:
// Dynamic state: // Dynamic state:
fixed m_Speed;
bool m_HasTarget; // whether we currently have valid paths and targets bool m_HasTarget; // whether we currently have valid paths and targets
// These values contain undefined junk if !HasTarget: // These values contain undefined junk if !HasTarget:
ICmpPathfinder::Path m_Path; ICmpPathfinder::Path m_Path;
@ -121,7 +122,8 @@ public:
{ {
m_HasTarget = false; m_HasTarget = false;
m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_WalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed();
m_Speed = m_WalkSpeed;
if (paramNode.GetChild("Run").IsOk()) if (paramNode.GetChild("Run").IsOk())
{ {
@ -129,7 +131,7 @@ public:
} }
else else
{ {
m_RunSpeed = m_Speed; m_RunSpeed = m_WalkSpeed;
} }
CmpPtr<ICmpObstruction> cmpObstruction(context, GetEntityId()); CmpPtr<ICmpObstruction> cmpObstruction(context, GetEntityId());
@ -202,9 +204,9 @@ public:
} }
} }
virtual fixed GetSpeed() virtual fixed GetWalkSpeed()
{ {
return m_Speed; return m_WalkSpeed;
} }
virtual fixed GetRunSpeed() virtual fixed GetRunSpeed()
@ -212,6 +214,11 @@ public:
return m_RunSpeed; return m_RunSpeed;
} }
virtual void SetSpeedFactor(fixed factor)
{
m_Speed = m_WalkSpeed.Multiply(factor);
}
virtual void SetDebugOverlay(bool enabled) virtual void SetDebugOverlay(bool enabled)
{ {
m_DebugOverlayEnabled = enabled; m_DebugOverlayEnabled = enabled;
@ -225,6 +232,12 @@ public:
virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z); virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z);
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange);
virtual void StopMoving()
{
SwitchState(IDLE);
}
private: private:
/** /**
@ -537,6 +550,8 @@ bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t
return (errCircle < errSquare); return (errCircle < errSquare);
} }
static const entity_pos_t g_GoalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{ {
PROFILE("MoveToAttackRange"); PROFILE("MoveToAttackRange");
@ -551,8 +566,6 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
// Reset any current movement // Reset any current movement
m_HasTarget = false; m_HasTarget = false;
ICmpPathfinder::Goal goal;
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null()) if (cmpObstructionManager.null())
return false; return false;
@ -588,13 +601,12 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
* (Those units should set minRange to 0 so they'll never be considered *too* close.) * (Those units should set minRange to 0 so they'll never be considered *too* close.)
*/ */
const entity_pos_t goalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
if (tag.valid()) if (tag.valid())
{ {
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag); ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
CFixedVector2D halfSize(obstruction.hw, obstruction.hh); CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
ICmpPathfinder::Goal goal;
goal.x = obstruction.x; goal.x = obstruction.x;
goal.z = obstruction.z; goal.z = obstruction.z;
@ -604,7 +616,7 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
{ {
// Too close to the square - need to move away // Too close to the square - need to move away
entity_pos_t goalDistance = minRange + goalDelta; entity_pos_t goalDistance = minRange + g_GoalDelta;
goal.type = ICmpPathfinder::Goal::SQUARE; goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u; goal.u = obstruction.u;
@ -642,7 +654,7 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
return false; return false;
} }
entity_pos_t goalDistance = maxRange - goalDelta; entity_pos_t goalDistance = maxRange - g_GoalDelta;
goal.type = ICmpPathfinder::Goal::CIRCLE; goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.hw = circleRadius + goalDistance; goal.hw = circleRadius + goalDistance;
@ -652,7 +664,7 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
// The target is large relative to our range, so treat it as a square and // The target is large relative to our range, so treat it as a square and
// get close enough that the diagonals come within range // get close enough that the diagonals come within range
entity_pos_t goalDistance = (maxRange - goalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2) entity_pos_t goalDistance = (maxRange - g_GoalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2)
goal.type = ICmpPathfinder::Goal::SQUARE; goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u; goal.u = obstruction.u;
@ -662,6 +674,13 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
goal.hh = obstruction.hh + delta; goal.hh = obstruction.hh + delta;
} }
} }
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
return true;
} }
else else
{ {
@ -673,31 +692,46 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
CFixedVector3D targetPos = cmpTargetPosition->GetPosition(); CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length(); return MoveToPointRange(targetPos.X, targetPos.Z, minRange, maxRange);
entity_pos_t goalDistance;
if (distance < minRange)
{
goalDistance = minRange + goalDelta;
}
else if (distance > maxRange)
{
goalDistance = maxRange - goalDelta;
}
else
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
// TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target)
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.x = targetPos.X;
goal.z = targetPos.Z;
goal.hw = m_Radius + goalDistance;
} }
}
bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
PROFILE("MoveToPointRange");
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length();
entity_pos_t goalDistance;
if (distance < minRange)
{
goalDistance = minRange + g_GoalDelta;
}
else if (distance > maxRange)
{
goalDistance = maxRange - g_GoalDelta;
}
else
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, x, z);
return false;
}
// TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target)
ICmpPathfinder::Goal goal;
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.x = x;
goal.z = z;
goal.hw = m_Radius + goalDistance;
m_FinalGoal = goal; m_FinalGoal = goal;
if (!RegeneratePath(pos, false)) if (!RegeneratePath(pos, false))

View File

@ -25,6 +25,8 @@ BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD_2("MoveToPoint", bool, ICmpUnitMotion, MoveToPoint, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("MoveToPoint", bool, ICmpUnitMotion, MoveToPoint, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("IsInAttackRange", bool, ICmpUnitMotion, IsInAttackRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("IsInAttackRange", bool, ICmpUnitMotion, IsInAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToAttackRange", bool, ICmpUnitMotion, MoveToAttackRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("MoveToAttackRange", bool, ICmpUnitMotion, MoveToAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
DEFINE_INTERFACE_METHOD_1("SetSpeedFactor", void, ICmpUnitMotion, SetSpeedFactor, fixed)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion) END_INTERFACE_WRAPPER(UnitMotion)

View File

@ -51,7 +51,7 @@ public:
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/** /**
* Attempt to walk into range of a given target, or as close as possible. * Attempt to walk into range of a given target entity, or as close as possible.
* If the unit is already in range, or cannot move anywhere at all, or if there is * If the unit is already in range, or cannot move anywhere at all, or if there is
* some other error, then returns false. * some other error, then returns false.
* Otherwise, sends a MotionChanged message and returns true; it will send another * Otherwise, sends a MotionChanged message and returns true; it will send another
@ -60,10 +60,25 @@ public:
*/ */
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* See MoveToAttackRange, but the target is the given point.
*/
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Stop moving immediately.
*/
virtual void StopMoving() = 0;
/**
* Set the current movement speed to be the default multiplied by the given factor.
*/
virtual void SetSpeedFactor(fixed factor) = 0;
/** /**
* Get the default speed that this unit will have when walking, in metres per second. * Get the default speed that this unit will have when walking, in metres per second.
*/ */
virtual fixed GetSpeed() = 0; virtual fixed GetWalkSpeed() = 0;
/** /**
* Get the default speed that this unit will have when running, in metres per second. * Get the default speed that this unit will have when running, in metres per second.

View File

@ -110,6 +110,22 @@ CMessage* CMessageRenderSubmit::FromJSVal(ScriptInterface& UNUSED(scriptInterfac
//////////////////////////////// ////////////////////////////////
jsval CMessageCreate::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(entity);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageCreate::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(entity_id_t, entity);
return new CMessageCreate(entity);
}
////////////////////////////////
jsval CMessageDestroy::ToJSVal(ScriptInterface& scriptInterface) const jsval CMessageDestroy::ToJSVal(ScriptInterface& scriptInterface) const
{ {
TOJSVAL_SETUP(); TOJSVAL_SETUP();

View File

@ -29,6 +29,28 @@
#include "ps/CLogger.h" #include "ps/CLogger.h"
#include "ps/Filesystem.h" #include "ps/Filesystem.h"
/**
* Used for script-only message types.
*/
class CMessageScripted : public CMessage
{
public:
virtual int GetType() const { return mtid; }
virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); }
virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); }
virtual jsval ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); }
CMessageScripted(int mtid, const std::string& name, const CScriptValRooted& msg) :
mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(msg)
{
}
int mtid;
std::string handlerName;
std::string globalHandlerName;
CScriptValRooted msg;
};
CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFunctions) : CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFunctions) :
m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine"), m_SimContext(context), m_CurrentlyHotloading(false) m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine"), m_SimContext(context), m_CurrentlyHotloading(false)
{ {
@ -45,6 +67,7 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti
{ {
m_ScriptInterface.RegisterFunction<void, int, std::string, CScriptVal, CComponentManager::Script_RegisterComponentType> ("RegisterComponentType"); m_ScriptInterface.RegisterFunction<void, int, std::string, CScriptVal, CComponentManager::Script_RegisterComponentType> ("RegisterComponentType");
m_ScriptInterface.RegisterFunction<void, std::string, CComponentManager::Script_RegisterInterface> ("RegisterInterface"); m_ScriptInterface.RegisterFunction<void, std::string, CComponentManager::Script_RegisterInterface> ("RegisterInterface");
m_ScriptInterface.RegisterFunction<void, std::string, CComponentManager::Script_RegisterMessageType> ("RegisterMessageType");
m_ScriptInterface.RegisterFunction<void, std::string, CScriptVal, CComponentManager::Script_RegisterGlobal> ("RegisterGlobal"); m_ScriptInterface.RegisterFunction<void, std::string, CScriptVal, CComponentManager::Script_RegisterGlobal> ("RegisterGlobal");
m_ScriptInterface.RegisterFunction<IComponent*, int, int, CComponentManager::Script_QueryInterface> ("QueryInterface"); m_ScriptInterface.RegisterFunction<IComponent*, int, int, CComponentManager::Script_QueryInterface> ("QueryInterface");
m_ScriptInterface.RegisterFunction<void, int, int, CScriptVal, CComponentManager::Script_PostMessage> ("PostMessage"); m_ScriptInterface.RegisterFunction<void, int, int, CScriptVal, CComponentManager::Script_PostMessage> ("PostMessage");
@ -281,7 +304,27 @@ void CComponentManager::Script_RegisterInterface(void* cbdata, std::string name)
// IIDs start at 1, so size+1 is the next unused one // IIDs start at 1, so size+1 is the next unused one
size_t id = componentManager->m_InterfaceIdsByName.size() + 1; size_t id = componentManager->m_InterfaceIdsByName.size() + 1;
componentManager->m_InterfaceIdsByName[name] = id; componentManager->m_InterfaceIdsByName[name] = id;
componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int )id); componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id);
}
void CComponentManager::Script_RegisterMessageType(void* cbdata, std::string name)
{
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
std::map<std::string, MessageTypeId>::iterator it = componentManager->m_MessageTypeIdsByName.find(name);
if (it != componentManager->m_MessageTypeIdsByName.end())
{
// Redefinitions are fine (and just get ignored) when hotloading; otherwise
// they're probably unintentional and should be reported
if (!componentManager->m_CurrentlyHotloading)
componentManager->m_ScriptInterface.ReportError("Registering message type with already-registered name"); // TODO: report the actual name
return;
}
// MTIDs start at 1, so size+1 is the next unused one
size_t id = componentManager->m_MessageTypeIdsByName.size() + 1;
componentManager->RegisterMessageType(id, name.c_str());
componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id);
} }
void CComponentManager::Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value) void CComponentManager::Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value)
@ -300,10 +343,27 @@ IComponent* CComponentManager::Script_QueryInterface(void* cbdata, int ent, int
return component; return component;
} }
CMessage* CComponentManager::ConstructMessage(int mtid, CScriptVal data)
{
if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here)
LOGERROR(L"PostMessage with invalid message type ID '%d'", mtid);
if (mtid < MT__LastNative)
{
return CMessageFromJSVal(mtid, m_ScriptInterface, data.get());
}
else
{
return new CMessageScripted(mtid, m_MessageTypeNamesById[mtid],
CScriptValRooted(m_ScriptInterface.GetContext(), data));
}
}
void CComponentManager::Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data) void CComponentManager::Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data)
{ {
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata); CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
CMessage* msg = componentManager->ConstructMessage(mtid, data);
if (!msg) if (!msg)
return; // error return; // error
@ -315,7 +375,8 @@ void CComponentManager::Script_PostMessage(void* cbdata, int ent, int mtid, CScr
void CComponentManager::Script_BroadcastMessage(void* cbdata, int mtid, CScriptVal data) void CComponentManager::Script_BroadcastMessage(void* cbdata, int mtid, CScriptVal data)
{ {
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata); CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
CMessage* msg = componentManager->ConstructMessage(mtid, data);
if (!msg) if (!msg)
return; // error return; // error
@ -399,6 +460,7 @@ void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, Comp
void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name)
{ {
m_MessageTypeIdsByName[name] = mtid; m_MessageTypeIdsByName[name] = mtid;
m_MessageTypeNamesById[mtid] = name;
} }
void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) void CComponentManager::SubscribeToMessageType(MessageTypeId mtid)
@ -587,6 +649,9 @@ entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entit
// TODO: maybe we should delete already-constructed components if one of them fails? // TODO: maybe we should delete already-constructed components if one of them fails?
} }
CMessageCreate msg(ent);
PostMessage(ent, msg);
return ent; return ent;
} }

View File

@ -216,6 +216,7 @@ private:
// Implementations of functions exposed to scripts // Implementations of functions exposed to scripts
static void Script_RegisterComponentType(void* cbdata, int iid, std::string cname, CScriptVal ctor); static void Script_RegisterComponentType(void* cbdata, int iid, std::string cname, CScriptVal ctor);
static void Script_RegisterInterface(void* cbdata, std::string name); static void Script_RegisterInterface(void* cbdata, std::string name);
static void Script_RegisterMessageType(void* cbdata, std::string name);
static void Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value); static void Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value);
static IComponent* Script_QueryInterface(void* cbdata, int ent, int iid); static IComponent* Script_QueryInterface(void* cbdata, int ent, int iid);
static void Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data); static void Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data);
@ -224,6 +225,7 @@ private:
static int Script_AddLocalEntity(void* cbdata, std::string templateName); static int Script_AddLocalEntity(void* cbdata, std::string templateName);
static void Script_DestroyEntity(void* cbdata, int ent); static void Script_DestroyEntity(void* cbdata, int ent);
CMessage* ConstructMessage(int mtid, CScriptVal data);
void SendGlobalMessage(const CMessage& msg) const; void SendGlobalMessage(const CMessage& msg) const;
ComponentTypeId GetScriptWrapper(InterfaceId iid); ComponentTypeId GetScriptWrapper(InterfaceId iid);
@ -242,7 +244,9 @@ private:
std::map<MessageTypeId, std::vector<ComponentTypeId> > m_GlobalMessageSubscriptions; std::map<MessageTypeId, std::vector<ComponentTypeId> > m_GlobalMessageSubscriptions;
std::map<std::string, ComponentTypeId> m_ComponentTypeIdsByName; std::map<std::string, ComponentTypeId> m_ComponentTypeIdsByName;
std::map<std::string, MessageTypeId> m_MessageTypeIdsByName; std::map<std::string, MessageTypeId> m_MessageTypeIdsByName;
std::map<MessageTypeId, std::string> m_MessageTypeNamesById;
std::map<std::string, InterfaceId> m_InterfaceIdsByName; std::map<std::string, InterfaceId> m_InterfaceIdsByName;
// TODO: maintaining both ComponentsBy* is nasty; can we get rid of one, // TODO: maintaining both ComponentsBy* is nasty; can we get rid of one,
// while keeping QueryInterface and PostMessage sufficiently efficient? // while keeping QueryInterface and PostMessage sufficiently efficient?

View File

@ -27,7 +27,7 @@ protected:
CMessage() { } CMessage() { }
public: public:
virtual ~CMessage() { } virtual ~CMessage() { }
virtual EMessageTypeId GetType() const = 0; virtual int GetType() const = 0;
virtual const char* GetScriptHandlerName() const = 0; virtual const char* GetScriptHandlerName() const = 0;
virtual const char* GetScriptGlobalHandlerName() const = 0; virtual const char* GetScriptGlobalHandlerName() const = 0;
virtual jsval ToJSVal(ScriptInterface&) const = 0; virtual jsval ToJSVal(ScriptInterface&) const = 0;

View File

@ -187,7 +187,7 @@ void ActorViewer::SetActor(const CStrW& name, const CStrW& animation)
{ {
CmpPtr<ICmpUnitMotion> cmpUnitMotion(m.Simulation2, m.Entity); CmpPtr<ICmpUnitMotion> cmpUnitMotion(m.Simulation2, m.Entity);
if (!cmpUnitMotion.null()) if (!cmpUnitMotion.null())
speed = cmpUnitMotion->GetSpeed().ToFloat(); speed = cmpUnitMotion->GetWalkSpeed().ToFloat();
else else
speed = 7.f; // typical unit speed speed = 7.f; // typical unit speed