Game Development Community

dev|Pro Game Development Curriculum

Triggering actions from an animation

by Richard Marrevee · 01/14/2012 (12:34 pm) · 18 comments

This resource was already posted some time ago by someone else (I'm sorry, but I don't recall who), but there seem to be a lot of interest in a method to synchronise animations and other actions like playing sounds at a certain frame of the animation. It requires some modification of the source code (in this case it was T3D 1.1 final, didn't had the time to port over to 1.2 don't know if I will for my current project The Masters Eye, but that shouldn't be to hard if not the same.

So let's start.

In shapebase.cpp find the method void ShapeBase::advanceThreads(F32 dt)

Now after:
updateThread(st);
            if (!isGhost()) {
               mDataBlock->onEndSequence_callback( this, i );
            }
         }
         mShapeInstance->advanceTime(dt,st.thread);
      }
   }

add:
if (mShapeInstance && !isGhost()) {
		for (U32 i = 1; i < 32; i++) {
			if (mShapeInstance->getTriggerState(i)) {
				char slot[3];
				dSprintf(slot,sizeof(slot),"%d",i);
				Con::executef(mDataBlock,"onAnimationTrigger",scriptThis(),slot);
			}
		}
	}

Now it seems that the scriptThis function is removed from source, so for those who are missing this function open sceneobject.cpp and add this at the end:
const char* SceneObject::scriptThis()
{
   return Con::getIntArg(getId());
}

And in sceneobject.h
// SimObject.
      virtual bool onAdd();
      virtual void onRemove();
      virtual void onDeleteNotify( SimObject *object );
      virtual void inspectPostApply();
      virtual bool writeField( StringTableEntry fieldName, const char* value );

      static void initPersistFields();

	  const char* scriptThis();// <== add this line

      DECLARE_CONOBJECT( SceneObject );

   private:

      SceneObject( const SceneObject& ); ///< @deprecated disallowed
Now rebuild the engine.

To use this you can add a trigger (1-31) to certain frame(s) of your shapebase derived objects animations (I use the shape-editor to do this).

Then write a script for this shapebase derived object like below:
function AISmith1::onAnimationTrigger(%this, %obj, %trigger)
{
   switch (%trigger)
   {
      case 5:
         %obj.playAudio(0,smithSound);
         
   }
}

In the above example I added trigger 5 to a certain frame of the AISmith1 model to trigger a sound (See vid below)



One final note:
Triggers with the same number but in different animations of an object will trigger the same action! In other words the triggers are per object, not per animation.

I hope you enjoy this.


About the author

Started programming in 1984 on an Oric, when time progressed switched to MSX, Amiga and finally the Windows PC with T3D. Now developing an epic fantasy game: The Master's Eye. Creator of the DoorClass pack and VolumetricFog pack @ richardsgamestudio.com


#1
01/14/2012 (4:05 pm)
You are my new coding hero! This is great.
#2
01/14/2012 (8:09 pm)
Nice work!

I think that the reason that you didn't find scriptThis is because they changed the API a bit.

Notice up around line 100 of shapebase.cpp:
IMPLEMENT_CALLBACK( ShapeBaseData, onEndSequence, void, ( ShapeBase* obj, S32 slot ), ( obj, slot ),
   "@brief Called when a thread playing a non-cyclic sequence reaches the end of the "
   "sequence.nn"
   "@param obj The ShapeBase objectn"
   "@param slot Thread slot that finished playingn" );

You see they have some callback macros setup. You could create one of these for your event and it should fit in with everything else.
#3
01/15/2012 (2:42 am)
It's very useful, great work ;)
#4
01/15/2012 (5:58 am)
very neat, thanks a lot :P
#6
01/19/2012 (1:42 am)
Wow, truly epic!
I will have to test whether this can "chain" in order to create "combo" style fighting.
Thank you. :)
#7
01/20/2012 (3:54 am)
No thanks guys, glad I could help.
#8
01/26/2012 (5:18 pm)
@Richard,

I have been trying to implement this into 1.2 but guess either something has changed to prevent it in 1.2 or I did something wrong. :(
#9
01/30/2012 (3:57 am)
@Donald,

I will take a look.
#10
01/30/2012 (8:48 am)
@Donald:

Just added this to 1.2 and it works as expected. Can you describe the problem?
#11
01/30/2012 (9:34 pm)
@Richard,

basically the action to the trigger never plays.

break points in the code show that mShapeInstance->getTriggerState(i)) is always false in the following section.

if (mShapeInstance && !isGhost()) {
		for (U32 i = 1; i < 32; i++) {
			if (mShapeInstance->getTriggerState(i)) {
				char slot[3];
				dSprintf(slot,sizeof(slot),"%d",i);
				Con::executef(mDataBlock,"onAnimationTrigger",scriptThis(),slot);
			}
		}
	}

and when I follow (mShapeInstance->getTriggerState(i))

bool TSShapeInstance::getTriggerState(U32 stateNum, bool clearState)
{
   AssertFatal(stateNum<=32 && stateNum>0,"TSShapeInstance::getTriggerState: state index out of range");

   stateNum--; // stateNum externally 1..32, internally 0..31
   U32 bit = 1 << stateNum;
   bool ret = ((mTriggerStates & bit)!=0);
   if (clearState)
      mTriggerStates &= ~bit;
   return ret;
}


bool ret =((mTriggerStates & bit) !=0); is false


i created triggers in the shape editor

[code]function LichbaseDAE::onLoad(%this)
{

%this.addTrigger("root", "6", "2");
%this.addTrigger("root", "41", "3");
%this.addTrigger("root", "63", "4");
%this.addTrigger("root", "86", "5");
%this.addTrigger("root", "103", "6");
%this.addTrigger("root", "121", "7");
%this.addTrigger("root", "153", "8");
%this.addTrigger("root", "155", "9");
%this.addTrigger("ambient", "13", "3");
%this.addTrigger("ambient", "32", "4");
%this.addTrigger("ambient", "53", "5");
%this.addTrigger("ambient", "91", "7");
%this.addTrigger("ambient", "132", "8");
%this.addTrigger("ambient", "155", "9");
}

so not sure what is happening
#12
01/31/2012 (11:25 am)
@Donald:

I've tried to reproduce your problem, but I can't find anything which is causing your problem.

Do you have the following function ...onAnimationTrigger(...) addition made in your script (just as an example to show how to use):

datablock PlayerData(AIBeggar1 : DefaultPlayerData)
{
   shapeFile = "art/shapes/actors/beggar/beggar1.dts";
   boundingBox = "1 1 2.75";

   maxForwardSpeed = 7;
   maxBackwardSpeed = 7;
   maxSideSpeed = 3;

   maxDamage= "30";
   kill_score = "-15";
   kill_reputation = "-15";
      
   maxDeathAnims = 1;
};

function AIBeggar1::onAnimationTrigger(%this, %obj, %trigger)
{
   switch (%trigger)
   {
      case 5:
         echo("Trigger 5 AIBeggar");
         %obj.playAudio(0,smithSound);
         
   }
}

#13
02/01/2012 (8:21 pm)
@Richard,

this is what I have in my datablock,

from break points in Torsion, function onAnimationTrigger is never being reached,



[code]
datablock PlayerData(LichData : DefaultPlayerData)
{
shapeFile = "art/shapes/actors/Lich/LichBase.DAE";
boundingBox = "0.75 0.8 1.3";
swimBoundingBox = "0.75 1.5 1.15";
shootingDelay = 1500;
DeathCry=LichDeathCrySound;
PainCry=LichPainCrySound;
maxDamage = 150;// bots are half as tough as the human player
runSurfaceAngle = 85;//yorks - was 70
range=50;

};
function LichData::onAnimationTrigger(%this, %obj, %trigger)
{
switch (%trigger)
{
case 2:
%obj.playAudio(0,MineArmedSound);
case 3:
%obj.playAudio(0,MineArmedSound);
case 4:
%obj.playAudio(0,MineArmedSound);
case 5:
%obj.playAudio(0,MineArmedSound);
case 6:

%obj.playAudio(0,MineArmedSound);
case 7:
%obj.playAudio(0,MineArmedSound);
}
}
#14
02/03/2012 (8:07 am)
@Donald:

I just added this to a fresh install of T3D 1.2 and everything works. Your datablock and so seem to be ok so I don't know exactly where it goes wrong.

Did you added this also to a fresh install?
#15
02/03/2012 (1:10 pm)
its not exactly a fresh install, sahara and autolayer for terrain painting are added but thats it
#16
02/07/2012 (4:00 am)
@Donald:

I'm sorry to say that I don't know what is causing your problem. I added the code to 1.2 and it worked as expected. I don't think that Sahara is a problem because I have sahara and this code in 1.1.

I don't know either if somebody else is having problems in 1.2, or have a fix for this.

One thing I did notice is that sometimes when debugging the code the trigger is missed, and therefore not throwing in the onAnimationTrigger callback.
#17
08/13/2012 (1:52 pm)
@Donald
I have the same problem
I think the problem consists in that mTriggerStates is null everytime
But I don't know why
Did you solve this?
#18
08/20/2012 (3:48 am)
It work for me on a heavy modified version of Torque 1.1 Final (1.2 patched).

I will test on T3D 1.2.

@Donald @genomegames
If you use *.dso files, delete them. May be is it the cause of your problems.

At the end of
void ShapeBase::advanceThreads(F32 dt)

My modified version:
// Make sure the thread is still valid after the call to onEndSequence_callback().
// Someone could have called destroyThread() while in there.
if(st.thread)
{
 mShapeInstance->advanceTime(dt,st.thread);
 if(!isGhost())
 {  
   for (S32 i = 1; i < 32; i++)
   {  
     if (mShapeInstance->getTriggerState(i))
     {  					      //datablock::onAnimationTrigger(%this, 
//                              %objId,
//                              %sequenceName,
//                              %slot(stateNum externally 1..32,
//                                    internally 0..31)
//So, we can choose to carry out some actions based per datablock, per object // and/or per sequence independantly of the object.

         Con::executef(mDataBlock,
              "onAnimationTrigger",
              Con::getIntArg(mId),
              st.thread->getSequenceName().c_str(),
              Con::getIntArg(i));  
     }  
    }  
   }  
}

I hope this code can help you.