forked from 0ad/0ad
# Improved unit animation in new simulation system
Tried to make the motion/AI/animation state transitions saner Added smoothed rotation of moving units Slightly more informative error reporting This was SVN commit r7319.
This commit is contained in:
parent
4cf5e1e394
commit
60a9e63c71
@ -33,7 +33,7 @@ Timer.prototype.OnUpdate = function(msg)
|
||||
try {
|
||||
cmp[t[2]](t[4]);
|
||||
} catch (e) {
|
||||
print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e);
|
||||
print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e+"\n");
|
||||
// TODO: should report in an error log
|
||||
}
|
||||
delete this.timers[id];
|
||||
|
@ -36,26 +36,8 @@ UnitAI.prototype.Init = function()
|
||||
this.attackRechargeTime = 0;
|
||||
// Timer for AttackTimeout
|
||||
this.attackTimer = undefined;
|
||||
|
||||
this.nextAnimation = undefined;
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnDestroy = function()
|
||||
{
|
||||
if (this.attackTimer)
|
||||
{
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnTurnStart = function()
|
||||
{
|
||||
if (this.nextAnimation)
|
||||
{
|
||||
this.SelectAnimation(this.nextAnimation.name, this.nextAnimation.once, this.nextAnimation.speed);
|
||||
this.nextAnimation = undefined;
|
||||
}
|
||||
// Current target entity ID
|
||||
this.attackTarget = undefined;
|
||||
};
|
||||
|
||||
//// Interface functions ////
|
||||
@ -79,35 +61,102 @@ UnitAI.prototype.Attack = function(target)
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
// Remember the target, and start moving towards it
|
||||
this.attackTarget = target;
|
||||
this.MoveToTarget(this.attackTarget);
|
||||
this.state = STATE_ATTACKING;
|
||||
|
||||
// Cancel any previous attack timer
|
||||
if (this.attackTimer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
|
||||
// TODO: start the attack animation here
|
||||
|
||||
// TODO: should check the range and move closer before attempting to attack
|
||||
|
||||
// Perform the attack after the prepare time, but not before the previous attack's recharge
|
||||
var timers = cmpAttack.GetTimers();
|
||||
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
|
||||
|
||||
var data = { "target": target, "timers": timers };
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, data);
|
||||
|
||||
this.state = STATE_ATTACKING;
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
//// Message handlers ////
|
||||
|
||||
UnitAI.prototype.OnMotionStopped = function()
|
||||
UnitAI.prototype.OnDestroy = function()
|
||||
{
|
||||
this.SelectAnimationDelayed("idle");
|
||||
if (this.attackTimer)
|
||||
{
|
||||
cmpTimer.CancelTimer(this.attackTimer);
|
||||
this.attackTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
UnitAI.prototype.OnMotionChanged = function(msg)
|
||||
{
|
||||
if (msg.speed)
|
||||
{
|
||||
// Started moving
|
||||
// => play the appropriate animation
|
||||
this.SelectAnimation("walk", false, msg.speed);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.state == STATE_WALKING)
|
||||
{
|
||||
// Stopped walking
|
||||
this.state = STATE_IDLE;
|
||||
this.SelectAnimation("idle");
|
||||
}
|
||||
else if (this.state == STATE_ATTACKING)
|
||||
{
|
||||
// We were attacking, and have stopped moving
|
||||
// => check if we can still reach the target now
|
||||
|
||||
if (!this.MoveIntoAttackRange())
|
||||
return;
|
||||
|
||||
// In range, so perform the attack,
|
||||
// after the prepare time but not before the previous attack's recharge
|
||||
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
|
||||
var timers = cmpAttack.GetTimers();
|
||||
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {});
|
||||
|
||||
// Start the idle animation before we switch to the attack
|
||||
this.SelectAnimation("idle");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//// Private functions ////
|
||||
|
||||
/**
|
||||
* Tries to move into range of the attack target.
|
||||
* Returns true if it's already in range.
|
||||
*/
|
||||
UnitAI.prototype.MoveIntoAttackRange = function()
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
|
||||
var rangeStatus = cmpAttack.CheckRange(this.attackTarget);
|
||||
if (rangeStatus.error)
|
||||
{
|
||||
if (rangeStatus.error == "out-of-range")
|
||||
{
|
||||
// Out of range => need to move closer
|
||||
// (The target has probably moved while we were chasing it)
|
||||
this.MoveToTarget(this.attackTarget);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise it's impossible to reach the target, so give up
|
||||
// and switch back to idle
|
||||
this.state = STATE_IDLE;
|
||||
this.SelectAnimation("idle");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
UnitAI.prototype.SelectAnimation = function(name, once, speed)
|
||||
{
|
||||
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
||||
@ -115,15 +164,8 @@ UnitAI.prototype.SelectAnimation = function(name, once, speed)
|
||||
return;
|
||||
|
||||
cmpVisual.SelectAnimation(name, once, speed);
|
||||
|
||||
this.nextAnimation = undefined;
|
||||
};
|
||||
|
||||
UnitAI.prototype.SelectAnimationDelayed = function(name, once, speed)
|
||||
{
|
||||
this.nextAnimation = { "name": name, "once": once, "speed": speed };
|
||||
}
|
||||
|
||||
UnitAI.prototype.MoveToTarget = function(target)
|
||||
{
|
||||
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
|
||||
@ -132,8 +174,6 @@ UnitAI.prototype.MoveToTarget = function(target)
|
||||
|
||||
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
|
||||
|
||||
this.SelectAnimation("walk", false, cmpMotion.GetSpeed());
|
||||
|
||||
var pos = cmpPositionTarget.GetPosition();
|
||||
cmpMotion.MoveToPoint(pos.x, pos.z, 0, 1);
|
||||
};
|
||||
@ -144,40 +184,25 @@ UnitAI.prototype.AttackTimeout = function(data)
|
||||
if (this.state != STATE_ATTACKING)
|
||||
return;
|
||||
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
|
||||
// Check if we can still reach the target
|
||||
var rangeStatus = cmpAttack.CheckRange(data.target);
|
||||
if (rangeStatus.error)
|
||||
{
|
||||
if (rangeStatus.error == "out-of-range")
|
||||
{
|
||||
// Out of range => need to move closer
|
||||
this.MoveToTarget(data.target);
|
||||
// Try again in a couple of seconds
|
||||
// (TODO: ought to have a cleverer way of detecting once we're back in range)
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", 2000, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise it's impossible to reach the target, so give up
|
||||
// and switch back to idle
|
||||
this.state = STATE_IDLE;
|
||||
this.SelectAnimation("idle");
|
||||
if (!this.MoveIntoAttackRange())
|
||||
return;
|
||||
}
|
||||
|
||||
// Play the attack animation
|
||||
this.SelectAnimationDelayed("melee", false, 1);
|
||||
this.SelectAnimation("melee", false, 1);
|
||||
|
||||
// Hit the target
|
||||
cmpAttack.PerformAttack(data.target);
|
||||
cmpAttack.PerformAttack(this.attackTarget);
|
||||
|
||||
// Set a timer to hit the target again
|
||||
this.attackRechargeTime = cmpTimer.GetTime() + data.timers.recharge;
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", data.timers.repeat, data);
|
||||
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
|
||||
var timers = cmpAttack.GetTimers();
|
||||
this.attackRechargeTime = cmpTimer.GetTime() + timers.recharge;
|
||||
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, data);
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||
|
@ -68,8 +68,8 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
float frameTime;
|
||||
float offset;
|
||||
float frameTime; // time in seconds since previous interpolate
|
||||
float offset; // range [0, 1] (inclusive); fractional time of current frame between previous/next simulation turns
|
||||
};
|
||||
|
||||
class CMessageRenderSubmit : public CMessage
|
||||
@ -140,14 +140,21 @@ public:
|
||||
entity_angle_t a;
|
||||
};
|
||||
|
||||
class CMessageMotionStopped : public CMessage
|
||||
/**
|
||||
* Sent by CCmpUnitMotion during Update, whenever the motion status has changed
|
||||
* since the previous update.
|
||||
*/
|
||||
class CMessageMotionChanged : public CMessage
|
||||
{
|
||||
public:
|
||||
DEFAULT_MESSAGE_IMPL(MotionStopped)
|
||||
DEFAULT_MESSAGE_IMPL(MotionChanged)
|
||||
|
||||
CMessageMotionStopped()
|
||||
CMessageMotionChanged(CFixed_23_8 speed) :
|
||||
speed(speed)
|
||||
{
|
||||
}
|
||||
|
||||
CFixed_23_8 speed; // metres per second, or 0 if not moving
|
||||
};
|
||||
|
||||
#endif // INCLUDED_MESSAGETYPES
|
||||
|
@ -37,7 +37,7 @@ MESSAGE(RenderSubmit) // non-deterministic (use with caution)
|
||||
MESSAGE(Destroy)
|
||||
MESSAGE(OwnershipChanged)
|
||||
MESSAGE(PositionChanged)
|
||||
MESSAGE(MotionStopped)
|
||||
MESSAGE(MotionChanged)
|
||||
|
||||
// TemplateManager must come before all other (non-test) components,
|
||||
// so that it is the first to be (de)serialized
|
||||
@ -79,7 +79,7 @@ INTERFACE(PlayerManager)
|
||||
COMPONENT(PlayerManagerScripted)
|
||||
|
||||
INTERFACE(Position)
|
||||
COMPONENT(Position)
|
||||
COMPONENT(Position) // must be before VisualActor
|
||||
|
||||
INTERFACE(Selectable)
|
||||
COMPONENT(Selectable)
|
||||
|
@ -40,6 +40,7 @@ public:
|
||||
static void ClassInit(CComponentManager& componentManager)
|
||||
{
|
||||
componentManager.SubscribeToMessageType(MT_TurnStart);
|
||||
componentManager.SubscribeToMessageType(MT_Interpolate);
|
||||
|
||||
// TODO: if this component turns out to be a performance issue, it should
|
||||
// be optimised by creating a new PositionStatic component that doesn't subscribe
|
||||
@ -62,6 +63,7 @@ public:
|
||||
|
||||
entity_pos_t m_YOffset;
|
||||
bool m_Floating;
|
||||
float m_RotYSpeed; // maximum radians per second, used by InterpolatedRotY to follow RotY
|
||||
|
||||
// Dynamic state:
|
||||
|
||||
@ -69,6 +71,7 @@ public:
|
||||
entity_pos_t m_X, m_Z, m_LastX, m_LastZ; // these values contain undefined junk if !InWorld
|
||||
|
||||
entity_angle_t m_RotX, m_RotY, m_RotZ;
|
||||
float m_InterpolatedRotY; // not serialized
|
||||
|
||||
bool m_Dirty; // true if position/rotation has changed since last TurnStart
|
||||
|
||||
@ -110,7 +113,10 @@ public:
|
||||
m_YOffset = paramNode.GetChild("Altitude").ToFixed();
|
||||
m_Floating = paramNode.GetChild("Floating").ToBool();
|
||||
|
||||
m_RotYSpeed = 6.f; // TODO: should get from template
|
||||
|
||||
m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0);
|
||||
m_InterpolatedRotY = 0;
|
||||
|
||||
m_Dirty = false;
|
||||
}
|
||||
@ -128,6 +134,8 @@ public:
|
||||
serialize.NumberFixed_Unbounded("z", m_Z);
|
||||
serialize.NumberFixed_Unbounded("last x", m_LastX);
|
||||
serialize.NumberFixed_Unbounded("last z", m_LastZ);
|
||||
// TODO: for efficiency, we probably shouldn't actually store the last position - it doesn't
|
||||
// matter if we don't have smooth interpolation after reloading a game
|
||||
}
|
||||
serialize.NumberFixed_Unbounded("rot x", m_RotX);
|
||||
serialize.NumberFixed_Unbounded("rot y", m_RotY);
|
||||
@ -167,6 +175,8 @@ public:
|
||||
deserialize.NumberFixed_Unbounded(m_YOffset);
|
||||
deserialize.Bool(m_Dirty);
|
||||
// TODO: should there be range checks on all these values?
|
||||
|
||||
m_InterpolatedRotY = m_RotY.ToFloat();
|
||||
}
|
||||
|
||||
virtual bool IsInWorld()
|
||||
@ -239,9 +249,17 @@ public:
|
||||
return CFixedVector3D(m_X, ground + m_YOffset, m_Z);
|
||||
}
|
||||
|
||||
virtual void TurnTo(entity_angle_t y)
|
||||
{
|
||||
m_RotY = y;
|
||||
|
||||
m_Dirty = true;
|
||||
}
|
||||
|
||||
virtual void SetYRotation(entity_angle_t y)
|
||||
{
|
||||
m_RotY = y;
|
||||
m_InterpolatedRotY = m_RotY.ToFloat();
|
||||
|
||||
m_Dirty = true;
|
||||
}
|
||||
@ -286,8 +304,8 @@ public:
|
||||
|
||||
CMatrix3D m;
|
||||
CMatrix3D mXZ;
|
||||
float Cos = cosf(m_RotY.ToFloat());
|
||||
float Sin = sinf(m_RotY.ToFloat());
|
||||
float Cos = cosf(m_InterpolatedRotY);
|
||||
float Sin = sinf(m_InterpolatedRotY);
|
||||
|
||||
m.SetIdentity();
|
||||
m._11 = -Cos;
|
||||
@ -309,7 +327,26 @@ public:
|
||||
{
|
||||
switch (msg.GetType())
|
||||
{
|
||||
case MT_Interpolate:
|
||||
{
|
||||
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
|
||||
|
||||
float rotY = m_RotY.ToFloat();
|
||||
float delta = rotY - m_InterpolatedRotY;
|
||||
// Wrap delta to -PI..PI
|
||||
delta = fmod(delta + PI, 2*PI); // range -2PI..2PI
|
||||
if (delta < 0) delta += 2*PI; // range 0..2PI
|
||||
delta -= PI; // range -PI..PI
|
||||
// Clamp to max rate
|
||||
float deltaClamped = clamp(delta, -m_RotYSpeed*msgData.frameTime, +m_RotYSpeed*msgData.frameTime);
|
||||
// Calculate new orientation, in a peculiar way in order to make sure the
|
||||
// result gets close to m_orientation (rather than being n*2*PI out)
|
||||
m_InterpolatedRotY = rotY + deltaClamped - delta;
|
||||
|
||||
break;
|
||||
}
|
||||
case MT_TurnStart:
|
||||
{
|
||||
m_LastX = m_X;
|
||||
m_LastZ = m_Z;
|
||||
if (m_Dirty)
|
||||
@ -323,6 +360,7 @@ public:
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -37,19 +37,31 @@ public:
|
||||
const CSimContext* m_Context;
|
||||
|
||||
// Template state:
|
||||
|
||||
CFixed_23_8 m_Speed; // in units per second
|
||||
|
||||
// Dynamic state:
|
||||
|
||||
bool m_HasTarget;
|
||||
ICmpPathfinder::Path m_Path;
|
||||
entity_pos_t m_TargetX, m_TargetZ; // these values contain undefined junk if !HasTarget
|
||||
|
||||
enum
|
||||
{
|
||||
IDLE,
|
||||
WALKING,
|
||||
STOPPING
|
||||
};
|
||||
int m_State;
|
||||
|
||||
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
|
||||
{
|
||||
m_Context = &context;
|
||||
m_HasTarget = false;
|
||||
|
||||
m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
|
||||
|
||||
m_State = IDLE;
|
||||
}
|
||||
|
||||
virtual void Deinit(const CSimContext& UNUSED(context))
|
||||
@ -65,6 +77,8 @@ public:
|
||||
serialize.NumberFixed_Unbounded("target x", m_TargetX);
|
||||
serialize.NumberFixed_Unbounded("target z", m_TargetZ);
|
||||
}
|
||||
|
||||
// TODO: m_State
|
||||
}
|
||||
|
||||
virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& deserialize)
|
||||
@ -86,12 +100,58 @@ public:
|
||||
case MT_Update:
|
||||
{
|
||||
CFixed_23_8 dt = static_cast<const CMessageUpdate&> (msg).turnLength;
|
||||
|
||||
if (m_State == STOPPING)
|
||||
{
|
||||
CMessageMotionChanged msg(CFixed_23_8::FromInt(0));
|
||||
context.GetComponentManager().PostMessage(GetEntityId(), msg);
|
||||
m_State = IDLE;
|
||||
}
|
||||
|
||||
Move(context, dt);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchState(const CSimContext& context, int state)
|
||||
{
|
||||
debug_assert(state == IDLE || state == WALKING);
|
||||
|
||||
// IDLE -> IDLE -- no change
|
||||
// IDLE -> WALKING -- send a MotionChanged message
|
||||
// WALKING -> IDLE -- set to STOPPING, so we'll send MotionChanged in the next Update
|
||||
// WALKING -> WALKING -- no change
|
||||
// STOPPING -> IDLE -- stay in STOPPING
|
||||
// STOPPING -> WALKING -- set to WALKING, send no messages
|
||||
|
||||
if (m_State == IDLE && state == WALKING)
|
||||
{
|
||||
CMessageMotionChanged msg(m_Speed);
|
||||
context.GetComponentManager().PostMessage(GetEntityId(), msg);
|
||||
m_State = WALKING;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_State == WALKING && state == IDLE)
|
||||
{
|
||||
m_State = STOPPING;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_State == STOPPING && state == IDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_State == STOPPING && state == WALKING)
|
||||
{
|
||||
m_State = WALKING;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void MoveToPoint(entity_pos_t x, entity_pos_t z, entity_pos_t minRadius, entity_pos_t maxRadius)
|
||||
{
|
||||
CmpPtr<ICmpPathfinder> cmpPathfinder (*m_Context, SYSTEM_ENTITY);
|
||||
@ -102,6 +162,8 @@ public:
|
||||
if (cmpPosition.null())
|
||||
return;
|
||||
|
||||
SwitchState(*m_Context, WALKING);
|
||||
|
||||
CFixedVector3D pos = cmpPosition->GetPosition();
|
||||
|
||||
m_Path.m_Waypoints.clear();
|
||||
@ -126,9 +188,14 @@ public:
|
||||
|
||||
// If there's no waypoints then we've stopped already, otherwise move to the first one
|
||||
if (m_Path.m_Waypoints.empty())
|
||||
m_Context->GetComponentManager().BroadcastMessage(CMessageMotionStopped());
|
||||
{
|
||||
m_HasTarget = false;
|
||||
SwitchState(*m_Context, IDLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
PickNextWaypoint(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,8 +221,6 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
|
||||
if (cmpPosition.null())
|
||||
return;
|
||||
|
||||
CFixed_23_8 maxdist = m_Speed.Multiply(dt);
|
||||
|
||||
CFixedVector3D pos = cmpPosition->GetPosition();
|
||||
pos.Y = CFixed_23_8::FromInt(0); // remove Y so it doesn't influence our distance calculations
|
||||
|
||||
@ -168,9 +233,12 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
|
||||
|
||||
// Face towards the target
|
||||
entity_angle_t angle = atan2_approx(offset.X, offset.Z);
|
||||
cmpPosition->SetYRotation(angle);
|
||||
cmpPosition->TurnTo(angle);
|
||||
|
||||
// If it's close, we can move there directly
|
||||
// Work out how far we can travel in dt
|
||||
CFixed_23_8 maxdist = m_Speed.Multiply(dt);
|
||||
|
||||
// If the target is close, we can move there directly
|
||||
if (offset.Length() <= maxdist)
|
||||
{
|
||||
// If we've reached the last waypoint, stop
|
||||
@ -178,12 +246,12 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
|
||||
{
|
||||
cmpPosition->MoveTo(target.X, target.Z);
|
||||
m_HasTarget = false;
|
||||
context.GetComponentManager().BroadcastMessage(CMessageMotionStopped());
|
||||
SwitchState(context, IDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, spend the rest of the time heading towards the next waypoint
|
||||
dt = dt - dt.Multiply(offset.Length() / maxdist);
|
||||
dt = dt - (offset.Length() / m_Speed);
|
||||
pos = target;
|
||||
PickNextWaypoint(pos);
|
||||
continue;
|
||||
|
@ -90,6 +90,12 @@ public:
|
||||
*/
|
||||
virtual CFixedVector3D GetPosition() = 0;
|
||||
|
||||
/**
|
||||
* Rotate smoothly to the given angle around the upwards axis.
|
||||
* @param y clockwise radians from the +Z axis.
|
||||
*/
|
||||
virtual void TurnTo(entity_angle_t y) = 0;
|
||||
|
||||
/**
|
||||
* Rotate immediately to the given angle around the upwards axis.
|
||||
* @param y clockwise radians from the +Z axis.
|
||||
|
@ -154,14 +154,18 @@ CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& UNUSED(scriptInter
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
jsval CMessageMotionStopped::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
|
||||
jsval CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const
|
||||
{
|
||||
return JSVAL_VOID;
|
||||
TOJSVAL_SETUP();
|
||||
SET_MSG_PROPERTY(speed);
|
||||
return OBJECT_TO_JSVAL(obj);
|
||||
}
|
||||
|
||||
CMessage* CMessageMotionStopped::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
|
||||
CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val)
|
||||
{
|
||||
return NULL;
|
||||
FROMJSVAL_SETUP();
|
||||
GET_MSG_PROPERTY(CFixed_23_8, speed);
|
||||
return new CMessageMotionChanged(speed);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
@ -221,7 +221,8 @@ void CComponentManager::Script_RegisterComponentType(void* cbdata, int iid, std:
|
||||
std::map<std::string, MessageTypeId>::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name);
|
||||
if (mit == componentManager->m_MessageTypeIdsByName.end())
|
||||
{
|
||||
componentManager->m_ScriptInterface.ReportError("Registered component has unrecognised 'On...' message handler method"); // TODO: report the actual name
|
||||
std::string msg = "Registered component has unrecognised '" + *it + "' message handler method";
|
||||
componentManager->m_ScriptInterface.ReportError(msg.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user