diff --git a/binaries/data/mods/public/simulation/components/Sound.js b/binaries/data/mods/public/simulation/components/Sound.js index e52f1b575e..d3e8215daa 100644 --- a/binaries/data/mods/public/simulation/components/Sound.js +++ b/binaries/data/mods/public/simulation/components/Sound.js @@ -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) diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index ca3280f08b..2a9db9d0e7 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -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); diff --git a/source/graphics/Model.cpp b/source/graphics/Model.cpp index 071b977464..f5c8fa800c 100644 --- a/source/graphics/Model.cpp +++ b/source/graphics/Model.cpp @@ -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) diff --git a/source/graphics/Model.h b/source/graphics/Model.h index b0925d9b2b..e8c928d3f8 100644 --- a/source/graphics/Model.h +++ b/source/graphics/Model.h @@ -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 diff --git a/source/graphics/SkeletonAnim.h b/source/graphics/SkeletonAnim.h index 3357701627..d4959b7346 100644 --- a/source/graphics/SkeletonAnim.h +++ b/source/graphics/SkeletonAnim.h @@ -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; diff --git a/source/graphics/Unit.cpp b/source/graphics/Unit.cpp index a3e2169f7c..9228e6af3d 100644 --- a/source/graphics/Unit.cpp +++ b/source/graphics/Unit.cpp @@ -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& props = model->GetProps(); for (std::vector::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); } diff --git a/source/graphics/Unit.h b/source/graphics/Unit.h index 14ec52df14..828121c60c 100644 --- a/source/graphics/Unit.h +++ b/source/graphics/Unit.h @@ -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 diff --git a/source/graphics/UnitAnimation.cpp b/source/graphics/UnitAnimation.cpp index 738bb00940..701768b803 100644 --- a/source/graphics/UnitAnimation.cpp +++ b/source/graphics/UnitAnimation.cpp @@ -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 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); } diff --git a/source/graphics/UnitAnimation.h b/source/graphics/UnitAnimation.h index 8dc2910359..7a13b02a24 100644 --- a/source/graphics/UnitAnimation.h +++ b/source/graphics/UnitAnimation.h @@ -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 diff --git a/source/simulation/Entity.cpp b/source/simulation/Entity.cpp index c1b8fe1ae0..ab604aca66 100644 --- a/source/simulation/Entity.cpp +++ b/source/simulation/Entity.cpp @@ -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"" ); } } } diff --git a/source/simulation/EntityStateProcessing.cpp b/source/simulation/EntityStateProcessing.cpp index 7fa23f0322..6a149254ed 100644 --- a/source/simulation/EntityStateProcessing.cpp +++ b/source/simulation/EntityStateProcessing.cpp @@ -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; diff --git a/source/simulation2/components/CCmpVisualActor.cpp b/source/simulation2/components/CCmpVisualActor.cpp index be96f8311a..eae6959f62 100644 --- a/source/simulation2/components/CCmpVisualActor.cpp +++ b/source/simulation2/components/CCmpVisualActor.cpp @@ -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) diff --git a/source/simulation2/components/ICmpVisual.cpp b/source/simulation2/components/ICmpVisual.cpp index af718ee183..98809bd932 100644 --- a/source/simulation2/components/ICmpVisual.cpp +++ b/source/simulation2/components/ICmpVisual.cpp @@ -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) diff --git a/source/simulation2/components/ICmpVisual.h b/source/simulation2/components/ICmpVisual.h index 5172911505..62582ec268 100644 --- a/source/simulation2/components/ICmpVisual.h +++ b/source/simulation2/components/ICmpVisual.h @@ -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. diff --git a/source/tools/atlas/GameInterface/ActorViewer.cpp b/source/tools/atlas/GameInterface/ActorViewer.cpp index bf5b7d3ef2..bb3ac764e4 100644 --- a/source/tools/atlas/GameInterface/ActorViewer.cpp +++ b/source/tools/atlas/GameInterface/ActorViewer.cpp @@ -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 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""); } }