/* ** Command & Conquer Renegade(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program 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 3 of the License, or ** (at your option) any later version. ** ** This program 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 this program. If not, see . */ /*********************************************************************************************** *** Confidential - Westwood Studios *** *********************************************************************************************** * * * Project Name : Commando * * * * $Archive:: /Commando/Code/Combat/animcontrol.cpp $* * * * $Author:: Byon_g $* * * * $Modtime:: 1/04/02 10:26a $* * * * $Revision:: 53 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "animcontrol.h" #include "assets.h" #include "debug.h" #include "rendobj.h" #include "chunkio.h" #include "saveload.h" #include "htree.h" /* ** */ enum { XXXCHUNKID_LEGS = 910991512, XXXCHUNKID_TORSO, CHUNKID_CHANNEL, CHUNKID_VARIABLES, CHUNKID_OLD, CHUNKID_NEW, CHUNKID_PARENT, CHUNKID_CHANNEL1, CHUNKID_CHANNEL2, MICROCHUNKID_BLEND_TIMER = 1, MICROCHUNKID_BLEND_TOTAL, MICROCHUNKID_FRAME, XXXMICROCHUNKID_WEIGHT, MICROCHUNKID_MODE, MICROCHUNKID_ANIMATION_NAME, MICROCHUNKID_MODEL_PTR, MICROCHUNKID_CHANNEL2_RATIO, MICROCHUNKID_TARGET_FRAME, MICROCHUNKID_SKELETON, }; /* ** AnimChannelClass */ AnimChannelClass::AnimChannelClass( void ) : Animation( NULL ), Frame( 0.0f ), NumFrames( 1 ), TargetFrame( 0.0f ), Mode( ANIM_MODE_ONCE ) { } AnimChannelClass::~AnimChannelClass( void ) { if ( Animation ) { Animation->Release_Ref(); Animation = NULL; } } AnimChannelClass & AnimChannelClass::operator = (const AnimChannelClass & src) { if (Animation != NULL) { Animation->Release_Ref(); } Animation = src.Animation; if (Animation != NULL) { Animation->Add_Ref(); } Frame = src.Frame; NumFrames = src.NumFrames; Mode = src.Mode; TargetFrame = src.TargetFrame; return *this; } bool AnimChannelClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_VARIABLES ); // save the anim first, because the load will stomp the frame & weight if ( Animation ) { csave.Begin_Micro_Chunk( MICROCHUNKID_ANIMATION_NAME ); const char * anim_name = Animation->Get_Name(); csave.Write( anim_name, strlen( anim_name ) + 1); csave.End_Micro_Chunk(); } WRITE_MICRO_CHUNK( csave, MICROCHUNKID_FRAME, Frame ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_MODE, Mode ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TARGET_FRAME, TargetFrame ); csave.End_Chunk(); return true; } bool AnimChannelClass::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_FRAME, Frame ); READ_MICRO_CHUNK( cload, MICROCHUNKID_MODE, Mode ); READ_MICRO_CHUNK( cload, MICROCHUNKID_TARGET_FRAME, TargetFrame ); case MICROCHUNKID_ANIMATION_NAME: { char anim_name[80]; cload.Read( anim_name, cload.Cur_Micro_Chunk_Length() ); Set_Animation( anim_name ); break; } default: Debug_Say(("Unhandled Micro Chunk:%d File:%s Line:%d\r\n",cload.Cur_Micro_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Micro_Chunk(); } break; default: Debug_Say(("Unhandled Chunk:%d File:%s Line:%d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Chunk(); } return true; } void AnimChannelClass::Set_Animation( const char *name ) { // If this is our current anim, bail if ( ( Animation != NULL ) && ( name != NULL ) ) { if ( stricmp( Animation->Get_Name(), name ) == 0 ) { return; } } if ( ( Animation == NULL ) && ( name == NULL ) ) { return; } // Release the old anim if ( Animation ) { Animation->Release_Ref(); Animation = NULL; } // we need to switch anims if (( name != NULL ) && ( name[0] != 0 ) ) { Animation = WW3DAssetManager::Get_Instance()->Get_HAnim( name ); } if ( Animation ) { SET_REF_OWNER( Animation ); NumFrames = Animation->Get_Num_Frames(); Mode = ANIM_MODE_ONCE; Frame = 0; TargetFrame = 0; } } void AnimChannelClass::Set_Animation( const HAnimClass *anim ) { // If this is our current anim, bail if ( Animation == anim ) { return; } // Release the old anim if ( Animation ) { Animation->Release_Ref(); Animation = NULL; } // we need to switch anims Animation = (HAnimClass *)anim; if ( Animation ) { Animation->Add_Ref(); NumFrames = Animation->Get_Num_Frames(); Mode = ANIM_MODE_ONCE; Frame = 0; TargetFrame = 0; } } void AnimChannelClass::Set_Mode( AnimMode mode, float frame ) { Mode = mode; if ( frame >= 0 ) { Frame = frame; } } void AnimChannelClass::Update( float dtime ) { if ( Mode == ANIM_MODE_STOP ) { return; } if ( Animation != NULL ) { switch ( Mode ) { case ANIM_MODE_LOOP: // // Increment the frame based on the current timeslice // Frame += dtime * Animation->Get_Frame_Rate(); // // Handle wrapping // if ( Frame >= NumFrames-1 ) { Frame -= NumFrames-1; } if ( Frame >= NumFrames ) { Frame = 0; } break; case ANIM_MODE_TARGET: // // Which direction are we animating? // if ( Frame < TargetFrame ) { Frame += dtime * Animation->Get_Frame_Rate(); // // If we overshoot targetframe, snap to targetframe // if (Frame >= TargetFrame) { Frame = TargetFrame; } } else if ( Frame > TargetFrame ) { Frame -= dtime * Animation->Get_Frame_Rate(); // // If we overshoot targetframe, snap to targetframe // if ( Frame <= TargetFrame ) { Frame = TargetFrame; } } break; case ANIM_MODE_ONCE: // // Increment the frame based on the current timeslice // Frame += dtime * Animation->Get_Frame_Rate(); // // Make sure we don't go past the end // if ( Frame > NumFrames-1 ) { Frame = NumFrames-1; } break; } #if 0 if ( dtime != 0 ) { Debug_Say(( "Anim %s frame %1.3f\n", Animation->Get_Name(), Frame )); } #endif } } void AnimChannelClass::Get_Animation_Data( AnimationDataList & list, float weight ) { if ( Animation != NULL && weight > 0 ) { AnimationDataRecord * record = list.Uninitialized_Add(); WWASSERT( record != NULL ); record->Animation = Animation; record->Frame = Frame; record->Weight = weight; } } void AnimChannelClass::Update_Model( RenderObjClass *anim_model ) { if ( Animation ) { anim_model->Set_Animation( Animation, Frame ); } else { anim_model->Set_Animation(); } } /* ** BlendableAnimChannelClass */ BlendableAnimChannelClass::BlendableAnimChannelClass( void ) : BlendTimer( 0 ), BlendTotal( 0 ) { } bool BlendableAnimChannelClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_NEW ); NewChannel.Save( csave ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_OLD ); OldChannel.Save( csave ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_VARIABLES ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_BLEND_TIMER, BlendTimer ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_BLEND_TOTAL, BlendTotal ); csave.End_Chunk(); return true; } bool BlendableAnimChannelClass::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_NEW: NewChannel.Load( cload ); break; case CHUNKID_OLD: OldChannel.Load( cload ); break; case CHUNKID_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_BLEND_TIMER, BlendTimer ); READ_MICRO_CHUNK( cload, MICROCHUNKID_BLEND_TOTAL, BlendTotal ); default: Debug_Say(( "Unrecognized BlendableAnimChannel Variable chunkID\n" )); break; } cload.Close_Micro_Chunk(); } break; default: Debug_Say(( "Unrecognized BlendableAnimChannel chunkID\n" )); break; } cload.Close_Chunk(); } return true; } void BlendableAnimChannelClass::Set_Animation( const char *name, float blendtime, float start_frame ) { // if setting to our current anim, bail if ( ( NewChannel.Peek_Animation() == NULL ) && ( name == NULL ) ) { return; } if ( ( NewChannel.Peek_Animation() != NULL ) && ( name != NULL ) ) { if ( stricmp( NewChannel.Peek_Animation()->Get_Name(), name ) == 0 ) { return; } } // if no current channel, or no blend, or no new name, don't blend if ( (NewChannel.Peek_Animation() == NULL) || (blendtime == 0) || (name == NULL) ) { BlendTotal = 0.0f; BlendTimer = 0.0f; } else if ( BlendTotal == 0.0 ) { //if not currently blending OldChannel = NewChannel; BlendTimer = 0.0f; BlendTotal = blendtime; #if 0 } else if ( OldChannel.Peek_Animation() == new_anim.Peek_Animation() ) { // if old anim is the new one OldChannel.Copy_From( NewChannel ); BlendTimer = (1.0f - (BlendTimer / BlendTotal)) * blendtime; BlendTotal = blendtime; #endif } else if ( (BlendTimer / BlendTotal) > 0.5 ) { // if more than halfway through the old blend OldChannel = NewChannel; BlendTimer = (1.0f - (BlendTimer / BlendTotal)) * blendtime; BlendTotal = blendtime; } else { BlendTimer = (BlendTimer / BlendTotal) * blendtime; BlendTotal = blendtime; } NewChannel.Set_Animation( name ); if ( NewChannel.Peek_Animation() != NULL ) { NewChannel.Set_Frame( start_frame ); } if ( name == NULL ) { OldChannel.Set_Animation( (const char *)NULL ); } } void BlendableAnimChannelClass::Set_Animation( const HAnimClass * anim, float blendtime, float start_frame ) { // if setting to our current anim, bail if ( ( NewChannel.Peek_Animation() == NULL ) && ( anim == NULL ) ) { return; } if ( ( NewChannel.Peek_Animation() != NULL ) && ( anim != NULL ) ) { if ( NewChannel.Peek_Animation() == anim ) { return; } } // if no current channel, or no blend, or no new name, don't blend if ( (NewChannel.Peek_Animation() == NULL) || (blendtime == 0) || (anim == NULL) ) { BlendTotal = 0.0f; BlendTimer = 0.0f; } else if ( BlendTotal == 0.0 ) { //if not currently blending OldChannel = NewChannel; BlendTimer = 0.0f; BlendTotal = blendtime; #if 0 } else if ( OldChannel.Peek_Animation() == new_anim.Peek_Animation() ) { // if old anim is the new one OldChannel.Copy_From( NewChannel ); BlendTimer = (1.0f - (BlendTimer / BlendTotal)) * blendtime; BlendTotal = blendtime; #endif } else if ( (BlendTimer / BlendTotal) > 0.5 ) { // if more than halfway through the old blend OldChannel = NewChannel; BlendTimer = (1.0f - (BlendTimer / BlendTotal)) * blendtime; BlendTotal = blendtime; } else { BlendTimer = (BlendTimer / BlendTotal) * blendtime; BlendTotal = blendtime; } NewChannel.Set_Animation( anim ); if ( NewChannel.Peek_Animation() != NULL ) { NewChannel.Set_Frame( start_frame ); } if ( anim == NULL ) { OldChannel.Set_Animation( (const HAnimClass *)NULL ); } } void BlendableAnimChannelClass::Update( float dtime ) { if ( BlendTotal != 0.0f ) { // if blending between two animations BlendTimer += dtime; // Bump blend timer forward if ( BlendTimer >= BlendTotal ) // blend complete, remove oldanim { BlendTotal = 0.0f; BlendTimer = 0.0f; OldChannel.Set_Animation( (const char *)NULL ); } } // Calculate which frame we are on in each of the animations NewChannel.Update( dtime ); OldChannel.Update( dtime ); } void BlendableAnimChannelClass::Get_Animation_Data( AnimationDataList & list, float weight ) { float blend_ratio = 1.0f; // assume no blending if ( BlendTotal != 0.0f ) { // if blending between two animations // Calculate the blend percentage between the two animations. // This starts at 0.0 (all OldAnimation) and proceeds to 1.0 (all Animation) blend_ratio = WWMath::Clamp( BlendTimer / BlendTotal, 0, 1 ); } NewChannel.Get_Animation_Data( list, weight * blend_ratio ); OldChannel.Get_Animation_Data( list, weight * ( 1 - blend_ratio ) ); } void BlendableAnimChannelClass::Update_Model( RenderObjClass *anim_model ) { float blend_ratio = 1.0f; // assume no blending if ( BlendTotal != 0.0f ) { // if blending between two animations // Calculate the blend percentage between the two animations. // This starts at 0.0 (all OldAnimation) and proceeds to 1.0 (all Animation) blend_ratio = WWMath::Clamp( BlendTimer / BlendTotal, 0, 1 ); } if ( OldChannel.Peek_Animation() ) { anim_model->Set_Animation( OldChannel.Peek_Animation(), OldChannel.Get_Frame(), NewChannel.Peek_Animation(), NewChannel.Get_Frame(), blend_ratio ); } else if ( NewChannel.Peek_Animation() ) { anim_model->Set_Animation( NewChannel.Peek_Animation(), NewChannel.Get_Frame() ); } else { anim_model->Set_Animation(); } } /* ** AnimControlClass */ AnimControlClass::AnimControlClass( void ) : Model( NULL ) { } AnimControlClass::~AnimControlClass( void ) { REF_PTR_RELEASE( Model ); } bool AnimControlClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_VARIABLES ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_MODEL_PTR, Model ); csave.End_Chunk(); return true; } bool AnimControlClass::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_VARIABLES: WWASSERT( Model == NULL ); while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_MODEL_PTR, Model ); default: Debug_Say(( "Unrecognized AnimControl Variable chunkID\n" )); break; } cload.Close_Micro_Chunk(); } if ( Model != NULL ) { REQUEST_REF_COUNTED_POINTER_REMAP( (RefCountClass **)&Model ); } else { Debug_Say(( "Loaded NULL model\n" )); } break; default: Debug_Say(( "Unrecognized AnimControl chunkID\n" )); break; } cload.Close_Chunk(); } return true; } void AnimControlClass::Set_Model( RenderObjClass *anim_model ) { REF_PTR_SET( Model, anim_model ); } /* ** SimpleAnimControlClass */ SimpleAnimControlClass::SimpleAnimControlClass( void ) { } SimpleAnimControlClass::~SimpleAnimControlClass( void ) { } bool SimpleAnimControlClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_PARENT ); AnimControlClass::Save( csave ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_CHANNEL ); Channel.Save( csave ); csave.End_Chunk(); return true; } bool SimpleAnimControlClass::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_PARENT: AnimControlClass::Load( cload ); break; case CHUNKID_CHANNEL: Channel.Load( cload ); break; default: Debug_Say(( "Unrecognized HumanAnimControl chunkID\n" )); break; } cload.Close_Chunk(); } return true; } void SimpleAnimControlClass::Set_Animation( const char *name, float blendtime, float start_frame ) { Channel.Set_Animation( name, blendtime, start_frame ); } void SimpleAnimControlClass::Set_Animation( const HAnimClass * anim, float blendtime, float start_frame ) { Channel.Set_Animation( anim, blendtime, start_frame ); } void SimpleAnimControlClass::Update( float dtime ) { Channel.Update( dtime ); // Setup the model for the current frame(s) assert( Model != NULL ); Channel.Update_Model( Model ); } /* ** HumanAnimControlClass */ HumanAnimControlClass::HumanAnimControlClass( void ) : AnimCombo( 2 ), Channel2Ratio( 0 ), Skeleton( 'A' ), AnimSpeedScale( 1 ) { } HumanAnimControlClass::~HumanAnimControlClass( void ) { } bool HumanAnimControlClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_PARENT ); AnimControlClass::Save( csave ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_VARIABLES ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_CHANNEL2_RATIO, Channel2Ratio ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_SKELETON, Skeleton ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_CHANNEL1 ); Channel1.Save( csave ); csave.End_Chunk(); csave.Begin_Chunk( CHUNKID_CHANNEL2 ); Channel2.Save( csave ); csave.End_Chunk(); // Don't need to save the animcombo return true; } bool HumanAnimControlClass::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_PARENT: AnimControlClass::Load( cload ); break; case CHUNKID_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_CHANNEL2_RATIO, Channel2Ratio ); READ_MICRO_CHUNK( cload, MICROCHUNKID_SKELETON, Skeleton ); default: Debug_Say(( "Unrecognized HumanAnimControl Variable chunkID\n" )); break; } cload.Close_Micro_Chunk(); } break; case CHUNKID_CHANNEL1: Channel1.Load( cload ); break; case CHUNKID_CHANNEL2: Channel2.Load( cload ); break; default: Debug_Say(( "Unrecognized HumanAnimControl chunkID\n" )); break; } cload.Close_Chunk(); } return true; } void HumanAnimControlClass::Build_Skeleton_Anim_Name( StringClass& new_name, const char * name ) { if (name == NULL) return; if (*name == NULL) { Debug_Say(( "No name in Build_Skeleton_Anim_Name\n" )); return; } new_name = name; if ( Skeleton == 'V' ) { // Special case for visceroids return; } // If the anim doesn't start with "S_A_HUMAN.", add it if ( ::strnicmp( name, "S_", 2 ) != 0 ) { new_name.Format( "S_%c_HUMAN.%s", Skeleton, name ); } // If the anim name is "S_A_HUMAN.H_A_*", and the Skeleton is not 'A', use // the other skeleton anim, if found if ( new_name.Get_Length() > 14 && Skeleton != 'A' && ::strnicmp( new_name, "S_A_HUMAN.H_A_", 14 ) == 0 ) { StringClass mod_name(new_name,true); mod_name[2] = Skeleton; mod_name[12] = Skeleton; // can we find the anim name? HAnimClass * anim = WW3DAssetManager::Get_Instance()->Get_HAnim( mod_name ); if ( anim != NULL ) { anim->Release_Ref(); new_name=mod_name; } } } void HumanAnimControlClass::Set_Animation( const char *name, float blendtime, float start_frame ) { StringClass new_name(0,true); Build_Skeleton_Anim_Name( new_name, name ); Channel1.Set_Animation( new_name, blendtime, start_frame ); Channel2.Set_Animation( (const char *)NULL ); Channel2Ratio = 0; } void HumanAnimControlClass::Set_Animation( const HAnimClass * anim, float blendtime, float start_frame ) { if ( anim != NULL ) { Set_Animation( anim->Get_Name(), blendtime, start_frame ); } else { Set_Animation( (const char *)NULL, blendtime, start_frame ); } } void HumanAnimControlClass::Set_Animation( const char *name1, const char * name2, float ratio, float blendtime ) { StringClass new_name1(0,true); Build_Skeleton_Anim_Name( new_name1, name1 ); StringClass new_name2(0,true); Build_Skeleton_Anim_Name( new_name2, name2 ); if ( ratio == 0 ) { Set_Animation( new_name1, blendtime ); return; } if ( Channel2Ratio == 0 ) { Channel2 = Channel1; } Channel1.Set_Animation( new_name1, blendtime ); Channel2.Set_Animation( new_name2, blendtime ); Channel2Ratio = ratio; } void HumanAnimControlClass::Set_Mode( AnimMode mode, float frame ) { Channel1.Set_Mode( mode, frame ); Channel2.Set_Mode( mode, frame ); } void HumanAnimControlClass::Set_Model( RenderObjClass *anim_model ) { AnimControlClass::Set_Model( anim_model ); // Update the skeleton if( anim_model != NULL ) { const HTreeClass * tree = anim_model->Get_HTree(); if ( tree != NULL ) { const char * name = tree->Get_Name(); Skeleton = name[2]; } } } /* ** */ StringClass _MonitorAnimDescription; HumanAnimControlClass * _Monitor = NULL; void HumanAnimControlClass::Update( float dtime ) { // Update channels Channel1.Update( dtime * AnimSpeedScale ); Channel2.Update( dtime * AnimSpeedScale ); if ( Model != NULL ) { // Get Animation data DataList.Reset_Active(); Channel1.Get_Animation_Data( DataList, (1 - Channel2Ratio) ); Channel2.Get_Animation_Data( DataList, Channel2Ratio ); // Use the cheapest anim method possible int total_animations = DataList.Count(); if ( total_animations == 0 ) { // Debug_Say(( "No animations to display!\n" )); Model->Set_Animation(); } else if ( total_animations == 1 ) { // Debug_Say(( "1 animation to display!\n" )); Model->Set_Animation( DataList[0].Animation, DataList[0].Frame ); } else if ( total_animations == 2 ) { // Debug_Say(( "2 animation to display!\n" )); float percent = DataList[1].Weight / (DataList[0].Weight + DataList[1].Weight); Model->Set_Animation( DataList[0].Animation, DataList[0].Frame, DataList[1].Animation, DataList[1].Frame, percent ); } else { // Debug_Say(( ">2 animation to display!\n" )); // set up anim combo AnimCombo.Reset(); for ( int i = 0; i < total_animations; i++ ) { HAnimComboDataClass * anim_data = new HAnimComboDataClass(); anim_data->Set_HAnim( DataList[i].Animation ); anim_data->Set_Frame( DataList[i].Frame ); anim_data->Set_Weight( DataList[i].Weight ); AnimCombo.Append_Anim_Combo_Data( anim_data ); } // Setup the model for the current frame(s) Model->Set_Animation( &AnimCombo ); } if ( _Monitor == this ) { for ( int i = 0; i < total_animations; i++ ) { if ( i == 0 ) { _MonitorAnimDescription = ""; } StringClass a(0,true); a.Format( "%s %1.0f%% %1.0f\n", DataList[i].Animation->Get_Name()+10, DataList[i].Weight*100, DataList[i].Frame ); _MonitorAnimDescription += a; } } } } /* ** */ void HumanAnimControlClass::Get_Information( StringClass & string ) { _Monitor = this; string += _MonitorAnimDescription; }