/* Copyright (C) 2012 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.anims = object->GetAnimations(m_State);
if (state.anims.empty())
state.anims = object->GetAnimations("idle");
ENSURE(!state.anims.empty()); // there must always be an idle animation
state.model = model;
state.animIdx = rand(0, state.anims.size());
state.time = 0.f;
state.pastLoadPos = false;
state.pastActionPos = false;
state.pastSoundPos = false;
m_AnimStates.push_back(state);
model->SetAnimation(state.anims[state.animIdx], !m_Looping);
// Detect if this unit has any non-static animations
for (size_t i = 0; i < state.anims.size(); i++)
if (state.anims[i]->m_AnimDef != NULL)
m_AnimStatesAreStatic = false;
// Recursively add all props
const std::vector& props = model->GetProps();
for (std::vector::const_iterator it = props.begin(); it != props.end(); ++it)
{
CModel* propModel = it->m_Model->ToCModel();
if (propModel)
AddModel(propModel, it->m_ObjectEntry);
}
}
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;
ReloadUnit(m_Model, m_Object);
}
}
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->anims[it->animIdx]->m_AnimDef;
if (animDef == NULL)
continue; // ignore static animations
float duration = animDef->GetDuration();
float actionPos = it->anims[it->animIdx]->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->anims[it->animIdx]->m_AnimDef;
if (animDef == NULL)
continue; // ignore static animations
float duration = animDef->GetDuration();
float actionPos = it->anims[it->animIdx]->m_ActionPos;
float loadPos = it->anims[it->animIdx]->m_ActionPos2;
float soundPos = it->anims[it->animIdx]->m_SoundPos;
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->anims[it->animIdx]->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);
// If there's a choice of multiple animations, pick a new random one
if (it->anims.size() > 1)
{
size_t newAnimIdx = rand(0, it->anims.size());
if (newAnimIdx != it->animIdx)
{
it->animIdx = newAnimIdx;
it->model->SetAnimation(it->anims[it->animIdx], !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);
}
}
}
}