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:
parent
5f366d798f
commit
e19146cf25
@ -176,6 +176,7 @@ hotkey.resourcepool.toggle = "Shift+R" ; Toggle Resource Pool.
|
||||
hotkey.grouppane.toggle = "Shift+G" ; Toggle Group Pane.
|
||||
hotkey.teamtray.toggle = "Shift+T" ; Toggle Team Tray.
|
||||
hotkey.session.ShowPlayersList = "Shift+P" ; Toggle Players List
|
||||
hotkey.session.devcommands.toggle = "Shift+D"
|
||||
|
||||
; > SESSION ORIENTATION KEYS
|
||||
hotkey.session.gui.flip = "Alt+G" ; Toggle GUI to top/bottom/left/right of screen.
|
||||
|
@ -17,7 +17,7 @@
|
||||
<Threshold>5</Threshold>
|
||||
<Decay>3</Decay>
|
||||
<Replacement>chicken_10.ogg</Replacement>
|
||||
<Path>/audio/actor/fauna/animal</Path>
|
||||
<Path>audio/actor/fauna/animal</Path>
|
||||
<Sound>chicken_13.ogg</Sound>
|
||||
<Sound>chicken_10.ogg</Sound>
|
||||
<Sound>chicken_11.ogg</Sound>
|
||||
|
@ -64,7 +64,12 @@
|
||||
/>
|
||||
|
||||
<!-- 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="100%-16 0 100% 16" type="checkbox" name="devControlAll" style="wheatCrossBox"/>
|
||||
|
||||
|
241
binaries/data/mods/public/simulation/components/AnimalAI.js
Normal file
241
binaries/data/mods/public/simulation/components/AnimalAI.js
Normal 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);
|
@ -301,6 +301,9 @@ UnitAI.prototype.StartGather = function()
|
||||
|
||||
// Start the gather animation and sound
|
||||
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()
|
||||
|
@ -0,0 +1 @@
|
||||
Engine.RegisterInterface("AnimalAI");
|
@ -1 +1,5 @@
|
||||
Engine.RegisterInterface("ResourceSupply");
|
||||
|
||||
// Message sent from gatherers to ResourceSupply entities
|
||||
// when beginning to gather
|
||||
Engine.RegisterMessageType("ResourceGather");
|
||||
|
244
binaries/data/mods/public/simulation/helpers/FSM.js
Normal file
244
binaries/data/mods/public/simulation/helpers/FSM.js
Normal 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);
|
9
binaries/data/mods/public/simulation/helpers/Random.js
Normal file
9
binaries/data/mods/public/simulation/helpers/Random.js
Normal 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);
|
@ -15,4 +15,17 @@
|
||||
<Circle radius="0.5"/>
|
||||
<Height>1.5</Height>
|
||||
</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>
|
||||
|
@ -25,7 +25,7 @@
|
||||
#include "simulation2/helpers/Position.h"
|
||||
|
||||
#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* GetScriptGlobalHandlerName() const { return "OnGlobal" #name; } \
|
||||
virtual jsval ToJSVal(ScriptInterface& scriptInterface) const; \
|
||||
@ -86,6 +86,23 @@ public:
|
||||
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.
|
||||
* (That is, after CComponentManager::DestroyComponentsSoon and inside FlushDestroyedComponents).
|
||||
|
@ -34,6 +34,7 @@ MESSAGE(TurnStart)
|
||||
MESSAGE(Update)
|
||||
MESSAGE(Interpolate) // non-deterministic (use with caution)
|
||||
MESSAGE(RenderSubmit) // non-deterministic (use with caution)
|
||||
MESSAGE(Create)
|
||||
MESSAGE(Destroy)
|
||||
MESSAGE(OwnershipChanged)
|
||||
MESSAGE(PositionChanged)
|
||||
|
@ -59,7 +59,7 @@ public:
|
||||
|
||||
// Template state:
|
||||
|
||||
fixed m_Speed; // in metres per second
|
||||
fixed m_WalkSpeed; // in metres per second
|
||||
fixed m_RunSpeed;
|
||||
entity_pos_t m_Radius;
|
||||
u8 m_PassClass;
|
||||
@ -67,6 +67,7 @@ public:
|
||||
|
||||
// Dynamic state:
|
||||
|
||||
fixed m_Speed;
|
||||
bool m_HasTarget; // whether we currently have valid paths and targets
|
||||
// These values contain undefined junk if !HasTarget:
|
||||
ICmpPathfinder::Path m_Path;
|
||||
@ -121,7 +122,8 @@ public:
|
||||
{
|
||||
m_HasTarget = false;
|
||||
|
||||
m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
|
||||
m_WalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed();
|
||||
m_Speed = m_WalkSpeed;
|
||||
|
||||
if (paramNode.GetChild("Run").IsOk())
|
||||
{
|
||||
@ -129,7 +131,7 @@ public:
|
||||
}
|
||||
else
|
||||
{
|
||||
m_RunSpeed = m_Speed;
|
||||
m_RunSpeed = m_WalkSpeed;
|
||||
}
|
||||
|
||||
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()
|
||||
@ -212,6 +214,11 @@ public:
|
||||
return m_RunSpeed;
|
||||
}
|
||||
|
||||
virtual void SetSpeedFactor(fixed factor)
|
||||
{
|
||||
m_Speed = m_WalkSpeed.Multiply(factor);
|
||||
}
|
||||
|
||||
virtual void SetDebugOverlay(bool enabled)
|
||||
{
|
||||
m_DebugOverlayEnabled = enabled;
|
||||
@ -225,6 +232,12 @@ public:
|
||||
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 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:
|
||||
/**
|
||||
@ -537,6 +550,8 @@ bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t
|
||||
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)
|
||||
{
|
||||
PROFILE("MoveToAttackRange");
|
||||
@ -551,8 +566,6 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
|
||||
// Reset any current movement
|
||||
m_HasTarget = false;
|
||||
|
||||
ICmpPathfinder::Goal goal;
|
||||
|
||||
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
|
||||
if (cmpObstructionManager.null())
|
||||
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.)
|
||||
*/
|
||||
|
||||
const entity_pos_t goalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
|
||||
|
||||
if (tag.valid())
|
||||
{
|
||||
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
|
||||
|
||||
CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
|
||||
ICmpPathfinder::Goal goal;
|
||||
goal.x = obstruction.x;
|
||||
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
|
||||
|
||||
entity_pos_t goalDistance = minRange + goalDelta;
|
||||
entity_pos_t goalDistance = minRange + g_GoalDelta;
|
||||
|
||||
goal.type = ICmpPathfinder::Goal::SQUARE;
|
||||
goal.u = obstruction.u;
|
||||
@ -642,7 +654,7 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
|
||||
return false;
|
||||
}
|
||||
|
||||
entity_pos_t goalDistance = maxRange - goalDelta;
|
||||
entity_pos_t goalDistance = maxRange - g_GoalDelta;
|
||||
|
||||
goal.type = ICmpPathfinder::Goal::CIRCLE;
|
||||
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
|
||||
// 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.u = obstruction.u;
|
||||
@ -662,6 +674,13 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
|
||||
goal.hh = obstruction.hh + delta;
|
||||
}
|
||||
}
|
||||
|
||||
m_FinalGoal = goal;
|
||||
if (!RegeneratePath(pos, false))
|
||||
return false;
|
||||
|
||||
SwitchState(WALKING);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -673,31 +692,46 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
|
||||
|
||||
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
|
||||
|
||||
entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length();
|
||||
|
||||
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;
|
||||
return MoveToPointRange(targetPos.X, targetPos.Z, minRange, maxRange);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!RegeneratePath(pos, false))
|
||||
|
@ -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_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_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)
|
||||
END_INTERFACE_WRAPPER(UnitMotion)
|
||||
|
@ -51,7 +51,7 @@ public:
|
||||
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
|
||||
* some other error, then returns false.
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
virtual fixed GetSpeed() = 0;
|
||||
virtual fixed GetWalkSpeed() = 0;
|
||||
|
||||
/**
|
||||
* Get the default speed that this unit will have when running, in metres per second.
|
||||
|
@ -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
|
||||
{
|
||||
TOJSVAL_SETUP();
|
||||
|
@ -29,6 +29,28 @@
|
||||
#include "ps/CLogger.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) :
|
||||
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, 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<IComponent*, int, int, CComponentManager::Script_QueryInterface> ("QueryInterface");
|
||||
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
|
||||
size_t id = componentManager->m_InterfaceIdsByName.size() + 1;
|
||||
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)
|
||||
@ -300,10 +343,27 @@ IComponent* CComponentManager::Script_QueryInterface(void* cbdata, int ent, int
|
||||
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)
|
||||
{
|
||||
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
|
||||
CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
|
||||
|
||||
CMessage* msg = componentManager->ConstructMessage(mtid, data);
|
||||
if (!msg)
|
||||
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)
|
||||
{
|
||||
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
|
||||
CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
|
||||
|
||||
CMessage* msg = componentManager->ConstructMessage(mtid, data);
|
||||
if (!msg)
|
||||
return; // error
|
||||
|
||||
@ -399,6 +460,7 @@ void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, Comp
|
||||
void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name)
|
||||
{
|
||||
m_MessageTypeIdsByName[name] = mtid;
|
||||
m_MessageTypeNamesById[mtid] = name;
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
|
||||
CMessageCreate msg(ent);
|
||||
PostMessage(ent, msg);
|
||||
|
||||
return ent;
|
||||
}
|
||||
|
||||
|
@ -216,6 +216,7 @@ private:
|
||||
// Implementations of functions exposed to scripts
|
||||
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_RegisterMessageType(void* cbdata, std::string name);
|
||||
static void Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value);
|
||||
static IComponent* Script_QueryInterface(void* cbdata, int ent, int iid);
|
||||
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 void Script_DestroyEntity(void* cbdata, int ent);
|
||||
|
||||
CMessage* ConstructMessage(int mtid, CScriptVal data);
|
||||
void SendGlobalMessage(const CMessage& msg) const;
|
||||
|
||||
ComponentTypeId GetScriptWrapper(InterfaceId iid);
|
||||
@ -242,7 +244,9 @@ private:
|
||||
std::map<MessageTypeId, std::vector<ComponentTypeId> > m_GlobalMessageSubscriptions;
|
||||
std::map<std::string, ComponentTypeId> m_ComponentTypeIdsByName;
|
||||
std::map<std::string, MessageTypeId> m_MessageTypeIdsByName;
|
||||
std::map<MessageTypeId, std::string> m_MessageTypeNamesById;
|
||||
std::map<std::string, InterfaceId> m_InterfaceIdsByName;
|
||||
|
||||
// TODO: maintaining both ComponentsBy* is nasty; can we get rid of one,
|
||||
// while keeping QueryInterface and PostMessage sufficiently efficient?
|
||||
|
||||
|
@ -27,7 +27,7 @@ protected:
|
||||
CMessage() { }
|
||||
public:
|
||||
virtual ~CMessage() { }
|
||||
virtual EMessageTypeId GetType() const = 0;
|
||||
virtual int GetType() const = 0;
|
||||
virtual const char* GetScriptHandlerName() const = 0;
|
||||
virtual const char* GetScriptGlobalHandlerName() const = 0;
|
||||
virtual jsval ToJSVal(ScriptInterface&) const = 0;
|
||||
|
@ -187,7 +187,7 @@ void ActorViewer::SetActor(const CStrW& name, const CStrW& animation)
|
||||
{
|
||||
CmpPtr<ICmpUnitMotion> cmpUnitMotion(m.Simulation2, m.Entity);
|
||||
if (!cmpUnitMotion.null())
|
||||
speed = cmpUnitMotion->GetSpeed().ToFloat();
|
||||
speed = cmpUnitMotion->GetWalkSpeed().ToFloat();
|
||||
else
|
||||
speed = 7.f; // typical unit speed
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user