Index: ps/trunk/source/graphics/UnitAnimation.cpp =================================================================== --- ps/trunk/source/graphics/UnitAnimation.cpp (revision 21334) +++ ps/trunk/source/graphics/UnitAnimation.cpp (revision 21335) @@ -1,282 +1,286 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "UnitAnimation.h" #include "graphics/Model.h" #include "graphics/ObjectEntry.h" #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" // 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) { if (desync == 0.0f) return speed; return speed * (1.f - desync + 2.f*desync*(rand(0, 256)/255.f)); } CUnitAnimation::CUnitAnimation(entity_id_t ent, CModel* model, CObjectEntry* object) : m_Entity(ent), m_State("idle"), m_Looping(true), m_Speed(1.f), m_SyncRepeatTime(0.f), m_OriginalSpeed(1.f), m_Desync(0.f) { ReloadUnit(model, object); } void CUnitAnimation::SetEntityID(entity_id_t ent) { m_Entity = ent; } void CUnitAnimation::AddModel(CModel* model, const CObjectEntry* object) { SModelAnimState state; state.model = model; state.object = object; state.anim = object->GetRandomAnimation(m_State, m_AnimationID); 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.anim, !m_Looping); // Detect if this unit has any non-static animations for (CSkeletonAnim* anim : object->GetAnimations(m_State)) if (anim->m_AnimDef != NULL) m_AnimStatesAreStatic = false; // Recursively add all props const std::vector& props = model->GetProps(); for (const CModel::Prop& prop : props) { CModel* propModel = prop.m_Model->ToCModel(); if (propModel) AddModel(propModel, prop.m_ObjectEntry); } } +void CUnitAnimation::ReloadAnimation() +{ + UpdateAnimationID(); + ReloadUnit(m_Model, m_Object); +} + void CUnitAnimation::ReloadUnit(CModel* model, const CObjectEntry* object) { m_Model = model; m_Object = object; m_AnimStates.clear(); m_AnimStatesAreStatic = true; AddModel(m_Model, m_Object); } void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, float desync, const CStrW& actionSound) { m_Looping = !once; m_OriginalSpeed = speed; m_Desync = desync; m_ActionSound = actionSound; m_Speed = DesyncSpeed(m_OriginalSpeed, m_Desync); m_SyncRepeatTime = 0.f; if (name != m_State) { m_State = name; - UpdateAnimationID(); - - ReloadUnit(m_Model, m_Object); + ReloadAnimation(); } } void CUnitAnimation::SetAnimationSyncRepeat(float repeatTime) { m_SyncRepeatTime = repeatTime; } void CUnitAnimation::SetAnimationSyncOffset(float actionTime) { if (m_AnimStatesAreStatic) return; // 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->anim->m_AnimDef; if (animDef == NULL) continue; // ignore static animations float duration = animDef->GetDuration(); float actionPos = it->anim->m_ActionPos; bool hasActionPos = (actionPos != -1.f); if (!hasActionPos) continue; float speed = duration / m_SyncRepeatTime; // Need to offset so that start+actionTime*speed = actionPos float start = actionPos - actionTime*speed; // Wrap it so that it's within the animation start = fmodf(start, duration); if (start < 0) start += duration; it->time = start; } } void CUnitAnimation::Update(float time) { if (m_AnimStatesAreStatic) return; // Advance all of the prop models independently for (std::vector::iterator it = m_AnimStates.begin(); it != m_AnimStates.end(); ++it) { CSkeletonAnimDef* animDef = it->anim->m_AnimDef; if (animDef == NULL) continue; // ignore static animations float duration = animDef->GetDuration(); 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); // Find the current animation speed float speed; if (m_SyncRepeatTime && hasActionPos) speed = duration / m_SyncRepeatTime; else speed = m_Speed * it->anim->m_Speed; // Convert from real time to scaled animation time float advance = time * speed; // If we're going to advance past the load point in this update, then load the ammo if (hasLoadPos && !it->pastLoadPos && it->time + advance >= loadPos) { it->model->ShowAmmoProp(); it->pastLoadPos = true; } // If we're going to advance past the action point in this update, then perform the action if (hasActionPos && !it->pastActionPos && it->time + advance >= actionPos) { if (hasLoadPos) it->model->HideAmmoProp(); if ( !hasSoundPos && !m_ActionSound.empty() ) { CmpPtr cmpSoundManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpSoundManager) cmpSoundManager->PlaySoundGroup(m_ActionSound, m_Entity); } it->pastActionPos = true; } if (hasSoundPos && !it->pastSoundPos && it->time + advance >= soundPos) { if (!m_ActionSound.empty() ) { CmpPtr cmpSoundManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpSoundManager) cmpSoundManager->PlaySoundGroup(m_ActionSound, m_Entity); } it->pastSoundPos = true; } if (it->time + advance < duration) { // If we're still within the current animation, then simply update it it->time += advance; it->model->UpdateTo(it->time); } else if (m_Looping) { // If we've finished the current animation and want to loop... // Wrap the timer around it->time = fmod(it->time + advance, duration); // Pick a new random animation CSkeletonAnim* anim; if (it->model == m_Model) { // we're handling the root model // choose animations from the complete state CStr oldID = m_AnimationID; UpdateAnimationID(); anim = it->object->GetRandomAnimation(m_State, m_AnimationID); if (oldID != m_AnimationID) for (SModelAnimState animState : m_AnimStates) if (animState.model != m_Model) animState.model->SetAnimation(animState.object->GetRandomAnimation(m_State, m_AnimationID)); } else // choose animations that match the root anim = it->object->GetRandomAnimation(m_State, m_AnimationID); if (anim != it->anim) { it->anim = anim; it->model->SetAnimation(anim, !m_Looping); } it->pastActionPos = false; it->pastLoadPos = false; it->pastSoundPos = false; it->model->UpdateTo(it->time); } else { // If we've finished the current animation and don't want to loop... // Update to very nearly the end of the last frame (but not quite the end else we'll wrap around when skinning) float nearlyEnd = duration - 1.f; if (fabs(it->time - nearlyEnd) > 1.f) { it->time = nearlyEnd; it->model->UpdateTo(it->time); } } } } void CUnitAnimation::UpdateAnimationID() { CStr& ID = m_Object->GetRandomAnimation(m_State)->m_ID; m_AnimationID = ID; } Index: ps/trunk/source/graphics/UnitAnimation.h =================================================================== --- ps/trunk/source/graphics/UnitAnimation.h (revision 21334) +++ ps/trunk/source/graphics/UnitAnimation.h (revision 21335) @@ -1,135 +1,140 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_UNITANIMATION #define INCLUDED_UNITANIMATION #include "ps/CStr.h" #include "simulation2/system/Entity.h" class CUnit; class CModel; class CSkeletonAnim; class CObjectEntry; /** * 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(entity_id_t ent, CModel* model, CObjectEntry* object); /** * Change the entity ID associated with this animation * (currently used for playing locational sound effects). */ void SetEntityID(entity_id_t ent); /** * 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 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, const CStrW& actionSound); /** * Adjust the speed of the current animation, so that Update(repeatTime) will do a * complete animation loop. * @param repeatTime time for complete loop of animation, in msec */ void SetAnimationSyncRepeat(float repeatTime); /** * Adjust the offset of the current animation, so that Update(actionTime) will advance it * to the 'action' point defined in the actor. * This must be called after SetAnimationSyncRepeat sets the speed. * @param actionTime time between now and when the action should occur, in msec */ void SetAnimationSyncOffset(float actionTime); /** * Advance the animation state. * @param time advance time in msec */ void Update(float time); /** * Regenerate internal animation state from the models in the current unit. * This should be called whenever the unit is changed externally, to keep this in sync. */ void ReloadUnit(CModel* model, const CObjectEntry* object); + /** + * Reload animation so any changes take immediate effect. + */ + void ReloadAnimation(); + private: /** * Picks a new animation ID from our current state */ void UpdateAnimationID(); struct SModelAnimState { CModel* model; CSkeletonAnim* anim; const CObjectEntry* object; float time; bool pastLoadPos; bool pastActionPos; bool pastSoundPos; }; std::vector m_AnimStates; /** * True if all the current AnimStates are static, so Update() doesn't need * to do any work at all */ bool m_AnimStatesAreStatic; void AddModel(CModel* model, const CObjectEntry* object); entity_id_t m_Entity; CModel* m_Model; const CObjectEntry* m_Object; CStr m_State; CStr m_AnimationID = ""; bool m_Looping; float m_OriginalSpeed; float m_Speed; float m_SyncRepeatTime; float m_Desync; CStrW m_ActionSound; }; #endif // INCLUDED_UNITANIMATION Index: ps/trunk/source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 21334) +++ ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 21335) @@ -1,793 +1,800 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpVisual.h" #include "simulation2/MessageTypes.h" #include "ICmpFootprint.h" #include "ICmpUnitRenderer.h" #include "ICmpOwnership.h" #include "ICmpPosition.h" #include "ICmpTemplateManager.h" #include "ICmpTerrain.h" #include "ICmpUnitMotion.h" #include "ICmpValueModificationManager.h" #include "ICmpVisibility.h" #include "simulation2/serialization/SerializeTemplates.h" #include "graphics/Decal.h" #include "graphics/Frustum.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/Unit.h" #include "graphics/UnitAnimation.h" #include "graphics/UnitManager.h" #include "maths/BoundingSphere.h" #include "maths/Matrix3D.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "renderer/Scene.h" class CCmpVisualActor : public ICmpVisual { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(VisualActor) private: std::wstring m_BaseActorName, m_ActorName; bool m_IsFoundationActor; CUnit* m_Unit; fixed m_R, m_G, m_B; // shading color std::map m_AnimOverride; // Current animation state fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; std::wstring m_SoundGroup; fixed m_AnimDesync; fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; std::map m_VariantSelections; u32 m_Seed; // seed used for random variations bool m_ConstructionPreview; bool m_VisibleInAtlasOnly; bool m_IsActorOnly; // an in-world entity should not have this or it might not be rendered. ICmpUnitRenderer::tag_t m_ModelTag; public: static std::string GetSchema() { return "Display the unit using the engine's actor system." "" "units/hellenes/infantry_spearman_b.xml" "" "" "structures/hellenes/barracks.xml" "structures/fndn_4x4.xml" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); m_Seed = GetEntityId(); m_IsFoundationActor = paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk(); if (m_IsFoundationActor) m_BaseActorName = m_ActorName = paramNode.GetChild("FoundationActor").ToString(); else m_BaseActorName = m_ActorName = paramNode.GetChild("Actor").ToString(); m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool(); m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk(); InitModel(paramNode); SelectAnimation("idle", false, fixed::FromInt(1), L""); } virtual void Deinit() { if (m_Unit) { GetSimContext().GetUnitManager().DeleteUnit(m_Unit); m_Unit = NULL; } } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("r", m_R); serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); SerializeMap()(serialize, "anim overrides", m_AnimOverride); serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); serialize.String("sound group", m_SoundGroup, 0, 256); serialize.NumberFixed_Unbounded("anim desync", m_AnimDesync); serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime); serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime); SerializeMap()(serialize, "variation", m_VariantSelections); serialize.NumberU32_Unbounded("seed", m_Seed); serialize.String("actor", m_ActorName, 0, 256); // TODO: store actor variables? } virtual void Serialize(ISerializer& serialize) { // TODO: store the actor name, if !debug and it differs from the template if (serialize.IsDebug()) { serialize.String("base actor", m_BaseActorName, 0, 256); } SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 oldSeed = GetActorSeed(); SerializeCommon(deserialize); // If we serialized a different seed or different actor, reload actor if (oldSeed != GetActorSeed() || m_BaseActorName != m_ActorName) ReloadActor(); else ReloadUnitAnimation(); if (m_Unit) { CmpPtr cmpOwnership(GetEntityHandle()); if (cmpOwnership) m_Unit->GetModel().SetPlayerID(cmpOwnership->GetOwner()); } } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update_Final: { const CMessageUpdate_Final& msgData = static_cast (msg); Update(msgData.turnLength); break; } case MT_OwnershipChanged: { if (!m_Unit) break; const CMessageOwnershipChanged& msgData = static_cast (msg); m_Unit->GetModel().SetPlayerID(msgData.to); break; } case MT_TerrainChanged: { if (!m_Unit) break; const CMessageTerrainChanged& msgData = static_cast (msg); m_Unit->GetModel().SetTerrainDirty(msgData.i0, msgData.j0, msgData.i1, msgData.j1); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"VisualActor") break; CmpPtr cmpValueModificationManager(GetSystemEntity()); std::wstring newActorName; if (m_IsFoundationActor) newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/FoundationActor", m_BaseActorName, GetEntityId()); else newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/Actor", m_BaseActorName, GetEntityId()); if (newActorName != m_ActorName) { m_ActorName = newActorName; ReloadActor(); } break; } case MT_InterpolatedPositionChanged: { const CMessageInterpolatedPositionChanged& msgData = static_cast (msg); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->UpdateUnitPos(m_ModelTag, msgData.inWorld, msgData.pos0, msgData.pos1); } break; } case MT_Destroy: { if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->RemoveUnit(m_ModelTag); m_ModelTag = ICmpUnitRenderer::tag_t(); } break; } } } virtual CBoundingBoxAligned GetBounds() const { if (!m_Unit) return CBoundingBoxAligned::EMPTY; return m_Unit->GetModel().GetWorldBounds(); } virtual CUnit* GetUnit() { return m_Unit; } virtual CBoundingBoxOriented GetSelectionBox() const { if (!m_Unit) return CBoundingBoxOriented::EMPTY; return m_Unit->GetModel().GetSelectionBox(); } virtual CVector3D GetPosition() const { if (!m_Unit) return CVector3D(0, 0, 0); return m_Unit->GetModel().GetTransform().GetTranslation(); } virtual std::wstring GetActorShortName() const { if (!m_Unit) return L""; return m_Unit->GetObject().m_Base->m_ShortName; } virtual std::wstring GetProjectileActor() const { if (!m_Unit) return L""; return m_Unit->GetObject().m_ProjectileModelName; } virtual CFixedVector3D GetProjectileLaunchPoint() const { if (!m_Unit) return CFixedVector3D(); if (m_Unit->GetModel().ToCModel()) { // Ensure the prop transforms are correct CmpPtr cmpUnitRenderer(GetSystemEntity()); CmpPtr cmpPosition(GetEntityHandle()); if (cmpUnitRenderer && cmpPosition) { float frameOffset = cmpUnitRenderer->GetFrameOffset(); CMatrix3D transform(cmpPosition->GetInterpolatedTransform(frameOffset)); m_Unit->GetModel().SetTransform(transform); m_Unit->GetModel().ValidatePosition(); } CModelAbstract* ammo = m_Unit->GetModel().ToCModel()->FindFirstAmmoProp(); if (ammo) { CVector3D vector = ammo->GetTransform().GetTranslation(); return CFixedVector3D(fixed::FromFloat(vector.X), fixed::FromFloat(vector.Y), fixed::FromFloat(vector.Z)); } } return CFixedVector3D(); } virtual void SetVariant(const CStr& key, const CStr& selection) { + if (m_VariantSelections[key] == selection) + return; + m_VariantSelections[key] = selection; if (m_Unit) + { m_Unit->SetEntitySelection(key, selection); + if (m_Unit->GetAnimation()) + m_Unit->GetAnimation()->ReloadAnimation(); + } } virtual std::string GetAnimationName() const { return m_AnimName; } virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) { m_AnimRunThreshold = fixed::Zero(); m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; m_SoundGroup = soundgroup; m_AnimDesync = fixed::FromInt(1)/20; // TODO: make this an argument m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); SetVariant("animation", m_AnimName); if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); } virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) { m_AnimOverride[name] = replace; } virtual void ResetMoveAnimation(const std::string& name) { std::map::const_iterator it = m_AnimOverride.find(name); if (it != m_AnimOverride.end()) m_AnimOverride.erase(name); } virtual void SelectMovementAnimation(fixed runThreshold) { SelectAnimation("walk", false, fixed::FromFloat(1.f), L""); m_AnimRunThreshold = runThreshold; } virtual void SetAnimationSyncRepeat(fixed repeattime) { m_AnimSyncRepeatTime = repeattime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); } virtual void SetAnimationSyncOffset(fixed actiontime) { m_AnimSyncOffsetTime = actiontime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } virtual void SetShadingColor(fixed r, fixed g, fixed b, fixed a) { m_R = r; m_G = g; m_B = b; UNUSED2(a); // TODO: why is this even an argument? if (m_Unit) { CModelAbstract& model = m_Unit->GetModel(); model.SetShadingColor(CColor(m_R.ToFloat(), m_G.ToFloat(), m_B.ToFloat(), 1.0f)); } } virtual void SetVariable(const std::string& name, float value) { if (m_Unit) m_Unit->GetModel().SetEntityVariable(name, value); } virtual u32 GetActorSeed() const { return m_Seed; } virtual void SetActorSeed(u32 seed) { if (seed == m_Seed) return; m_Seed = seed; ReloadActor(); } virtual bool HasConstructionPreview() const { return m_ConstructionPreview; } virtual void Hotload(const VfsPath& name) { if (!m_Unit) return; if (name != m_ActorName) return; ReloadActor(); } private: /// Helper function shared by component init and actor reloading void InitModel(const CParamNode& paramNode); /// Helper method; initializes the model selection shape descriptor from XML. Factored out for readability of @ref Init. void InitSelectionShapeDescriptor(const CParamNode& paramNode); // ReloadActor is used when the actor or seed changes. void ReloadActor(); // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); void Update(fixed turnLength); }; REGISTER_COMPONENT_TYPE(VisualActor) // ------------------------------------------------------------------------------------------------------------------ void CCmpVisualActor::InitModel(const CParamNode& paramNode) { if (!GetSimContext().HasUnitManager()) return; std::set selections; std::wstring actorName = m_ActorName; if (actorName.find(L".xml") == std::wstring::npos) actorName += L".xml"; m_Unit = GetSimContext().GetUnitManager().CreateUnit(actorName, GetActorSeed(), selections); if (!m_Unit) return; CModelAbstract& model = m_Unit->GetModel(); if (model.ToCModel()) { u32 modelFlags = 0; if (paramNode.GetChild("SilhouetteDisplay").ToBool()) modelFlags |= MODELFLAG_SILHOUETTE_DISPLAY; if (paramNode.GetChild("SilhouetteOccluder").ToBool()) modelFlags |= MODELFLAG_SILHOUETTE_OCCLUDER; CmpPtr cmpVisibility(GetEntityHandle()); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) modelFlags |= MODELFLAG_IGNORE_LOS; model.ToCModel()->AddFlagsRec(modelFlags); } if (paramNode.GetChild("DisableShadows").IsOk()) { if (model.ToCModel()) model.ToCModel()->RemoveShadowsRec(); else if (model.ToCModelDecal()) model.ToCModelDecal()->RemoveShadows(); } // Initialize the model's selection shape descriptor. This currently relies on the component initialization order; the // Footprint component must be initialized before this component (VisualActor) to support the ability to use the footprint // shape for the selection box (instead of the default recursive bounding box). See TypeList.h for the order in // which components are initialized; if for whatever reason you need to get rid of this dependency, you can always just // initialize the selection shape descriptor on-demand. InitSelectionShapeDescriptor(paramNode); m_Unit->SetID(GetEntityId()); bool floating = m_Unit->GetObject().m_Base->m_Properties.m_FloatOnWater; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) cmpPosition->SetActorFloating(floating); if (!m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); if (cmpModelRenderer) { // TODO: this should account for all possible props, animations, etc, // else we might accidentally cull the unit when it should be visible CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); int flags = 0; if (m_IsActorOnly) flags |= ICmpUnitRenderer::ACTOR_ONLY; if (m_VisibleInAtlasOnly) flags |= ICmpUnitRenderer::VISIBLE_IN_ATLAS_ONLY; m_ModelTag = cmpModelRenderer->AddUnit(GetEntityHandle(), m_Unit, boundSphere, flags); } } } void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode& paramNode) { // by default, we don't need a custom selection shape and we can just keep the default behaviour CModelAbstract::CustomSelectionShape* shapeDescriptor = NULL; const CParamNode& shapeNode = paramNode.GetChild("SelectionShape"); if (shapeNode.IsOk()) { if (shapeNode.GetChild("Bounds").IsOk()) { // default; no need to take action } else if (shapeNode.GetChild("Footprint").IsOk()) { CmpPtr cmpFootprint(GetEntityHandle()); if (cmpFootprint) { ICmpFootprint::EShape fpShape; // fp stands for "footprint" entity_pos_t fpSize0, fpSize1, fpHeight; // fp stands for "footprint" cmpFootprint->GetShape(fpShape, fpSize0, fpSize1, fpHeight); float size0 = fpSize0.ToFloat(); float size1 = fpSize1.ToFloat(); // TODO: we should properly distinguish between CIRCLE and SQUARE footprint shapes here, but since cylinders // aren't implemented yet and are almost indistinguishable from boxes for small enough sizes anyway, // we'll just use boxes for either case. However, for circular footprints the size0 and size1 values both // represent the radius, so we do have to adjust them to match the size1 and size0's of square footprints // (which represent the full width and depth). if (fpShape == ICmpFootprint::CIRCLE) { size0 *= 2; size1 *= 2; } shapeDescriptor = new CModelAbstract::CustomSelectionShape; shapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; shapeDescriptor->m_Size0 = size0; shapeDescriptor->m_Size1 = size1; shapeDescriptor->m_Height = fpHeight.ToFloat(); } else { LOGERROR("[VisualActor] Cannot apply footprint-based SelectionShape; Footprint component not initialized."); } } else if (shapeNode.GetChild("Box").IsOk()) { // TODO: we might need to support the ability to specify a different box center in the future shapeDescriptor = new CModelAbstract::CustomSelectionShape; shapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; shapeDescriptor->m_Size0 = shapeNode.GetChild("Box").GetChild("@width").ToFixed().ToFloat(); shapeDescriptor->m_Size1 = shapeNode.GetChild("Box").GetChild("@depth").ToFixed().ToFloat(); shapeDescriptor->m_Height = shapeNode.GetChild("Box").GetChild("@height").ToFixed().ToFloat(); } else if (shapeNode.GetChild("Cylinder").IsOk()) { LOGWARNING("[VisualActor] TODO: Cylinder selection shapes are not yet implemented; defaulting to recursive bounding boxes"); } else { // shouldn't happen by virtue of validation against schema LOGERROR("[VisualActor] No selection shape specified"); } } ENSURE(m_Unit); // the model is now responsible for cleaning up the descriptor m_Unit->GetModel().SetCustomSelectionShape(shapeDescriptor); } void CCmpVisualActor::ReloadActor() { if (!m_Unit) return; // Save some data from the old unit CColor shading = m_Unit->GetModel().GetShadingColor(); player_id_t playerID = m_Unit->GetModel().GetPlayerID(); // Replace with the new unit GetSimContext().GetUnitManager().DeleteUnit(m_Unit); // HACK: selection shape needs template data, but rather than storing all that data // in the component, we load the template here and pass it into a helper function CmpPtr cmpTemplateManager(GetSystemEntity()); const CParamNode* node = cmpTemplateManager->LoadLatestTemplate(GetEntityId()); ENSURE(node && node->GetChild("VisualActor").IsOk()); InitModel(node->GetChild("VisualActor")); ReloadUnitAnimation(); m_Unit->GetModel().SetShadingColor(shading); m_Unit->GetModel().SetPlayerID(playerID); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); cmpModelRenderer->UpdateUnit(m_ModelTag, m_Unit, boundSphere); } } void CCmpVisualActor::ReloadUnitAnimation() { if (!m_Unit) return; m_Unit->SetEntitySelection(m_VariantSelections); if (!m_Unit->GetAnimation()) return; m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); // We'll lose the exact synchronisation but we should at least make sure it's going at the correct rate if (!m_AnimSyncRepeatTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); if (!m_AnimSyncOffsetTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } void CCmpVisualActor::Update(fixed UNUSED(turnLength)) { // This function is currently only used to update the animation if the speed in // CCmpUnitMotion changes. This also only happens in the "special movement mode" // triggered by SelectMovementAnimation. // TODO: This should become event based, in order to save performance and to make the code // far less hacky. We should also take into account the speed when the animation is different // from the "special movement mode" walking animation. // If we're not in the special movement mode, nothing to do. if (m_AnimRunThreshold.IsZero()) return; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CmpPtr cmpUnitMotion(GetEntityHandle()); if (!cmpUnitMotion) return; fixed speed = cmpUnitMotion->GetCurrentSpeed(); std::string name; if (speed.IsZero()) { speed = fixed::FromFloat(1.f); name = "idle"; } else name = speed < m_AnimRunThreshold ? "walk" : "run"; std::map::const_iterator it = m_AnimOverride.find(name); if (it != m_AnimOverride.end()) name = it->second; // Selecting the animation is going to reset the anim run threshold, so save it fixed runThreshold = m_AnimRunThreshold; SelectAnimation(name, false, speed, L""); m_AnimRunThreshold = runThreshold; }