Add animation protocol: walk/idle selection, emote triggers, WASM exports

Implement the animation system from the Phase 1 plan:

Protocol: Add walkAnimId/idleAnimId fields to PlayerStateMsg (2 extra bytes
per 15Hz tick), add MSG_EMOTE (type 9) with EmoteMsg struct, and define
shared animation lookup tables (walk: 6 anims, idle: 3, emote: 2).

NetworkManager: Store local walk/idle animation indices, include them in
every state broadcast, handle incoming MSG_EMOTE by dispatching to the
target remote player's TriggerEmote(). Add SetWalkAnimation(),
SetIdleAnimation(), SendEmote(), GetPlayerCount() public API.

RemotePlayer: Replace per-animation raw pointers with AnimCache struct
and lazy m_animCacheMap (name -> ROI map, built on first use, cleared on
Despawn). UpdateFromNetwork() detects walk/idle ID changes and swaps the
active animation cache. UpdateAnimation() now has three states: moving
(configurable walk anim), emote (one-shot with duration tracking,
interrupted by movement), and idle (configurable idle anim after 2.5s
timeout). Add TriggerEmote() for one-shot emote playback.

WASM exports: mp_set_walk_animation(), mp_set_idle_animation(),
mp_trigger_emote(), mp_get_player_count() with EMSCRIPTEN_KEEPALIVE.
CMakeLists.txt adds EXPORTED_FUNCTIONS and EXPORTED_RUNTIME_METHODS
for Svelte ccall/cwrap access.

https://claude.ai/code/session_01BEYdu8gXr1QmYwzRRgaEA6
This commit is contained in:
Claude 2026-03-02 03:06:48 +00:00
parent 4ad835271e
commit 3e85941cbc
No known key found for this signature in database
7 changed files with 334 additions and 68 deletions

View File

@ -15,6 +15,8 @@ endif()
if (EMSCRIPTEN)
add_compile_options(-pthread)
add_link_options(-sUSE_WEBGL2=1 -sMIN_WEBGL_VERSION=2 -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=2gb -sUSE_PTHREADS=1 -sPROXY_TO_PTHREAD=1 -sOFFSCREENCANVAS_SUPPORT=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sFORCE_FILESYSTEM=1 -sWASMFS=1 -sEXIT_RUNTIME=1)
add_link_options("-sEXPORTED_FUNCTIONS=[\"_main\",\"_mp_set_walk_animation\",\"_mp_set_idle_animation\",\"_mp_trigger_emote\",\"_mp_get_player_count\"]")
add_link_options("-sEXPORTED_RUNTIME_METHODS=[\"ccall\",\"cwrap\"]")
set(SDL_PTHREADS ON CACHE BOOL "Enable SDL pthreads" FORCE)
endif()

View File

@ -41,6 +41,11 @@ class NetworkManager : public MxCore {
bool IsConnected() const;
bool WasRejected() const;
void SetWalkAnimation(uint8_t p_index);
void SetIdleAnimation(uint8_t p_index);
void SendEmote(uint8_t p_emoteId);
int GetPlayerCount() const;
void OnWorldEnabled(LegoWorld* p_world);
void OnWorldDisabled(LegoWorld* p_world);
@ -61,6 +66,7 @@ class NetworkManager : public MxCore {
void HandleLeave(const PlayerLeaveMsg& p_msg);
void HandleState(const PlayerStateMsg& p_msg);
void HandleHostAssign(const HostAssignMsg& p_msg);
void HandleEmote(const EmoteMsg& p_msg);
void RemoveRemotePlayer(uint32_t p_peerId);
void RemoveAllRemotePlayers();
@ -80,6 +86,8 @@ class NetworkManager : public MxCore {
uint32_t m_sequence;
uint32_t m_lastBroadcastTime;
uint8_t m_lastValidActorId;
uint8_t m_localWalkAnimId;
uint8_t m_localIdleAnimId;
bool m_inIsleWorld;
bool m_registered;

View File

@ -17,6 +17,7 @@ enum MessageType : uint8_t {
MSG_WORLD_SNAPSHOT = 6,
MSG_WORLD_EVENT = 7,
MSG_WORLD_EVENT_REQUEST = 8,
MSG_EMOTE = 9,
MSG_ASSIGN_ID = 0xFF
};
@ -76,6 +77,8 @@ struct PlayerStateMsg {
float direction[3];
float up[3];
float speed;
uint8_t walkAnimId; // Index into walk animation table (0 = default)
uint8_t idleAnimId; // Index into idle animation table (0 = default)
};
// Server -> all: announces which peer is the host
@ -116,8 +119,40 @@ struct WorldEventRequestMsg {
uint8_t padding; // Alignment
};
// One-shot emote trigger, broadcast to all peers
struct EmoteMsg {
MessageHeader header;
uint8_t emoteId; // Index into emote table
};
#pragma pack(pop)
// Walk animation table: index -> CNs name
static const char* const g_walkAnimNames[] = {
"CNs001xx", // 0: Normal (default)
"CNs002xx", // 1: Joyful
"CNs003xx", // 2: Gloomy
"CNs005xx", // 3: Leaning
"CNs006xx", // 4: Scared
"CNs007xx", // 5: Hyper
};
static const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
// Idle animation table: index -> CNs name
static const char* const g_idleAnimNames[] = {
"CNs008xx", // 0: Sway (default)
"CNs009xx", // 1: Groove
"CNs010xx", // 2: Excited
};
static const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
// Emote table: index -> CNs name
static const char* const g_emoteAnimNames[] = {
"CNs011xx", // 0: Wave
"CNs012xx", // 1: Hat Tip
};
static const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]);
// Validate actorId is a playable character (1-5, not brickster)
inline bool IsValidActorId(uint8_t p_actorId)
{

View File

@ -4,6 +4,8 @@
#include "mxtypes.h"
#include <cstdint>
#include <map>
#include <string>
class LegoROI;
class LegoWorld;
@ -32,9 +34,49 @@ class RemotePlayer {
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
void SetVisible(bool p_visible);
void TriggerEmote(uint8_t p_emoteId);
private:
void BuildWalkROIMap(LegoWorld* p_isleWorld);
// Cached ROI map entry for an animation
struct AnimCache {
LegoAnim* anim;
LegoROI** roiMap;
MxU32 roiMapSize;
AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {}
~AnimCache()
{
if (roiMap) {
delete[] roiMap;
}
}
AnimCache(const AnimCache&) = delete;
AnimCache& operator=(const AnimCache&) = delete;
AnimCache(AnimCache&& p_other) noexcept
: anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize)
{
p_other.roiMap = nullptr;
p_other.roiMapSize = 0;
p_other.anim = nullptr;
}
AnimCache& operator=(AnimCache&& p_other) noexcept
{
if (this != &p_other) {
if (roiMap) {
delete[] roiMap;
}
anim = p_other.anim;
roiMap = p_other.roiMap;
roiMapSize = p_other.roiMapSize;
p_other.roiMap = nullptr;
p_other.roiMapSize = 0;
p_other.anim = nullptr;
}
return *this;
}
};
void BuildROIMap(
LegoAnim* p_anim,
LegoROI* p_rootROI,
@ -42,6 +84,7 @@ class RemotePlayer {
LegoROI**& p_roiMap,
MxU32& p_roiMapSize
);
AnimCache* GetOrBuildAnimCache(const char* p_animName);
void UpdateTransform(float p_deltaTime);
void UpdateAnimation(float p_deltaTime);
void UpdateVehicleState();
@ -69,18 +112,26 @@ class RemotePlayer {
float m_currentDirection[3];
float m_currentUp[3];
LegoAnim* m_walkAnim;
LegoROI** m_walkRoiMap;
MxU32 m_walkRoiMapSize;
// Animation state
uint8_t m_walkAnimId;
uint8_t m_idleAnimId;
AnimCache* m_walkAnimCache;
AnimCache* m_idleAnimCache;
float m_animTime;
float m_idleTime;
float m_idleAnimTime;
bool m_wasMoving;
LegoAnim* m_idleAnim;
LegoROI** m_idleRoiMap;
MxU32 m_idleRoiMapSize;
float m_idleAnimTime;
// Emote state
AnimCache* m_emoteAnimCache;
float m_emoteTime;
float m_emoteDuration;
bool m_emoteActive;
// ROI map cache: animation name -> cached ROI map (invalidated on world change)
std::map<std::string, AnimCache> m_animCacheMap;
// Ride animation (vehicle-specific, not cached globally)
LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize;

View File

@ -10,6 +10,7 @@
#include "misc.h"
#ifdef __EMSCRIPTEN__
#include "extensions/multiplayer/websockettransport.h"
#include <emscripten.h>
#endif
using namespace Extensions;
@ -124,3 +125,42 @@ bool Extensions::IsMultiplayerRejected()
{
return Extension<MultiplayerExt>::Call(CheckRejected).value_or(FALSE);
}
#ifdef __EMSCRIPTEN__
extern "C" {
EMSCRIPTEN_KEEPALIVE void mp_set_walk_animation(int index)
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
mgr->SetWalkAnimation(static_cast<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE void mp_set_idle_animation(int index)
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
mgr->SetIdleAnimation(static_cast<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE void mp_trigger_emote(int index)
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
mgr->SendEmote(static_cast<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE int mp_get_player_count()
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
return mgr->GetPlayerCount();
}
return 0;
}
} // extern "C"
#endif

View File

@ -30,7 +30,7 @@ void NetworkManager::SendMessage(const T& p_msg)
NetworkManager::NetworkManager()
: m_transport(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), m_lastBroadcastTime(0),
m_lastValidActorId(0), m_inIsleWorld(false), m_registered(false)
m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), m_inIsleWorld(false), m_registered(false)
{
}
@ -212,6 +212,8 @@ void NetworkManager::BroadcastLocalState()
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
SDL_memcpy(msg.up, up, sizeof(msg.up));
msg.speed = speed;
msg.walkAnimId = m_localWalkAnimId;
msg.idleAnimId = m_localIdleAnimId;
SendMessage(msg);
}
@ -291,6 +293,13 @@ void NetworkManager::ProcessIncomingPackets()
}
break;
}
case MSG_EMOTE: {
EmoteMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_EMOTE) {
HandleEmote(msg);
}
break;
}
default:
break;
}
@ -378,6 +387,47 @@ void NetworkManager::HandleHostAssign(const HostAssignMsg& p_msg)
}
}
void NetworkManager::SetWalkAnimation(uint8_t p_index)
{
if (p_index < g_walkAnimCount) {
m_localWalkAnimId = p_index;
}
}
void NetworkManager::SetIdleAnimation(uint8_t p_index)
{
if (p_index < g_idleAnimCount) {
m_localIdleAnimId = p_index;
}
}
void NetworkManager::SendEmote(uint8_t p_emoteId)
{
if (p_emoteId >= g_emoteAnimCount) {
return;
}
EmoteMsg msg{};
msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++};
msg.emoteId = p_emoteId;
SendMessage(msg);
}
int NetworkManager::GetPlayerCount() const
{
// +1 for the local player
return static_cast<int>(m_remotePlayers.size()) + 1;
}
void NetworkManager::HandleEmote(const EmoteMsg& p_msg)
{
uint32_t peerId = p_msg.header.peerId;
auto it = m_remotePlayers.find(peerId);
if (it != m_remotePlayers.end()) {
it->second->TriggerEmote(p_msg.emoteId);
}
}
void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
{
auto it = m_remotePlayers.find(p_peerId);

View File

@ -39,10 +39,10 @@ static bool IsLargeVehicle(int8_t p_vehicleType)
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
m_hasReceivedUpdate(false), m_walkAnim(nullptr), m_walkRoiMap(nullptr), m_walkRoiMapSize(0), m_animTime(0.0f),
m_idleTime(0.0f), m_wasMoving(false), m_idleAnim(nullptr), m_idleRoiMap(nullptr), m_idleRoiMapSize(0),
m_idleAnimTime(0.0f), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr),
m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE)
m_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr),
m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr),
m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE)
{
SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId);
@ -92,15 +92,9 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
m_spawned = true;
m_visible = false;
BuildWalkROIMap(p_isleWorld);
MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx");
if (idlePresenter) {
m_idleAnim = static_cast<LegoAnimPresenter*>(idlePresenter)->GetAnimation();
if (m_idleAnim) {
BuildROIMap(m_idleAnim, m_roi, nullptr, m_idleRoiMap, m_idleRoiMapSize);
}
}
// Build initial walk and idle animation caches
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
}
void RemotePlayer::Despawn()
@ -117,19 +111,13 @@ void RemotePlayer::Despawn()
m_roi = nullptr;
}
if (m_walkRoiMap) {
delete[] m_walkRoiMap;
m_walkRoiMap = nullptr;
m_walkRoiMapSize = 0;
}
if (m_idleRoiMap) {
delete[] m_idleRoiMap;
m_idleRoiMap = nullptr;
m_idleRoiMapSize = 0;
}
// Clear all cached animation ROI maps (anim pointers are world-owned, not ours)
m_animCacheMap.clear();
m_walkAnimCache = nullptr;
m_idleAnimCache = nullptr;
m_emoteAnimCache = nullptr;
m_emoteActive = false;
m_walkAnim = nullptr;
m_idleAnim = nullptr;
m_spawned = false;
m_visible = false;
}
@ -152,6 +140,18 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
SET3(m_currentUp, m_targetUp);
m_hasReceivedUpdate = true;
}
// Swap walk animation if changed
if (p_msg.walkAnimId != m_walkAnimId && p_msg.walkAnimId < g_walkAnimCount) {
m_walkAnimId = p_msg.walkAnimId;
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
}
// Swap idle animation if changed
if (p_msg.idleAnimId != m_idleAnimId && p_msg.idleAnimId < g_idleAnimCount) {
m_idleAnimId = p_msg.idleAnimId;
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
}
}
void RemotePlayer::Tick(float p_deltaTime)
@ -211,24 +211,62 @@ void RemotePlayer::SetVisible(bool p_visible)
}
}
void RemotePlayer::BuildWalkROIMap(LegoWorld* p_isleWorld)
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
{
if (!p_isleWorld) {
return;
if (!p_animName || !m_roi) {
return nullptr;
}
MxCore* presenter = p_isleWorld->Find("LegoAnimPresenter", "CNs001xx");
// Check if already cached
auto it = m_animCacheMap.find(p_animName);
if (it != m_animCacheMap.end()) {
return &it->second;
}
// Look up the animation presenter in the current world
LegoWorld* world = CurrentWorld();
if (!world) {
return nullptr;
}
MxCore* presenter = world->Find("LegoAnimPresenter", p_animName);
if (!presenter) {
return nullptr;
}
LegoAnim* anim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
if (!anim) {
return nullptr;
}
// Build and cache
AnimCache& cache = m_animCacheMap[p_animName];
cache.anim = anim;
BuildROIMap(anim, m_roi, nullptr, cache.roiMap, cache.roiMapSize);
return &cache;
}
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
{
if (p_emoteId >= g_emoteAnimCount || !m_spawned) {
return;
}
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter);
m_walkAnim = animPresenter->GetAnimation();
if (!m_walkAnim) {
// Only play emotes when stationary
if (m_targetSpeed > 0.01f) {
return;
}
BuildROIMap(m_walkAnim, m_roi, nullptr, m_walkRoiMap, m_walkRoiMapSize);
AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]);
if (!cache || !cache->anim) {
return;
}
m_emoteAnimCache = cache;
m_emoteTime = 0.0f;
m_emoteDuration = (float) cache->anim->GetDuration();
m_emoteActive = true;
}
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
@ -334,59 +372,101 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
void RemotePlayer::UpdateAnimation(float p_deltaTime)
{
LegoAnim* anim = nullptr;
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
return;
}
LegoROI** roiMap = nullptr;
// Determine the active walk/ride animation and its ROI map
LegoAnim* walkAnim = nullptr;
LegoROI** walkRoiMap = nullptr;
MxU32 walkRoiMapSize = 0;
if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) {
anim = m_rideAnim;
roiMap = m_rideRoiMap;
walkAnim = m_rideAnim;
walkRoiMap = m_rideRoiMap;
walkRoiMapSize = m_rideRoiMapSize;
}
else if (m_walkAnim && m_walkRoiMap) {
anim = m_walkAnim;
roiMap = m_walkRoiMap;
}
else {
return;
else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) {
walkAnim = m_walkAnimCache->anim;
walkRoiMap = m_walkAnimCache->roiMap;
walkRoiMapSize = m_walkAnimCache->roiMapSize;
}
MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize;
for (MxU32 i = 1; i < roiMapSize; i++) {
if (roiMap[i] != nullptr) {
roiMap[i]->SetVisibility(TRUE);
// Ensure visibility of all mapped ROIs
if (walkRoiMap) {
for (MxU32 i = 1; i < walkRoiMapSize; i++) {
if (walkRoiMap[i] != nullptr) {
walkRoiMap[i]->SetVisibility(TRUE);
}
}
}
for (MxU32 i = 1; i < m_idleRoiMapSize; i++) {
if (m_idleRoiMap[i] != nullptr) {
m_idleRoiMap[i]->SetVisibility(TRUE);
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
for (MxU32 i = 1; i < m_idleAnimCache->roiMapSize; i++) {
if (m_idleAnimCache->roiMap[i] != nullptr) {
m_idleAnimCache->roiMap[i]->SetVisibility(TRUE);
}
}
}
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
bool isMoving = inVehicle || m_targetSpeed > 0.01f;
// Movement interrupts emotes
if (isMoving && m_emoteActive) {
m_emoteActive = false;
m_emoteAnimCache = nullptr;
}
if (isMoving) {
// Walking / riding
if (!walkAnim || !walkRoiMap) {
return;
}
if (inVehicle || m_targetSpeed > 0.01f) {
if (m_targetSpeed > 0.01f) {
m_animTime += p_deltaTime * 2000.0f;
}
float duration = (float) anim->GetDuration();
float duration = (float) walkAnim->GetDuration();
if (duration > 0.0f) {
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
MxMatrix transform(m_roi->GetLocal2World());
LegoTreeNode* root = anim->GetRoot();
LegoTreeNode* root = walkAnim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, roiMap);
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, walkRoiMap);
}
}
m_wasMoving = true;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
}
else if (m_idleAnim && m_idleRoiMap) {
else if (m_emoteActive && m_emoteAnimCache && m_emoteAnimCache->anim && m_emoteAnimCache->roiMap) {
// Emote playback (one-shot)
m_emoteTime += p_deltaTime * 1000.0f;
if (m_emoteTime >= m_emoteDuration) {
// Emote completed -- return to stationary flow
m_emoteActive = false;
m_emoteAnimCache = nullptr;
m_wasMoving = false;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
}
else {
MxMatrix transform(m_roi->GetLocal2World());
LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(
root->GetChild(i),
transform,
(LegoTime) m_emoteTime,
m_emoteAnimCache->roiMap
);
}
}
}
else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) {
// Idle animation
if (m_wasMoving) {
m_wasMoving = false;
m_idleTime = 0.0f;
@ -400,18 +480,18 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
m_idleAnimTime += p_deltaTime * 1000.0f;
}
float duration = (float) m_idleAnim->GetDuration();
float duration = (float) m_idleAnimCache->anim->GetDuration();
if (duration > 0.0f) {
float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration);
MxMatrix transform(m_roi->GetLocal2World());
LegoTreeNode* root = m_idleAnim->GetRoot();
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(
root->GetChild(i),
transform,
(LegoTime) timeInCycle,
m_idleRoiMap
m_idleAnimCache->roiMap
);
}
}