1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "simulation2/system/Component.h"
21 #include "ICmpVisual.h"
23 #include "simulation2/MessageTypes.h"
24 #include "simulation2/serialization/SerializedTypes.h"
26 #include "ICmpFootprint.h"
27 #include "ICmpIdentity.h"
28 #include "ICmpMirage.h"
29 #include "ICmpOwnership.h"
30 #include "ICmpPosition.h"
31 #include "ICmpTemplateManager.h"
32 #include "ICmpTerrain.h"
33 #include "ICmpUnitMotion.h"
34 #include "ICmpUnitRenderer.h"
35 #include "ICmpValueModificationManager.h"
36 #include "ICmpVisibility.h"
37 #include "ICmpSound.h"
39 #include "graphics/Decal.h"
40 #include "graphics/Model.h"
41 #include "graphics/ObjectBase.h"
42 #include "graphics/ObjectEntry.h"
43 #include "graphics/Unit.h"
44 #include "graphics/UnitAnimation.h"
45 #include "graphics/UnitManager.h"
46 #include "maths/BoundingSphere.h"
47 #include "maths/Frustum.h"
48 #include "maths/Matrix3D.h"
49 #include "maths/Vector3D.h"
50 #include "ps/CLogger.h"
51 #include "ps/GameSetup/Config.h"
52 #include "renderer/Scene.h"
54 class CCmpVisualActor final
: public ICmpVisual
57 static void ClassInit(CComponentManager
& componentManager
)
59 componentManager
.SubscribeToMessageType(MT_InterpolatedPositionChanged
);
60 componentManager
.SubscribeToMessageType(MT_OwnershipChanged
);
61 componentManager
.SubscribeToMessageType(MT_ValueModification
);
62 componentManager
.SubscribeToMessageType(MT_Create
);
63 componentManager
.SubscribeToMessageType(MT_Destroy
);
66 DEFAULT_COMPONENT_ALLOCATOR(VisualActor
)
69 std::wstring m_BaseActorName
, m_ActorName
;
70 bool m_IsFoundationActor
;
72 // Not initialized in non-visual mode
74 CModelAbstract::CustomSelectionShape
* m_ShapeDescriptor
= nullptr;
76 fixed m_R
, m_G
, m_B
; // shading color
78 // Current animation state
79 std::string m_AnimName
;
82 std::wstring m_SoundGroup
;
84 fixed m_AnimSyncRepeatTime
; // 0.0 if not synced
85 fixed m_AnimSyncOffsetTime
;
87 std::map
<CStr
, CStr
> m_VariantSelections
;
89 u32 m_Seed
; // seed used for random variations
91 bool m_ConstructionPreview
;
93 bool m_VisibleInAtlasOnly
;
94 bool m_IsActorOnly
; // an in-world entity should not have this or it might not be rendered.
96 bool m_SilhouetteDisplay
;
97 bool m_SilhouetteOccluder
;
98 bool m_DisableShadows
;
100 ICmpUnitRenderer::tag_t m_ModelTag
;
103 static std::string
GetSchema()
106 "<a:help>Display the unit using the engine's actor system.</a:help>"
108 "<Actor>units/hellenes/infantry_spearman_b.xml</Actor>"
111 "<Actor>structures/hellenes/barracks.xml</Actor>"
112 "<FoundationActor>structures/fndn_4x4.xml</FoundationActor>"
114 "<element name='Actor' a:help='Filename of the actor to be used for this unit'>"
118 "<element name='FoundationActor' a:help='Filename of the actor to be used the foundation while this unit is being constructed'>"
123 "<element name='Foundation' a:help='Used internally; if present, the unit will be rendered as a foundation'>"
128 "<element name='ConstructionPreview' a:help='If present, the unit should have a construction preview'>"
133 "<element name='DisableShadows' a:help='Used internally; if present, shadows will be disabled'>"
138 "<element name='ActorOnly' a:help='Used internally; if present, the unit will only be rendered if the user has high enough graphical settings.'>"
142 "<element name='SilhouetteDisplay'>"
143 "<data type='boolean'/>"
145 "<element name='SilhouetteOccluder'>"
146 "<data type='boolean'/>"
149 "<element name='SelectionShape'>"
151 "<element name='Bounds' a:help='Determines the selection box based on the model bounds'>"
154 "<element name='Footprint' a:help='Determines the selection box based on the entity Footprint component'>"
157 "<element name='Box' a:help='Sets the selection shape to a box of specified dimensions'>"
158 "<attribute name='width'>"
159 "<data type='decimal'>"
160 "<param name='minExclusive'>0.0</param>"
163 "<attribute name='height'>"
164 "<data type='decimal'>"
165 "<param name='minExclusive'>0.0</param>"
168 "<attribute name='depth'>"
169 "<data type='decimal'>"
170 "<param name='minExclusive'>0.0</param>"
174 "<element name='Cylinder' a:help='Sets the selection shape to a cylinder of specified dimensions'>"
175 "<attribute name='radius'>"
176 "<data type='decimal'>"
177 "<param name='minExclusive'>0.0</param>"
180 "<attribute name='height'>"
181 "<data type='decimal'>"
182 "<param name='minExclusive'>0.0</param>"
189 "<element name='VisibleInAtlasOnly'>"
190 "<data type='boolean'/>"
194 void Init(const CParamNode
& paramNode
) override
197 m_R
= m_G
= m_B
= fixed::FromInt(1);
199 m_ConstructionPreview
= paramNode
.GetChild("ConstructionPreview").IsOk();
201 m_Seed
= GetEntityId();
203 m_IsFoundationActor
= paramNode
.GetChild("Foundation").IsOk() && paramNode
.GetChild("FoundationActor").IsOk();
205 m_BaseActorName
= paramNode
.GetChild(m_IsFoundationActor
? "FoundationActor" : "Actor").ToWString();
206 ParseActorName(m_BaseActorName
);
208 m_VisibleInAtlasOnly
= paramNode
.GetChild("VisibleInAtlasOnly").ToBool();
209 m_IsActorOnly
= paramNode
.GetChild("ActorOnly").IsOk();
211 m_SilhouetteDisplay
= paramNode
.GetChild("SilhouetteDisplay").ToBool();
212 m_SilhouetteOccluder
= paramNode
.GetChild("SilhouetteOccluder").ToBool();
213 m_DisableShadows
= paramNode
.GetChild("DisableShadows").ToBool();
215 // Initialize the model's selection shape descriptor. This currently relies on the component initialization order; the
216 // Footprint component must be initialized before this component (VisualActor) to support the ability to use the footprint
217 // shape for the selection box (instead of the default recursive bounding box). See TypeList.h for the order in
218 // which components are initialized; if for whatever reason you need to get rid of this dependency, you can always just
219 // initialize the selection shape descriptor on-demand.
220 InitSelectionShapeDescriptor(paramNode
);
223 void Deinit() override
227 GetSimContext().GetUnitManager().DeleteUnit(m_Unit
);
233 void SerializeCommon(S
& serialize
)
235 serialize
.NumberFixed_Unbounded("r", m_R
);
236 serialize
.NumberFixed_Unbounded("g", m_G
);
237 serialize
.NumberFixed_Unbounded("b", m_B
);
239 serialize
.StringASCII("anim name", m_AnimName
, 0, 256);
240 serialize
.Bool("anim once", m_AnimOnce
);
241 serialize
.NumberFixed_Unbounded("anim speed", m_AnimSpeed
);
242 serialize
.String("sound group", m_SoundGroup
, 0, 256);
243 serialize
.NumberFixed_Unbounded("anim desync", m_AnimDesync
);
244 serialize
.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime
);
245 serialize
.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime
);
247 Serializer(serialize
, "variation", m_VariantSelections
);
249 serialize
.NumberU32_Unbounded("seed", m_Seed
);
250 serialize
.String("actor", m_ActorName
, 0, 256);
252 // TODO: store actor variables?
255 void Serialize(ISerializer
& serialize
) override
257 // TODO: store the actor name, if !debug and it differs from the template
259 if (serialize
.IsDebug())
261 serialize
.String("base actor", m_BaseActorName
, 0, 256);
264 SerializeCommon(serialize
);
267 void Deserialize(const CParamNode
& paramNode
, IDeserializer
& deserialize
) override
271 u32 oldSeed
= GetActorSeed();
273 SerializeCommon(deserialize
);
277 // If we serialized a different seed or different actor, reload actor
278 if (oldSeed
!= GetActorSeed() || m_BaseActorName
!= m_ActorName
)
281 ReloadUnitAnimation();
285 CmpPtr
<ICmpOwnership
> cmpOwnership(GetEntityHandle());
287 m_Unit
->GetModel().SetPlayerID(cmpOwnership
->GetOwner());
291 void HandleMessage(const CMessage
& msg
, bool UNUSED(global
)) override
293 switch (msg
.GetType())
295 case MT_OwnershipChanged
:
297 RecomputeActorName();
302 const CMessageOwnershipChanged
& msgData
= static_cast<const CMessageOwnershipChanged
&> (msg
);
303 m_Unit
->GetModel().SetPlayerID(msgData
.to
);
307 case MT_ValueModification
:
309 // Mirages don't respond to technology modifications.
310 CmpPtr
<ICmpMirage
> cmpMirage(GetEntityHandle());
314 const CMessageValueModification
& msgData
= static_cast<const CMessageValueModification
&> (msg
);
315 if (msgData
.component
!= L
"VisualActor")
318 RecomputeActorName();
322 case MT_InterpolatedPositionChanged
:
324 const CMessageInterpolatedPositionChanged
& msgData
= static_cast<const CMessageInterpolatedPositionChanged
&> (msg
);
325 if (m_ModelTag
.valid())
327 CmpPtr
<ICmpUnitRenderer
> cmpModelRenderer(GetSystemEntity());
328 cmpModelRenderer
->UpdateUnitPos(m_ModelTag
, msgData
.inWorld
, msgData
.pos0
, msgData
.pos1
);
336 SelectAnimation("idle");
341 if (m_ModelTag
.valid())
343 CmpPtr
<ICmpUnitRenderer
> cmpModelRenderer(GetSystemEntity());
344 cmpModelRenderer
->RemoveUnit(m_ModelTag
);
345 m_ModelTag
= ICmpUnitRenderer::tag_t();
352 CBoundingBoxAligned
GetBounds() const override
355 return CBoundingBoxAligned::EMPTY
;
356 return m_Unit
->GetModel().GetWorldBounds();
359 CUnit
* GetUnit() override
364 CBoundingBoxOriented
GetSelectionBox() const override
367 return CBoundingBoxOriented::EMPTY
;
368 return m_Unit
->GetModel().GetSelectionBox();
371 CVector3D
GetPosition() const override
374 return CVector3D(0, 0, 0);
375 return m_Unit
->GetModel().GetTransform().GetTranslation();
378 std::wstring
GetProjectileActor() const override
382 return m_Unit
->GetObject().m_ProjectileModelName
;
385 CFixedVector3D
GetProjectileLaunchPoint() const override
388 return CFixedVector3D();
390 if (m_Unit
->GetModel().ToCModel())
392 // Ensure the prop transforms are correct
393 CmpPtr
<ICmpUnitRenderer
> cmpUnitRenderer(GetSystemEntity());
394 CmpPtr
<ICmpPosition
> cmpPosition(GetEntityHandle());
395 if (cmpUnitRenderer
&& cmpPosition
)
397 float frameOffset
= cmpUnitRenderer
->GetFrameOffset();
398 CMatrix3D
transform(cmpPosition
->GetInterpolatedTransform(frameOffset
));
399 m_Unit
->GetModel().SetTransform(transform
);
400 m_Unit
->GetModel().ValidatePosition();
403 CModelAbstract
* ammo
= m_Unit
->GetModel().ToCModel()->FindFirstAmmoProp();
406 CVector3D vector
= ammo
->GetTransform().GetTranslation();
407 return CFixedVector3D(fixed::FromFloat(vector
.X
), fixed::FromFloat(vector
.Y
), fixed::FromFloat(vector
.Z
));
411 return CFixedVector3D();
414 void SetVariant(const CStr
& key
, const CStr
& selection
) override
416 if (m_VariantSelections
[key
] == selection
)
419 m_VariantSelections
[key
] = selection
;
423 m_Unit
->SetEntitySelection(key
, selection
);
424 if (m_Unit
->GetAnimation())
425 m_Unit
->GetAnimation()->ReloadAnimation();
429 std::string
GetAnimationName() const override
434 void SelectAnimation(const std::string
& name
, bool once
= false, fixed speed
= fixed::FromInt(1)) override
440 m_AnimDesync
= fixed::FromInt(1)/20; // TODO: make this an argument
441 m_AnimSyncRepeatTime
= fixed::Zero();
442 m_AnimSyncOffsetTime
= fixed::Zero();
444 SetVariant("animation", m_AnimName
);
446 CmpPtr
<ICmpSound
> cmpSound(GetEntityHandle());
448 m_SoundGroup
= cmpSound
->GetSoundGroup(wstring_from_utf8(m_AnimName
));
450 if (!m_Unit
|| !m_Unit
->GetAnimation() || !m_Unit
->GetID())
453 m_Unit
->GetAnimation()->SetAnimationState(m_AnimName
, m_AnimOnce
, m_AnimSpeed
.ToFloat(), m_AnimDesync
.ToFloat(), m_SoundGroup
.c_str());
456 void SelectMovementAnimation(const std::string
& name
, fixed speed
) override
458 ENSURE(name
== "idle" || name
== "walk" || name
== "run");
459 if (m_AnimName
!= "idle" && m_AnimName
!= "walk" && m_AnimName
!= "run")
461 if (m_AnimName
== name
&& speed
== m_AnimSpeed
)
463 SelectAnimation(name
, false, speed
);
466 void SetAnimationSyncRepeat(fixed repeattime
) override
468 m_AnimSyncRepeatTime
= repeattime
;
470 if (m_Unit
&& m_Unit
->GetAnimation())
471 m_Unit
->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime
.ToFloat());
474 void SetAnimationSyncOffset(fixed actiontime
) override
476 m_AnimSyncOffsetTime
= actiontime
;
478 if (m_Unit
&& m_Unit
->GetAnimation())
479 m_Unit
->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime
.ToFloat());
482 void SetShadingColor(fixed r
, fixed g
, fixed b
, fixed a
) override
487 UNUSED2(a
); // TODO: why is this even an argument?
491 CModelAbstract
& model
= m_Unit
->GetModel();
492 model
.SetShadingColor(CColor(m_R
.ToFloat(), m_G
.ToFloat(), m_B
.ToFloat(), 1.0f
));
496 void SetVariable(const std::string
& name
, float value
) override
499 m_Unit
->GetModel().SetEntityVariable(name
, value
);
502 u32
GetActorSeed() const override
507 void SetActorSeed(u32 seed
) override
516 void RecomputeActorName() override
518 CmpPtr
<ICmpValueModificationManager
> cmpValueModificationManager(GetSystemEntity());
519 std::wstring newActorName
;
520 if (m_IsFoundationActor
)
521 newActorName
= cmpValueModificationManager
->ApplyModifications(L
"VisualActor/FoundationActor", m_BaseActorName
, GetEntityId());
523 newActorName
= cmpValueModificationManager
->ApplyModifications(L
"VisualActor/Actor", m_BaseActorName
, GetEntityId());
525 if (newActorName
!= m_ActorName
)
527 ParseActorName(newActorName
);
532 bool HasConstructionPreview() const override
534 return m_ConstructionPreview
;
537 void Hotload(const VfsPath
& name
) override
542 if (!name
.empty() && name
!= m_ActorName
)
549 // Replace {phenotype} with the correct value in m_ActorName
550 void ParseActorName(std::wstring base
);
552 /// Helper function shared by component init and actor reloading
555 /// Helper method; initializes the model selection shape descriptor from XML. Factored out for readability of @ref Init.
556 void InitSelectionShapeDescriptor(const CParamNode
& paramNode
);
558 // ReloadActor is used when the actor or seed changes.
560 // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical.
561 // It is also used by ReloadActor.
562 void ReloadUnitAnimation();
565 REGISTER_COMPONENT_TYPE(VisualActor
)
567 // ------------------------------------------------------------------------------------------------------------------
569 void CCmpVisualActor::ParseActorName(std::wstring base
)
571 CmpPtr
<ICmpIdentity
> cmpIdentity(GetEntityHandle());
572 const std::wstring pattern
= L
"{phenotype}";
575 size_t pos
= base
.find(pattern
);
576 while (pos
!= std::string::npos
)
578 base
.replace(pos
, pattern
.size(), cmpIdentity
->GetPhenotype());
579 pos
= base
.find(pattern
, pos
+ pattern
.size());
586 void CCmpVisualActor::InitModel()
588 if (!GetSimContext().HasUnitManager())
591 std::wstring actorName
= m_ActorName
;
592 if (actorName
.find(L
".xml") == std::wstring::npos
)
593 actorName
+= L
".xml";
594 m_Unit
= GetSimContext().GetUnitManager().CreateUnit(actorName
, GetActorSeed());
598 CModelAbstract
& model
= m_Unit
->GetModel();
599 if (model
.ToCModel())
603 if (m_SilhouetteDisplay
)
604 modelFlags
|= MODELFLAG_SILHOUETTE_DISPLAY
;
606 if (m_SilhouetteOccluder
)
607 modelFlags
|= MODELFLAG_SILHOUETTE_OCCLUDER
;
609 CmpPtr
<ICmpVisibility
> cmpVisibility(GetEntityHandle());
610 if (cmpVisibility
&& cmpVisibility
->GetAlwaysVisible())
611 modelFlags
|= MODELFLAG_IGNORE_LOS
;
613 model
.ToCModel()->AddFlagsRec(modelFlags
);
616 if (m_DisableShadows
)
618 if (model
.ToCModel())
619 model
.ToCModel()->RemoveShadowsRec();
620 else if (model
.ToCModelDecal())
621 model
.ToCModelDecal()->RemoveShadows();
624 m_Unit
->SetID(GetEntityId());
626 bool floating
= m_Unit
->GetObject().m_Base
->m_Properties
.m_FloatOnWater
;
627 CmpPtr
<ICmpPosition
> cmpPosition(GetEntityHandle());
629 cmpPosition
->SetActorFloating(floating
);
631 if (!m_ModelTag
.valid())
633 CmpPtr
<ICmpUnitRenderer
> cmpModelRenderer(GetSystemEntity());
634 if (cmpModelRenderer
)
636 // TODO: this should account for all possible props, animations, etc,
637 // else we might accidentally cull the unit when it should be visible
638 CBoundingBoxAligned bounds
= m_Unit
->GetModel().GetWorldBoundsRec();
639 CBoundingSphere boundSphere
= CBoundingSphere::FromSweptBox(bounds
);
643 flags
|= ICmpUnitRenderer::ACTOR_ONLY
;
644 if (m_VisibleInAtlasOnly
)
645 flags
|= ICmpUnitRenderer::VISIBLE_IN_ATLAS_ONLY
;
647 m_ModelTag
= cmpModelRenderer
->AddUnit(GetEntityHandle(), m_Unit
, boundSphere
, flags
);
651 // the model is now responsible for cleaning up the descriptor
652 if (m_ShapeDescriptor
!= nullptr)
653 m_Unit
->GetModel().SetCustomSelectionShape(m_ShapeDescriptor
);
656 void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode
& paramNode
)
658 // by default, we don't need a custom selection shape and we can just keep the default behaviour
659 m_ShapeDescriptor
= nullptr;
661 const CParamNode
& shapeNode
= paramNode
.GetChild("SelectionShape");
662 if (shapeNode
.IsOk())
664 if (shapeNode
.GetChild("Bounds").IsOk())
666 // default; no need to take action
668 else if (shapeNode
.GetChild("Footprint").IsOk())
670 CmpPtr
<ICmpFootprint
> cmpFootprint(GetEntityHandle());
673 ICmpFootprint::EShape fpShape
; // fp stands for "footprint"
674 entity_pos_t fpSize0
, fpSize1
, fpHeight
; // fp stands for "footprint"
675 cmpFootprint
->GetShape(fpShape
, fpSize0
, fpSize1
, fpHeight
);
677 float size0
= fpSize0
.ToFloat();
678 float size1
= fpSize1
.ToFloat();
680 // TODO: we should properly distinguish between CIRCLE and SQUARE footprint shapes here, but since cylinders
681 // aren't implemented yet and are almost indistinguishable from boxes for small enough sizes anyway,
682 // we'll just use boxes for either case. However, for circular footprints the size0 and size1 values both
683 // represent the radius, so we do have to adjust them to match the size1 and size0's of square footprints
684 // (which represent the full width and depth).
685 if (fpShape
== ICmpFootprint::CIRCLE
)
691 m_ShapeDescriptor
= new CModelAbstract::CustomSelectionShape
;
692 m_ShapeDescriptor
->m_Type
= CModelAbstract::CustomSelectionShape::BOX
;
693 m_ShapeDescriptor
->m_Size0
= size0
;
694 m_ShapeDescriptor
->m_Size1
= size1
;
695 m_ShapeDescriptor
->m_Height
= fpHeight
.ToFloat();
699 LOGERROR("[VisualActor] Cannot apply footprint-based SelectionShape; Footprint component not initialized.");
702 else if (shapeNode
.GetChild("Box").IsOk())
704 // TODO: we might need to support the ability to specify a different box center in the future
705 m_ShapeDescriptor
= new CModelAbstract::CustomSelectionShape
;
706 m_ShapeDescriptor
->m_Type
= CModelAbstract::CustomSelectionShape::BOX
;
707 m_ShapeDescriptor
->m_Size0
= shapeNode
.GetChild("Box").GetChild("@width").ToFixed().ToFloat();
708 m_ShapeDescriptor
->m_Size1
= shapeNode
.GetChild("Box").GetChild("@depth").ToFixed().ToFloat();
709 m_ShapeDescriptor
->m_Height
= shapeNode
.GetChild("Box").GetChild("@height").ToFixed().ToFloat();
711 else if (shapeNode
.GetChild("Cylinder").IsOk())
713 LOGWARNING("[VisualActor] TODO: Cylinder selection shapes are not yet implemented; defaulting to recursive bounding boxes");
717 // shouldn't happen by virtue of validation against schema
718 LOGERROR("[VisualActor] No selection shape specified");
723 void CCmpVisualActor::ReloadActor()
728 // Save some data from the old unit
729 CColor shading
= m_Unit
->GetModel().GetShadingColor();
730 player_id_t playerID
= m_Unit
->GetModel().GetPlayerID();
732 // Replace with the new unit
733 GetSimContext().GetUnitManager().DeleteUnit(m_Unit
);
735 // HACK: selection shape needs template data, but rather than storing all that data
736 // in the component, we load the template here and pass it into a helper function
737 CmpPtr
<ICmpTemplateManager
> cmpTemplateManager(GetSystemEntity());
738 const CParamNode
* node
= cmpTemplateManager
->LoadLatestTemplate(GetEntityId());
739 ENSURE(node
&& node
->GetChild("VisualActor").IsOk());
741 InitSelectionShapeDescriptor(node
->GetChild("VisualActor"));
747 if (m_ModelTag
.valid())
749 CmpPtr
<ICmpUnitRenderer
> cmpModelRenderer(GetSystemEntity());
750 if (cmpModelRenderer
)
751 cmpModelRenderer
->RemoveUnit(m_ModelTag
);
752 m_ModelTag
= ICmpUnitRenderer::tag_t
{};
757 ReloadUnitAnimation();
759 m_Unit
->GetModel().SetShadingColor(shading
);
761 m_Unit
->GetModel().SetPlayerID(playerID
);
763 if (m_ModelTag
.valid())
765 CmpPtr
<ICmpUnitRenderer
> cmpModelRenderer(GetSystemEntity());
766 CBoundingBoxAligned bounds
= m_Unit
->GetModel().GetWorldBoundsRec();
767 CBoundingSphere boundSphere
= CBoundingSphere::FromSweptBox(bounds
);
768 cmpModelRenderer
->UpdateUnit(m_ModelTag
, m_Unit
, boundSphere
);
772 void CCmpVisualActor::ReloadUnitAnimation()
777 m_Unit
->SetEntitySelection(m_VariantSelections
);
779 if (!m_Unit
->GetAnimation())
782 m_Unit
->GetAnimation()->SetAnimationState(m_AnimName
, m_AnimOnce
, m_AnimSpeed
.ToFloat(), m_AnimDesync
.ToFloat(), m_SoundGroup
.c_str());
784 // We'll lose the exact synchronisation but we should at least make sure it's going at the correct rate
785 if (!m_AnimSyncRepeatTime
.IsZero())
786 m_Unit
->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime
.ToFloat());
787 if (!m_AnimSyncOffsetTime
.IsZero())
788 m_Unit
->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime
.ToFloat());