1
0
forked from 0ad/0ad

# Fix animation syncing.

Make animations trigger sound effects.
Adjust attack animation timings to match simulation.
Simplify the animation speed settings.

This was SVN commit r7438.
This commit is contained in:
Ykkrosh 2010-04-05 23:09:34 +00:00
parent cfca28cab0
commit 4c0d47707b
15 changed files with 281 additions and 153 deletions

View File

@ -4,6 +4,11 @@ Sound.prototype.Init = function()
{
};
Sound.prototype.GetSoundGroup = function(name)
{
return this.template.SoundGroups[name] || "";
};
Sound.prototype.PlaySoundGroup = function(name)
{
if (name in this.template.SoundGroups)

View File

@ -169,8 +169,11 @@ UnitAI.prototype.OnMotionChanged = function(msg)
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");
// Start the attack animation and sound, but synced to the timers
this.SelectAnimation("melee", false, 1.0, "attack");
this.SetAnimationSync(time, timers.repeat);
// TODO: this drifts since the sim is quantised to sim turns and these timers aren't
// TODO: we should probably only bother syncing projectile attacks, not melee
}
else if (this.state == STATE_REPAIRING)
{
@ -185,8 +188,8 @@ UnitAI.prototype.OnMotionChanged = function(msg)
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
// Start the repair/build animation
this.SelectAnimation("build");
// Start the repair/build animation and sound
this.SelectAnimation("build", false, 1.0, "build");
}
else if (this.state == STATE_GATHERING)
{
@ -207,8 +210,8 @@ UnitAI.prototype.OnMotionChanged = function(msg)
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {"typename": typename});
// Start the gather animation
this.SelectAnimation(typename);
// Start the gather animation and sound
this.SelectAnimation(typename, false, 1.0, typename);
}
}
};
@ -353,13 +356,30 @@ UnitAI.prototype.MoveIntoGatherRange = function()
// TODO: refactor all this repetitive code
UnitAI.prototype.SelectAnimation = function(name, once, speed)
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation(name, once, speed);
var soundgroup = "";
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSync(actiontime, repeattime);
};
UnitAI.prototype.MoveToTarget = function(target, range)
@ -386,12 +406,6 @@ UnitAI.prototype.AttackTimeout = function(data)
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Play the attack animation
this.SelectAnimation("melee", false, 1);
// Play the sound
// TODO: these sounds should be triggered by the animation instead
PlaySound("attack", this.entity);
// Hit the target
cmpAttack.PerformAttack(this.attackTarget);
@ -416,11 +430,7 @@ UnitAI.prototype.RepairTimeout = function(data)
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
// Play the sound
PlaySound("build", this.entity);
// Repair/build the target
// TODO: these sounds should be triggered by the animation instead
var status = cmpBuilder.PerformBuilding(this.repairTarget);
// If the target is fully built and repaired, then stop and go back to idle
@ -449,10 +459,6 @@ UnitAI.prototype.GatherTimeout = function(data)
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Play the gather_* sound
// TODO: these sounds should be triggered by the animation instead
PlaySound(data.typename, this.entity);
// Gather from the target
var status = cmpResourceGatherer.PerformGather(this.gatherTarget);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -213,18 +213,18 @@ void CModel::CalcAnimatedObjectBound(CSkeletonAnimDef* anim,CBound& result)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// BuildAnimation: load raw animation frame animation from given file, and build a
// animation specific to this model
CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const char* name, float speed, double actionpos, double actionpos2)
CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const char* name, float speed, float actionpos, float actionpos2)
{
CSkeletonAnimDef* def = m_SkeletonAnimManager.GetAnimation(pathname);
if (!def) return NULL;
if (!def)
return NULL;
CSkeletonAnim* anim=new CSkeletonAnim;
CSkeletonAnim* anim = new CSkeletonAnim();
anim->m_Name = name;
anim->m_AnimDef=def;
anim->m_Speed=speed;
anim->m_ActionPos=(float)( actionpos /* * anim->m_AnimDef->GetDuration() */ / speed );
anim->m_ActionPos2=(float)( actionpos2 /* * anim->m_AnimDef->GetDuration() */ / speed );
anim->m_AnimDef = def;
anim->m_Speed = speed;
anim->m_ActionPos = actionpos * anim->m_AnimDef->GetDuration();
anim->m_ActionPos2 = actionpos2 * anim->m_AnimDef->GetDuration();
anim->m_ObjectBounds.SetEmpty();
InvalidateBounds();
@ -233,21 +233,18 @@ CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const char* name,
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Update: update this model by the given time, in seconds
// Update: update this model by the given time, in msec
void CModel::Update(float time)
{
if (m_Anim && m_Anim->m_AnimDef && m_BoneMatrices)
{
// adjust for animation speed
float animTimeDelta = time*m_AnimSpeed;
float oldAnimTime = m_AnimTime;
// update animation time, but don't calculate bone matrices - do that (lazily) when
// something requests them; that saves some calculation work for offscreen models,
// and also assures the world space, inverted bone matrices (required for normal
// skinning) are up to date with respect to m_Transform
m_AnimTime += animTimeDelta;
m_AnimTime += time;
float duration = m_Anim->m_AnimDef->GetDuration();
if (m_AnimTime > duration)
@ -291,17 +288,26 @@ bool CModel::NeedsNewAnim(float time) const
if (m_Anim && m_Anim->m_AnimDef && m_BoneMatrices)
{
// adjust for animation speed
float animtime = time * m_AnimSpeed;
float duration = m_Anim->m_AnimDef->GetDuration();
if (m_AnimTime + animtime > duration)
if (m_AnimTime + time > duration)
return true;
}
return false;
}
void CModel::CheckActionTriggers(float time, bool& action, bool& action2) const
{
if (m_Anim && m_Anim->m_AnimDef && m_BoneMatrices)
{
if (m_AnimTime <= m_Anim->m_ActionPos && m_AnimTime + time > m_Anim->m_ActionPos)
action = true;
if (m_AnimTime <= m_Anim->m_ActionPos2 && m_AnimTime + time > m_Anim->m_ActionPos2)
action2 = true;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// InvalidatePosition
void CModel::InvalidatePosition()
@ -369,7 +375,7 @@ void CModel::ValidatePosition()
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SetAnimation: set the given animation as the current animation on this model;
// return false on error, else true
bool CModel::SetAnimation(CSkeletonAnim* anim, bool once, float speed, CSkeletonAnim* next)
bool CModel::SetAnimation(CSkeletonAnim* anim, bool once, CSkeletonAnim* next)
{
m_Anim=NULL; // in case something fails
@ -405,10 +411,7 @@ bool CModel::SetAnimation(CSkeletonAnim* anim, bool once, float speed, CSkeleton
InvalidateBounds();
// start anim from beginning
m_AnimTime=0;
// Adjust speed by animation base rate.
m_AnimSpeed = speed * anim->m_Speed;
m_AnimTime = 0;
}
m_Anim = anim;
@ -423,7 +426,6 @@ void CModel::CopyAnimationFrom(CModel* source)
m_Anim = source->m_Anim;
m_NextAnim = source->m_NextAnim;
m_AnimTime = source->m_AnimTime;
m_AnimSpeed = source->m_AnimSpeed;
m_Flags &= ~MODELFLAG_CASTSHADOWS;
if (source->m_Flags & MODELFLAG_CASTSHADOWS)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -73,6 +73,9 @@ public:
// returns true if Update(time) will require a new animation (due to the
// current one ending)
bool NeedsNewAnim(float time) const;
// sets the bool& arguments if the given time update will cross the trigger points for those actions;
// otherwise leaves the arguments unchanged
void CheckActionTriggers(float time, bool& action, bool& action2) const;
// get the model's geometry data
CModelDefPtr GetModelDef() { return m_pModelDef; }
@ -96,7 +99,7 @@ public:
CColor GetShadingColor() { return m_ShadingColor; }
// set the given animation as the current animation on this model
bool SetAnimation(CSkeletonAnim* anim, bool once = false, float speed = 1000.0f, CSkeletonAnim* next = NULL);
bool SetAnimation(CSkeletonAnim* anim, bool once = false, CSkeletonAnim* next = NULL);
// get the currently playing animation, if any
CSkeletonAnim* GetAnimation() const { return m_Anim; }
@ -146,9 +149,17 @@ public:
return m_InverseBindBoneMatrices;
}
// load raw animation frame animation from given file, and build a
// animation specific to this model
CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const char* name, float speed, double actionpos, double actionpos2);
/**
* Load raw animation frame animation from given file, and build an
* animation specific to this model.
* @param pathname animation file to load
* @param name animation name (e.g. "idle")
* @param speed animation speed as a factor of the default animation speed
* @param actionpos offset of 'action' event, in range [0, 1]
* @param actionpos2 offset of 'action2' event, in range [0, 1]
* @return new animation, or NULL on error
*/
CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const char* name, float speed, float actionpos, float actionpos2);
// add a prop to the model on the given point
void AddProp(SPropPoint* point, CModel* model, CObjectEntry* objectentry);
@ -199,8 +210,6 @@ private:
CSkeletonAnim* m_Anim;
// animation to switch back to when the current one finishes
CSkeletonAnim* m_NextAnim;
// rate at which the animation should play
float m_AnimSpeed;
// time (in MS) into the current animation
float m_AnimTime;
// current state of all bones on this model; null if associated modeldef isn't skeletal

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -34,12 +34,13 @@ class CSkeletonAnim
public:
// the name of the action which uses this animation (e.g. "idle")
CStr m_Name;
// the raw animation frame data
// the raw animation frame data; may be NULL if this is a static 'animation'
CSkeletonAnimDef* m_AnimDef;
// speed at which this animation runs
// speed at which this animation runs, as a factor of the AnimDef default speed
// (treated as 0 if m_AnimDef == NULL)
float m_Speed;
// Times during the animation at which the interesting bits happen. Measured
// as fractions (0..1) of the total animation length.
// Times during the animation at which the interesting bits happen,
// as msec times in the range [0, AnimDef->GetDuration].
// ActionPos is used for melee hits, projectile launches, etc.
// ActionPos2 is used for loading projectile ammunition.
float m_ActionPos;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -112,22 +112,19 @@ static CSkeletonAnim* GetRandomAnimation(const CStr& name, CObjectEntry* object)
return anim;
}
static bool SetRandomAnimation(const CStr& name, bool once, float speed,
static bool SetRandomAnimation(const CStr& name, bool once,
CModel* model, CObjectEntry* object)
{
CSkeletonAnim* anim = GetRandomAnimation(name, object);
if (anim)
{
float actualSpeed = 1000.f;
if (speed && anim->m_AnimDef)
actualSpeed = speed * anim->m_AnimDef->GetDuration();
model->SetAnimation(anim, once, actualSpeed);
model->SetAnimation(anim, once);
// Recursively apply the animation name to props
const std::vector<CModel::Prop>& props = model->GetProps();
for (std::vector<CModel::Prop>::const_iterator it = props.begin(); it != props.end(); ++it)
{
bool ok = SetRandomAnimation(name, once, speed, it->m_Model, it->m_ObjectEntry);
bool ok = SetRandomAnimation(name, once, it->m_Model, it->m_ObjectEntry);
if (! ok)
return false;
}
@ -144,9 +141,9 @@ static bool SetRandomAnimation(const CStr& name, bool once, float speed,
bool CUnit::SetRandomAnimation(const CStr& name, bool once, float speed)
bool CUnit::SetRandomAnimation(const CStr& name, bool once)
{
return ::SetRandomAnimation(name, once, speed, m_Model, m_Object);
return ::SetRandomAnimation(name, once, m_Model, m_Object);
}
CSkeletonAnim* CUnit::GetRandomAnimation(const CStr& name)
@ -164,19 +161,19 @@ bool CUnit::IsPlayingAnimation(const CStr& name)
return (m_Model->GetAnimation() && m_Model->GetAnimation()->m_Name == name);
}
void CUnit::SetAnimationState(const CStr& name, bool once, float speed, bool keepSelection)
void CUnit::SetAnimationState(const CStr& name, bool once, float speed, float desync, bool keepSelection, const CStrW& soundgroup)
{
m_Animation->SetAnimationState(name, once, speed, keepSelection);
m_Animation->SetAnimationState(name, once, speed, desync, keepSelection, soundgroup);
}
void CUnit::SetAnimationSync(float timeUntilActionPos)
void CUnit::SetAnimationSync(float actionTime, float repeatTime)
{
m_Animation->SetAnimationSync(timeUntilActionPos);
m_Animation->SetAnimationSync(actionTime, repeatTime);
}
void CUnit::UpdateModel(float frameTime)
{
m_Animation->Update(frameTime);
m_Animation->Update(frameTime*1000.0f);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -68,19 +68,18 @@ public:
void ShowAmmunition();
void HideAmmunition();
// Sets the animation a random one matching 'name'. If none is found,
// sets to idle instead. Applies recursively to props.
// SetEntitySelection(name) should typically be used before this.
bool SetRandomAnimation(const CStr& name, bool once = false, float speed = 0.0f);
/// See CUnitAnimation::SetAnimationState
void SetAnimationState(const CStr& name, bool once, float speed, float desync, bool keepSelection, const CStrW& soundgroup);
void SetAnimationState(const CStr& name, bool once = false, float speed = 0.0f, bool keepSelection = false);
void SetAnimationSync(float timeUntilActionPos);
/// See CUnitAnimation::SetAnimationSync
void SetAnimationSync(float actionTime, float repeatTime);
/**
* Update the model's animation.
* @param frameTime time in seconds
*/
void UpdateModel(float frameTime);
// Returns a random animation matching 'name'. If none is found,
// returns idle instead.
CSkeletonAnim* GetRandomAnimation(const CStr& name);
bool HasAnimation(const CStr& name);
// Sets the entity-selection, and updates the unit to use the new
@ -131,7 +130,18 @@ private:
// object manager which looks after this unit's objectentry
CObjectManager& m_ObjectManager;
// Sets the animation a random one matching 'name'. If none is found,
// sets to idle instead. Applies recursively to props.
// SetEntitySelection(name) should typically be used before this.
bool SetRandomAnimation(const CStr& name, bool once = false);
// Returns a random animation matching 'name'. If none is found,
// returns idle instead.
CSkeletonAnim* GetRandomAnimation(const CStr& name);
void ReloadObject();
friend class CUnitAnimation;
};
#endif

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -23,93 +23,121 @@
#include "graphics/SkeletonAnim.h"
#include "graphics/SkeletonAnimDef.h"
#include "graphics/Unit.h"
#include "lib/rand.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpSoundManager.h"
namespace
// Randomly modify the speed, so that units won't stay perfectly
// synchronised if they're playing animations of the same length
static float DesyncSpeed(float speed, float desync)
{
// Randomly modify the speed, so that units won't stay perfectly
// synchronised if they're playing animations of the same length
float DesyncSpeed(float speed)
{
// const float var = 0.05f; // max fractional variation from default
// return speed * (1.f - var + 2.f*var*(rand(0, 256)/255.f));
// TODO: enable this desyncing for cases where we don't care about
// accurate looping, and just don't do it for e.g. projectile-launchers
// where we do care
if (desync == 0.0f)
return speed;
}
return speed * (1.f - desync + 2.f*desync*(rand(0, 256)/255.f));
}
CUnitAnimation::CUnitAnimation(CUnit& unit)
: m_Unit(unit), m_State("idle"), m_Looping(true), m_Speed(0.f), m_OriginalSpeed(0.f), m_TimeToNextSync(0.f)
: m_Unit(unit), m_State("idle"), m_Looping(true), m_Speed(1.f), m_OriginalSpeed(1.f), m_Desync(0.f)
{
}
void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, bool keepSelection)
void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, float desync, bool keepSelection, const CStrW& actionSound)
{
if (name == m_State)
return;
m_State = name;
m_Looping = !once;
m_Speed = m_OriginalSpeed = speed;
m_TimeToNextSync = 0.f;
m_OriginalSpeed = speed;
m_Desync = desync;
m_ActionSound = actionSound;
m_Speed = DesyncSpeed(m_OriginalSpeed, m_Desync);
if (! keepSelection)
m_Unit.SetEntitySelection(name);
m_Unit.SetRandomAnimation(m_State, !m_Looping, DesyncSpeed(m_Speed));
m_Unit.SetRandomAnimation(m_State, !m_Looping);
}
void CUnitAnimation::SetAnimationSync(float timeUntilActionPos)
void CUnitAnimation::SetAnimationSync(float actionTime, float repeatTime)
{
// We need to finish looping our animation at the specified time from now.
// Assume it's playing at nearly the right speed, and we just need to perhaps
// shift it a little bit to stay in sync.
m_TimeToNextSync = timeUntilActionPos;
CModel* model = m_Unit.GetModel();
float duration = model->m_Anim->m_AnimDef->GetDuration();
// Calculate the required playback speed so ActionPos coincides with timeUntilActionPos
float currentPos = model->m_AnimTime / model->m_Anim->m_AnimDef->GetDuration();
float length = (model->m_Anim->m_ActionPos - currentPos);
if (length < 0.f)
length += 1.f;
float requiredSpeed = length / m_TimeToNextSync;
// Set the speed so it loops once in repeatTime
float speed = duration / repeatTime;
// Shift in the right direction
if (requiredSpeed > m_OriginalSpeed)
m_Speed = std::min(requiredSpeed, m_OriginalSpeed*1.1f);
else if (requiredSpeed < m_OriginalSpeed)
m_Speed = std::max(requiredSpeed, m_OriginalSpeed*0.9f);
// Need to offset so that start+actionTime*speed = ActionPos
float start = model->m_Anim->m_ActionPos - actionTime*speed;
// Wrap it so that it's within the animation
start = fmodf(start, duration);
if (start < 0)
start += duration;
model->m_AnimSpeed = m_Speed * model->m_Anim->m_AnimDef->GetDuration() * model->m_Anim->m_Speed;
// TODO: this should use the ActionPos2, instead of totally ignoring it
m_Unit.ShowAmmunition();
// Make the animation run at the computed timings
model->m_AnimTime = start;
m_Speed = m_OriginalSpeed = speed;
m_Desync = 0.f;
}
void CUnitAnimation::Update(float time)
{
CModel* model = m_Unit.GetModel();
// Convert from real time to scaled animation time
float advance = time*m_Speed;
// Check if the current animation will trigger the action events
bool action = false;
bool action2 = false;
m_Unit.GetModel()->CheckActionTriggers(advance, action, action2);
// Choose a new random animation if we're going to loop
if (m_Looping && model->NeedsNewAnim(time))
{
m_Unit.SetRandomAnimation(m_State, !m_Looping, DesyncSpeed(m_Speed));
// TODO: this really ought to transition smoothly into the new animation,
// Re-desynchronise the animation, to keep the irregular drifting between separate units
m_Speed = DesyncSpeed(m_OriginalSpeed, m_Desync);
advance = time*m_Speed;
// TODO: should subtract time remaining in previous animation from advance, to be a bit more precise
m_Unit.SetRandomAnimation(m_State, false);
// TODO: this really ought to interpolate smoothly into the new animation,
// instead of just cutting off the end of the previous one and jumping
// straight into the new.
// Check if the start of the new animation will trigger the action events
// (This is additive with the previous CheckActionTriggers)
m_Unit.GetModel()->CheckActionTriggers(advance, action, action2);
}
if (m_TimeToNextSync >= 0.0 && m_TimeToNextSync-time < 0.0)
// TODO: props should get a new random animation once they loop, independent
// of the object they're propped onto, if we ever bother with animated props
// If we're going to advance past the action point in this update, then perform the action
if (action)
{
m_Unit.HideAmmunition();
m_TimeToNextSync -= time;
if (!m_ActionSound.empty())
{
CmpPtr<ICmpSoundManager> cmpSoundManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
if (!cmpSoundManager.null())
cmpSoundManager->PlaySoundGroup(m_ActionSound, m_Unit.GetID());
}
}
// TODO: props should get a new random animation once they loop, independent
// of the object they're propped onto
// Ditto for the second action point
if (action2)
{
m_Unit.ShowAmmunition();
}
model->Update(time);
// TODO: some animations (e.g. wood chopping) have two action points that should trigger sounds,
// so we ought to support that somehow
model->Update(advance);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -22,25 +22,61 @@
class CUnit;
/**
* Deals with synchronisation issues between raw animation data (CModel, CSkeletonAnim)
* and the simulation system (via CUnit), providing a simple fire-and-forget API to play animations.
* (This is really just a component of CUnit and could probably be merged back into that class.)
*/
class CUnitAnimation
{
NONCOPYABLE(CUnitAnimation);
public:
/**
* Construct for a given unit, defaulting to the "idle" animation.
*/
CUnitAnimation(CUnit& unit);
// (All times are measured in seconds)
/**
* Start playing an animation.
* The unit's actor defines the available animations, and if more than one is available
* then one is picked at random (with a new random choice each loop).
* By default, animations start immediately and run at the given speed with no syncing.
* Use SetAnimationSync after this to force a specific timing, if it needs to match the
* simulation timing.
* Alternatively, set @p desync to a non-zero value (e.g. 0.05) to slightly randomise the
* offset and speed, so units don't all move in lockstep.
* @param name animation's name ("idle", "walk", etc)
* @param once if true then the animation freezes on its last frame; otherwise it loops
* @param speed fraction of actor-defined speed to play back at (should typically be 1.0)
* @param desync maximum fraction of length/speed to randomly adjust timings (or 0.0 for no desyncing)
* @param keepSelection if false then the random actor variation will have the selection @p name added
* @param actionSound sound group name to be played at the 'action' point in the animation, or empty string
*/
void SetAnimationState(const CStr& name, bool once, float speed, float desync, bool keepSelection, const CStrW& actionSound);
void SetAnimationState(const CStr& name, bool once, float speed, bool keepSelection);
void SetAnimationSync(float timeUntilActionPos);
/**
* Adjust the timing of the current animation, so that Update(actionTime) will advance it
* to the 'action' point defined in the actor, and so that Update(repeatTime) will do a
* complete animation loop.
* @param actionTime time between now and when the action should occur, in msec
* @param repeatTime time for complete loop of animation, in msec
*/
void SetAnimationSync(float actionTime, float repeatTime);
/**
* Advance the animation state.
* @param time advance time in msec
*/
void Update(float time);
private:
CUnit& m_Unit;
CStr m_State;
bool m_Looping;
float m_Speed;
float m_OriginalSpeed;
float m_TimeToNextSync;
float m_Speed;
float m_Desync;
CStrW m_ActionSound;
};
#endif // INCLUDED_UNITANIMATION

View File

@ -331,7 +331,7 @@ void CEntity::Kill(bool keepActor)
// Play death animation and keep the actor in the game in a dead state
// (TODO: remove the actor after some time through some kind of fading mechanism)
m_actor->SetAnimationState( "death", true );
m_actor->SetAnimationState( "death", true, 1.f, 0.f, false, L"" );
}
else
{
@ -629,7 +629,7 @@ void CEntity::UpdateOrders( int timestep )
{
if( ( m_lastState != -1 ) || !m_actor->GetModel()->GetAnimation() )
{
m_actor->SetAnimationState( "idle" );
m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" );
}
}
}

View File

@ -93,7 +93,7 @@ float CEntity::ChooseMovementSpeed( float distance )
{
if ( !m_actor->IsPlayingAnimation( anim_name ) )
{
m_actor->SetAnimationState( anim_name, false, speed );
m_actor->SetAnimationState( anim_name, false, speed, 0.f, false, L"" );
// Animation desync
m_actor->GetModel()->Update( rand( 0, 1000 ) / 1000.0f );
@ -518,7 +518,7 @@ bool CEntity::ProcessContactActionNoPathing( CEntityOrder* current, int timestep
// Cancel current order
entf_clear(ENTF_IS_RUNNING);
entf_clear(ENTF_SHOULD_RUN);
m_actor->SetAnimationState( "idle" );
m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" );
PopOrder();
if( m_orderQueue.empty() && target.IsValid() )
{
@ -571,7 +571,7 @@ bool CEntity::ProcessContactActionNoPathing( CEntityOrder* current, int timestep
if( current->m_source == CEntityOrder::SOURCE_UNIT_AI && !m_stance->AllowsMovement() )
{
PopOrder();
m_actor->SetAnimationState( "idle" );
m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" );
return false; // We're not allowed to move at all by the current stance
}
@ -581,7 +581,7 @@ bool CEntity::ProcessContactActionNoPathing( CEntityOrder* current, int timestep
// The pathfinder will push its result in front of the current order
if( !g_Pathfinder.RequestAvoidPath( me, current, action->m_MinRange + 2.0f ) )
{
m_actor->SetAnimationState( "idle" ); // Nothing we can do.. maybe we'll find a better target
m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" ); // Nothing we can do.. maybe we'll find a better target
PopOrder();
}
@ -664,8 +664,8 @@ bool CEntity::ProcessContactActionNoPathing( CEntityOrder* current, int timestep
entf_clear(ENTF_IS_RUNNING);
}
m_actor->SetAnimationState( animation, false, 1000.f / (float)action->m_Speed );
m_actor->SetAnimationSync( (float)( action->m_Speed / 2) / 1000.f );
m_actor->SetAnimationState( animation, false, 1000.f / (float)action->m_Speed, 0.f, false, L"" );
// m_actor->SetAnimationSync( (float)( action->m_Speed / 2) / 1000.f );
m_fsm_cyclepos = 0;

View File

@ -53,6 +53,7 @@ public:
std::string m_AnimName;
bool m_AnimOnce;
float m_AnimSpeed;
std::wstring m_SoundGroup;
CCmpVisualActor() :
m_Unit(NULL)
@ -80,7 +81,7 @@ public:
return;
}
SelectAnimation("idle", false, 0.f);
SelectAnimation("idle", false, 0.f, L"");
m_Unit->SetID(GetEntityId()); // TODO: is it safe to be using entity IDs for unit IDs?
}
@ -163,19 +164,30 @@ public:
return m_Unit->GetModel()->GetTransform().GetTranslation();
}
virtual void SelectAnimation(std::string name, bool once, float speed)
virtual void SelectAnimation(std::string name, bool once, float speed, std::wstring soundgroup)
{
if (!m_Unit)
return;
if (!isfinite(speed) || speed < 0) // JS 'undefined' converts to NaN, which causes Bad Things
speed = 0;
speed = 1.f;
float desync = 0.05f; // TODO: make this an argument
m_AnimName = name;
m_AnimOnce = once;
m_AnimSpeed = speed;
m_SoundGroup = soundgroup;
m_Unit->SetAnimationState(name, once, speed);
m_Unit->SetAnimationState(name, once, speed, desync, false, soundgroup.c_str());
}
virtual void SetAnimationSync(float actiontime, float repeattime)
{
if (!m_Unit)
return;
m_Unit->SetAnimationSync(actiontime, repeattime);
}
virtual void SetShadingColour(CFixed_23_8 r, CFixed_23_8 g, CFixed_23_8 b, CFixed_23_8 a)

View File

@ -22,6 +22,7 @@
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Visual)
DEFINE_INTERFACE_METHOD_3("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, float)
DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, float, std::wstring)
DEFINE_INTERFACE_METHOD_2("SetAnimationSync", void, ICmpVisual, SetAnimationSync, float, float)
DEFINE_INTERFACE_METHOD_4("SetShadingColour", void, ICmpVisual, SetShadingColour, CFixed_23_8, CFixed_23_8, CFixed_23_8, CFixed_23_8)
END_INTERFACE_WRAPPER(Visual)

View File

@ -44,11 +44,22 @@ public:
/**
* Start playing the given animation. If there are multiple possible animations then it will
* pick one at random (not network-synchronised).
* If @p soundgroup is specified, then the sound will be played at each 'event' point in the
* animation cycle.
* @param name animation name (e.g. "idle", "walk", "melee"; the names are determined by actor XML files)
* @param once if true then the animation will play once and freeze at the final frame, else it will loop
* @param speed some kind of animation speed multiplier (TODO: work out exactly what the scale is)
* @param speed animation speed multiplier (typically 1.0 for the default speed)
* @param soundgroup VFS path of sound group .xml, relative to audio/, or empty string for none
*/
virtual void SelectAnimation(std::string name, bool once, float speed) = 0;
virtual void SelectAnimation(std::string name, bool once, float speed, std::wstring soundgroup) = 0;
/**
* Adjust the timing (offset and speed) of the current animation, so it can match
* simulation events.
* @param actiontime time between now and when the 'action' event should occur, in msec
* @param repeattime time for complete loop of animation, in msec
*/
virtual void SetAnimationSync(float actiontime, float repeattime) = 0;
/**
* Set the shading colour that will be modulated with the model's textures.

View File

@ -183,18 +183,28 @@ void ActorViewer::SetActor(const CStrW& name, const CStrW& animation)
// same for all units. We ought to get it from the entity definition
// (if there is one)
if (anim == "walk")
{
speed = 7.f;
m.CurrentSpeed = speed;
}
else if (anim == "run")
{
speed = 12.f;
m.CurrentSpeed = speed;
}
else
speed = 0.f;
m.CurrentSpeed = speed;
{
// Play the animation at normal speed, but movement speed is zero
speed = 1.f;
m.CurrentSpeed = 0.f;
}
CmpPtr<ICmpVisual> cmpVisual(m.Simulation2, m.Entity);
if (!cmpVisual.null())
{
// TODO: SetEntitySelection(anim)
cmpVisual->SelectAnimation(anim, false, speed);
// TODO: maybe we could play soundgroups from entities in here, to help test timings?
cmpVisual->SelectAnimation(anim, false, speed, L"");
}
}