diff --git a/binaries/data/mods/public/art/actors/actor.rnc b/binaries/data/mods/public/art/actors/actor.rnc index 88cf832a8f..7c046a233e 100644 --- a/binaries/data/mods/public/art/actors/actor.rnc +++ b/binaries/data/mods/public/art/actors/actor.rnc @@ -38,6 +38,7 @@ element actor { element animations { element animation { attribute name { text } & + attribute frequency { xsd:nonNegativeInteger }? & attribute file { text }? & attribute speed { xsd:nonNegativeInteger } & attribute event { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? & diff --git a/binaries/data/mods/public/art/actors/actor.rng b/binaries/data/mods/public/art/actors/actor.rng index a94bc4ef5e..f689a3a06f 100644 --- a/binaries/data/mods/public/art/actors/actor.rng +++ b/binaries/data/mods/public/art/actors/actor.rng @@ -93,6 +93,11 @@ + + + + + diff --git a/binaries/data/mods/public/art/variants/variant.rnc b/binaries/data/mods/public/art/variants/variant.rnc index 3f445305b5..1c04a2364e 100644 --- a/binaries/data/mods/public/art/variants/variant.rnc +++ b/binaries/data/mods/public/art/variants/variant.rnc @@ -34,6 +34,7 @@ element variant { element animations { element animation { attribute name { text } & + attribute frequency { xsd:nonNegativeInteger }? & attribute file { text }? & attribute speed { xsd:nonNegativeInteger } & attribute event { xsd:decimal { minInclusive = "0" maxInclusive = "1" } }? & diff --git a/binaries/data/mods/public/art/variants/variant.rng b/binaries/data/mods/public/art/variants/variant.rng index d9e88d11a2..f7cdb7a95a 100644 --- a/binaries/data/mods/public/art/variants/variant.rng +++ b/binaries/data/mods/public/art/variants/variant.rng @@ -82,6 +82,11 @@ NOTE: To modify this Relax NG grammar, edit the Relax NG Compact (.rnc) file + + + + + diff --git a/source/graphics/Model.cpp b/source/graphics/Model.cpp index da8d3c1682..d85f512923 100644 --- a/source/graphics/Model.cpp +++ b/source/graphics/Model.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -255,7 +255,7 @@ const CBoundingBoxAligned CModel::GetObjectSelectionBoundsRec() ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // BuildAnimation: load raw animation frame animation from given file, and build a // animation specific to this model -CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, float speed, float actionpos, float actionpos2, float soundpos) +CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, int frequency, float speed, float actionpos, float actionpos2, float soundpos) { CSkeletonAnimDef* def = m_SkeletonAnimManager.GetAnimation(pathname); if (!def) @@ -263,6 +263,7 @@ CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, CSkeletonAnim* anim = new CSkeletonAnim(); anim->m_Name = name; + anim->m_Frequency = frequency; anim->m_AnimDef = def; anim->m_Speed = speed; diff --git a/source/graphics/Model.h b/source/graphics/Model.h index 6f513cd928..dcdcf7cbf8 100644 --- a/source/graphics/Model.h +++ b/source/graphics/Model.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -201,13 +201,14 @@ public: * animation specific to this model. * @param pathname animation file to load * @param name animation name (e.g. "idle") + * @param frequency influences the random choices * @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] * @param sound offset of 'sound' event, in range [0, 1] * @return new animation, or NULL on error */ - CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const CStr& name, float speed, float actionpos, float actionpos2, float soundpos); + CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const CStr& name, int frequency, float speed, float actionpos, float actionpos2, float soundpos); /** * Add a prop to the model on the given point. diff --git a/source/graphics/ObjectBase.cpp b/source/graphics/ObjectBase.cpp index b79e5d0c4d..860615f80f 100644 --- a/source/graphics/ObjectBase.cpp +++ b/source/graphics/ObjectBase.cpp @@ -163,6 +163,8 @@ void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& vari { if (ae.Name == at_name) anim.m_AnimName = ae.Value; + else if (ae.Name == at_frequency) + anim.m_Frequency = ae.Value.ToInt(); else if (ae.Name == at_file) anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8(); else if (ae.Name == at_speed) diff --git a/source/graphics/ObjectBase.h b/source/graphics/ObjectBase.h index 6cc19080df..5adb307f69 100644 --- a/source/graphics/ObjectBase.h +++ b/source/graphics/ObjectBase.h @@ -42,9 +42,10 @@ public: struct Anim { // constructor - Anim() : m_Speed(1.f), m_ActionPos(-1.f), m_ActionPos2(-1.f), m_SoundPos(-1.f) {} + Anim() : m_Frequency(0), m_Speed(1.f), m_ActionPos(-1.f), m_ActionPos2(-1.f), m_SoundPos(-1.f) {} // name of the animation - "Idle", "Run", etc CStr m_AnimName; + int m_Frequency; // filename of the animation - manidle.psa, manrun.psa, etc VfsPath m_FileName; // animation speed, as specified in XML actor file diff --git a/source/graphics/ObjectEntry.cpp b/source/graphics/ObjectEntry.cpp index eae43a9bda..05e3203d31 100644 --- a/source/graphics/ObjectEntry.cpp +++ b/source/graphics/ObjectEntry.cpp @@ -161,7 +161,7 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections if (!it->second.m_FileName.empty()) { - CSkeletonAnim* anim = model->BuildAnimation(it->second.m_FileName, name, it->second.m_Speed, it->second.m_ActionPos, it->second.m_ActionPos2, it->second.m_SoundPos); + CSkeletonAnim* anim = model->BuildAnimation(it->second.m_FileName, name, it->second.m_Frequency, it->second.m_Speed, it->second.m_ActionPos, it->second.m_ActionPos2, it->second.m_SoundPos); if (anim) m_Animations.insert(std::make_pair(name, anim)); } @@ -248,15 +248,24 @@ bool CObjectEntry::BuildVariation(const std::vector >& selections CSkeletonAnim* CObjectEntry::GetRandomAnimation(const CStr& animationName) const { - SkeletonAnimMap::const_iterator lower = m_Animations.lower_bound(animationName); - SkeletonAnimMap::const_iterator upper = m_Animations.upper_bound(animationName); - size_t count = std::distance(lower, upper); - if (count == 0) - return NULL; + std::vector anims = GetAnimations(animationName); - size_t id = rand(0, count); - std::advance(lower, id); - return lower->second; + int totalFreq = 0; + for (CSkeletonAnim* anim : anims) + totalFreq += anim->m_Frequency; + + if (totalFreq == 0) + return anims[rand(0, anims.size())]; + + int r = rand(0, totalFreq); + for (CSkeletonAnim* anim : anims) + { + r -= anim->m_Frequency; + if (r < 0) + return anim; + } + LOGERROR("No animation found"); + return NULL; } std::vector CObjectEntry::GetAnimations(const CStr& animationName) const @@ -265,7 +274,23 @@ std::vector CObjectEntry::GetAnimations(const CStr& animationNam SkeletonAnimMap::const_iterator lower = m_Animations.lower_bound(animationName); SkeletonAnimMap::const_iterator upper = m_Animations.upper_bound(animationName); + for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it) anims.push_back(it->second); + + if (anims.size() == 0) + for (const std::pair& anim : m_Animations) + if (anim.second->m_Frequency > 0) + anims.push_back(anim.second); + + if (anims.size() == 0) + { + SkeletonAnimMap::const_iterator lower = m_Animations.lower_bound("idle"); + SkeletonAnimMap::const_iterator upper = m_Animations.upper_bound("idle"); + for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it) + anims.push_back(it->second); + } + + ENSURE(anims.size() > 0); return anims; } diff --git a/source/graphics/ObjectEntry.h b/source/graphics/ObjectEntry.h index 00451914bd..da266c6343 100644 --- a/source/graphics/ObjectEntry.h +++ b/source/graphics/ObjectEntry.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -63,11 +63,20 @@ public: std::wstring m_ProjectileModelName; - // Returns a randomly-chosen animation matching the given name. - // If none is found, returns NULL. + /** + * Returns a randomly-chosen animation matching the given name. + * The chosen animation is picked randomly from the GetAnimations list + * with the frequencies as weights (if there are any defined). + * This method should always return an animation + */ CSkeletonAnim* GetRandomAnimation(const CStr& animationName) const; - // Returns all the animations matching the given name. + /** + * Returns all the animations matching the given name. + * - Prefers the animations names like the animationName + * - Second choice are animations with a frequency + * - Last choice are the Idle animations (which are always added) + */ std::vector GetAnimations(const CStr& animationName) const; // corresponding model diff --git a/source/graphics/SkeletonAnim.h b/source/graphics/SkeletonAnim.h index 13f6d586e8..9713143e09 100644 --- a/source/graphics/SkeletonAnim.h +++ b/source/graphics/SkeletonAnim.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -34,6 +34,8 @@ class CSkeletonAnim public: // the name of the action which uses this animation (e.g. "idle") CStr m_Name; + // frequency of the animation + int m_Frequency; // the raw animation frame data; may be NULL if this is a static 'animation' CSkeletonAnimDef* m_AnimDef; // speed at which this animation runs, as a factor of the AnimDef default speed diff --git a/source/graphics/UnitAnimation.cpp b/source/graphics/UnitAnimation.cpp index ee8dac54cd..3a1e3b5a03 100644 --- a/source/graphics/UnitAnimation.cpp +++ b/source/graphics/UnitAnimation.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -41,7 +41,7 @@ static float DesyncSpeed(float speed, float desync) } CUnitAnimation::CUnitAnimation(entity_id_t ent, CModel* model, CObjectEntry* object) - : m_Entity(ent), m_State("idle"), m_Looping(true), + : m_Entity(ent), m_State("idle"), m_AnimationName("idle"), m_Looping(true), m_Speed(1.f), m_SyncRepeatTime(0.f), m_OriginalSpeed(1.f), m_Desync(0.f) { ReloadUnit(model, object); @@ -56,26 +56,23 @@ void CUnitAnimation::AddModel(CModel* model, const CObjectEntry* object) { SModelAnimState state; - state.anims = object->GetAnimations(m_State); - - if (state.anims.empty()) - state.anims = object->GetAnimations("idle"); - ENSURE(!state.anims.empty()); // there must always be an idle animation - state.model = model; - state.animIdx = rand(0, state.anims.size()); + state.object = object; + state.anim = object->GetRandomAnimation(m_State); state.time = 0.f; state.pastLoadPos = false; state.pastActionPos = false; state.pastSoundPos = false; + ENSURE(state.anim != NULL); // there must always be an idle animation + m_AnimStates.push_back(state); - model->SetAnimation(state.anims[state.animIdx], !m_Looping); + model->SetAnimation(state.anim, !m_Looping); // Detect if this unit has any non-static animations - for (size_t i = 0; i < state.anims.size(); i++) - if (state.anims[i]->m_AnimDef != NULL) + for (CSkeletonAnim* anim : object->GetAnimations(m_State)) + if (anim->m_AnimDef != NULL) m_AnimStatesAreStatic = false; // Recursively add all props @@ -111,6 +108,7 @@ void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, if (name != m_State) { m_State = name; + m_AnimationName = name; ReloadUnit(m_Model, m_Object); } @@ -129,13 +127,13 @@ void CUnitAnimation::SetAnimationSyncOffset(float actionTime) // Update all the synced prop models to each coincide with actionTime for (std::vector::iterator it = m_AnimStates.begin(); it != m_AnimStates.end(); ++it) { - CSkeletonAnimDef* animDef = it->anims[it->animIdx]->m_AnimDef; + CSkeletonAnimDef* animDef = it->anim->m_AnimDef; if (animDef == NULL) continue; // ignore static animations float duration = animDef->GetDuration(); - float actionPos = it->anims[it->animIdx]->m_ActionPos; + float actionPos = it->anim->m_ActionPos; bool hasActionPos = (actionPos != -1.f); if (!hasActionPos) @@ -162,15 +160,15 @@ void CUnitAnimation::Update(float time) // Advance all of the prop models independently for (std::vector::iterator it = m_AnimStates.begin(); it != m_AnimStates.end(); ++it) { - CSkeletonAnimDef* animDef = it->anims[it->animIdx]->m_AnimDef; + CSkeletonAnimDef* animDef = it->anim->m_AnimDef; if (animDef == NULL) continue; // ignore static animations float duration = animDef->GetDuration(); - float actionPos = it->anims[it->animIdx]->m_ActionPos; - float loadPos = it->anims[it->animIdx]->m_ActionPos2; - float soundPos = it->anims[it->animIdx]->m_SoundPos; + float actionPos = it->anim->m_ActionPos; + float loadPos = it->anim->m_ActionPos2; + float soundPos = it->anim->m_SoundPos; bool hasActionPos = (actionPos != -1.f); bool hasLoadPos = (loadPos != -1.f); bool hasSoundPos = (soundPos != -1.f); @@ -180,7 +178,7 @@ void CUnitAnimation::Update(float time) if (m_SyncRepeatTime && hasActionPos) speed = duration / m_SyncRepeatTime; else - speed = m_Speed * it->anims[it->animIdx]->m_Speed; + speed = m_Speed * it->anim->m_Speed; // Convert from real time to scaled animation time float advance = time * speed; @@ -232,15 +230,29 @@ void CUnitAnimation::Update(float time) // Wrap the timer around it->time = fmod(it->time + advance, duration); - // If there's a choice of multiple animations, pick a new random one - if (it->anims.size() > 1) + // Pick a new random animation + CSkeletonAnim* anim; + if (it->model == m_Model) { - size_t newAnimIdx = rand(0, it->anims.size()); - if (newAnimIdx != it->animIdx) - { - it->animIdx = newAnimIdx; - it->model->SetAnimation(it->anims[it->animIdx], !m_Looping); - } + // we're handling the root model + // choose animations from the complete state + anim = it->object->GetRandomAnimation(m_State); + m_AnimationName = anim->m_Name; + // if we use a new animation name, + // update the animations of all non-root models + if (it->anim->m_Name != m_AnimationName) + for (SModelAnimState animState : m_AnimStates) + if (animState.model != m_Model) + animState.model->SetAnimation(animState.object->GetRandomAnimation(m_AnimationName)); + } + else + // choose animations that match the root + anim = it->object->GetRandomAnimation(m_AnimationName); + + if (anim != it->anim) + { + it->anim = anim; + it->model->SetAnimation(anim, !m_Looping); } it->pastActionPos = false; diff --git a/source/graphics/UnitAnimation.h b/source/graphics/UnitAnimation.h index dea9373d1b..d9d7d228dc 100644 --- a/source/graphics/UnitAnimation.h +++ b/source/graphics/UnitAnimation.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -95,8 +95,8 @@ private: struct SModelAnimState { CModel* model; - std::vector anims; - size_t animIdx; + CSkeletonAnim* anim; + const CObjectEntry* object; float time; bool pastLoadPos; bool pastActionPos; @@ -117,6 +117,7 @@ private: CModel* m_Model; const CObjectEntry* m_Object; CStr m_State; + CStr m_AnimationName; bool m_Looping; float m_OriginalSpeed; float m_Speed;