Animation-driven events

I’ve been contemplating several ways on how one might go about tying events to a character’s animation. The previous engine I used allowed you to set triggers on individual frames of animation. This meant it was fairly easy to add effects such as particles/sounds to a character’s walk.

Looking at Panda3d docs, it seems that one way to do this would be to define a series of Intervals which are then played together in a Parallel.

One starts the walking animation, and the other triggers a sound at time X, and then you just loop this Parallel.

However, in my case, I don’t always trigger an animation using an Actor interval from the anim start. I want to use a LerpAnimInterval to smoothly blend from different poses. So, this makes getting the timing and organization of a proper sequence hard to manage.

Any ideas on what might be a better way to do this?

Hmm, offhand, I can’t think of any built-in way to do this automatically. You could wrap an ActorInterval inside a Parallel that covers the whole cycle of the animation, along with your needed callbacks and such; and then you could always use that ActorInterval to play your animation.

If you use LerpAnimInterval within a larger interval, starting the animation midstream, you can do this by referencing the ActorInterval within a MetaInterval that starts it midstream.

It does seem awfully clumsy, though. Seems like there ought to be some easy way to trigger callbacks whenever a particular frame is reached (or passed), by whatever means, ActorInterval or otherwise.

David

I have been working on an animation driven events system for some time now and I’ve come up with the following:

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.interval.MetaInterval import Sequence
from direct.interval.FunctionInterval import Func,Wait
from pandac.PandaModules import CMetaInterval
from direct.actor import Actor
import sys,random

"""
The parent object which has an Actor as member named "model".
parent

Name of animation object.
name

To loop the animation or not.
loop

The end sequence function which is called at the end of the sequence. This function should handle looping.
endseqfunc

Animations with corresponding names should already have been loaded by the actor.
Events should be listed chronologically.
animations=[{"Animation":	"animation name",
			 "Events":
			 [{"Func":		"function name",
			   "Frame":		"frame on which to call the function",
			   "Args":		"extra arguments"}]
		   }]
"""
class animation():
	def __init__(self,parent,name,loop=0,endseqfunc=None,animations=None):
		self.parent=parent
		self.name=name
		self.loop=loop
		self.animations=animations
		self.num=0
		self.sequence={}
		
		if self.animations:
			for item in animations:
				self.parent.model.setControlEffect(item["Animation"],0)
				
				count=0
				ctrl=self.parent.model.getAnimControl(item["Animation"])
				if ctrl:
					count=ctrl.getNumFrames()
				
				self.sequence[item["Animation"]]=Sequence()
				
				lastanim=0
				
				if item["Events"]:
					for event in item["Events"]:
						self.sequence[item["Animation"]].append(self.parent.model.actorInterval(item["Animation"],startFrame=lastanim+1,endFrame=event["Frame"]))
						
						self.sequence[item["Animation"]].append(Func(eval(event["Func"]),item["Animation"],args=event["Args"]))
						
						lastanim=event["Frame"]
				
				if lastanim < count:
					self.sequence[item["Animation"]].append(self.parent.model.actorInterval(item["Animation"],startFrame=(lastanim+1),endFrame=count))
				
				self.sequence[item["Animation"]].append(Func(endseqfunc,self))
	
	def printFrameNum(self,anim,args):
		ctrl=self.parent.model.getAnimControl(anim)
		if ctrl:
			print "Run event on frame", ctrl.getFrame()
	
	def startSequence(self):
		self.setControlEffect(0)
		self.randomize()
		self.parent.model.loop(self.getAnim())
		self.sequence[self.getAnim()].start()
		self.setControlEffect(1)
	
	def finishSequence(self):
		self.sequence[self.getAnim()].finish()
		self.setControlEffect(0)
	
	def getAnim(self):
		return self.animations[self.num]["Animation"]
	
	def getAnimControl(self):
		return self.parent.model.getAnimControl(self.getAnim())
	
	def isPlaying(self):
		return self.parent.model.getAnimControl(self.getAnim()).isPlaying()
	
	def randomize(self):
		if len(self.animations)==0:
			print "Animation list empty"
			return False
		
		self.num=int(random.random()*len(self.animations))
	
	def setControlEffect(self,effect):
		self.parent.model.setControlEffect(self.getAnim(),effect)


class model(DirectObject):
	def __init__(self):
		self.model=Actor.Actor("human02.egg")
		
		self.model.loadAnims({"walk1":"human02_walk01.egg",
							  "idle1":"human02_idle01.egg",
							  "primarymeleeattack1":"human02_primarymeleeattack01.egg"})
		
		self.currentAnim=None
		self.previousAnim=None
		self.blending=False
		
		self.animation={}
		
		anims=[{"Animation":"walk1",
		        "Events":
				[]
			  }]
		self.animation["walk"]=animation(self,"walk",1,self.endSequence,anims)
		
		anims=[{"Animation":"idle1",
		        "Events":
				[]
			  }]
		self.animation["idle"]=animation(self,"idle",1,self.endSequence,anims)
		
		anims=[{"Animation":"primarymeleeattack1",
		        "Events":
				[{"Func":"self.printFrameNum",
				  "Frame":20,
				  "Args":None}]
			  }]
		self.animation["primarymeleeattack"]=animation(self,"primarymeleeattack",0,self.endSequence,anims)
		
		self.model.reparentTo(render)
		
		self.model.setBlend(animBlend=True)
		
		self.accept("1",self.playAnim,[self.animation["idle"]])
		self.accept("2",self.playAnim,[self.animation["walk"]])
		self.accept("mouse1",self.playAnim,[self.animation["primarymeleeattack"]])
	
	def playAnim(self,animobj):
		if self.currentAnim==animobj or self.blending:
			return False
		
		if self.currentAnim:
			self.previousAnim=self.currentAnim
			self.blending=True
			Sequence(LerpFunc(self.setControlEffect,fromData=1,toData=0,duration=0.1,extraArgs=[self.currentAnim]),Func(self.blendDone)).start()
		
		self.startSequence(animobj)
	
	def startSequence(self,animobj):
		self.currentAnim=animobj
		animobj.startSequence()
		
	def endSequence(self,animobj):
		animobj.finishSequence()
		
		if self.currentAnim==animobj:
			if animobj.loop:
				animobj.startSequence()
			elif self.previousAnim != None:
				self.playAnim(self.previousAnim)
				self.previousAnim=None
	
	def blendDone(self):
		self.blending=False
	
	def setControlEffect(self,t,arg):
		#print "set effect",t,arg
		arg.setControlEffect(t)

m=model()

base.disableMouse()
camera.setPos(2,2,1.5)
camera.lookAt(m.model)

run()

Blending sort of works and is done with manually setting the control effect of the previous animation. The problem I have is that it sometimes does not blend properly.

Here’s a working sample with model and animations: http://www.mediafire.com/?i390qu66q5n8yuq

Push 1 to start the looping idle animation. Then click multiple times to start the stagger animation. You will notice that the event is called on one of the stagger animations, on frame 5. This event just prints a line but you can set the function that should be called for the event.

You will notice that sometimes, when blending, the previous animation’s control effect does not seem to reach 0. The problem is that if you have an animation like a melee attack this is not acceptable.

Anyone have some ideas of how to improve on this and get the blending to work properly?

EDIT | Updated code.

Are you explicitly setting the control effect to 0? Are you sure this call is actually being made, and are you sure it is passing the correct anim name when it is made?

David

Got it working, updated the code above. Now its on to adding support for subparts…