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;
}