In trying to figure out how to handle animation, I have written this Sprite Wrapper.
This should be universal and super-flexible.
Have your animation frames set up as meshes in an inactive layer, with appropriate names (sort of outlined in the script…better documentation will be needed for general human consumption). Have a python script call an instance of Sprite, passing a master mesh (could just be a plain plane) as the first argument.
With a couple properties defined on the master Sprite mesh, the wrapper can smoothly and properly animate both ‘one-shot’ animations and animation loops. You set the number of frames in your cycle, as well as the length of the cycle in logic ticks (or whatever you call it…60 ticks = 1 second?). This, along with the Name of the sprite, and optional ‘action’ and ‘direction’ (which would require the special direction script in my game to work here) parameters, it will dutifully animate any sprite, in any action, from any angle. Sprite textures do not have to be ‘gridded out’ (which would be the case for using animated textues), you can have any number of frames playing at any framerate, and any of this can be altered on-the-fly.
The Sprite class keeps its current frame as an attribute, along with a ‘hit_frame’ value which will trigger an event. In the case of my game so far, the player’s weapon animation is triggered by the player (via left mouse click). The actual attack logic is triggered by the weapon’s animation reaching the appropriate frame; so you have a proper delay between triggering an attack and actually landing that attack.
(it still needs work, but here is what I have so far)
'''
Sprite objects should have the following properties defined:
(int) "frames": the number of frames in the animation cycle
(int) "rate": The number of real frames the animation lasts (60=1 sec. at 60fps)
(int) "hit_frame": (optional) the frame in which the Sprite triggers an event (like a weapon/monster attack)
'''
class Sprite:
def __init__(self, own, Name, loop=False):
self.own = own
self.Name = Name
self.loop = loop #if true, animation runs on a loop. Otherwise, it runs as a one-shot
self.timer = 0
self.current_frame = 0
self.active = False
self.direction = None #If not None, add a direction to the frame
self.action = None #if not None, action is added to mesh string to provide multiple animation cycles. 'walk', 'attack', 'death', 'leg-hump', etc
#Naming conventions for animation frame meshes is 'N_A_D_F' where:
#N = Name of the object ("Goblin")
#A = Name of action(if present) ("Attack")
#D = Direction facing of the sprite (-3 to 4)
#F = The frame number (4), first frame must be 0.
#That mesh would be named "Goblin_Attack_-1_4"
#Only the first and last values are required. A and D are optional.
#start an animation
def start(self):
if not self.active:
self.active = True
#(experimental) stop an animation
def stop(self):
if self.active:
self.active = False
self.timer = 0
self.current_frame = 0
#(experimental) pause an animation
def pause(self):
if self.active:
self.active = False
#sets the mesh based on the NADF formula
def set_mesh(self,frame=None):
N = self.Name + '_'
A = ''
D = ''
F = str(self.current_frame)
if frame != None:
F = str(frame)
if self.action:
A = self.action + '_'
if self.direction:
D = self.direction + '_'
self.own.replaceMesh(N+A+D+F)
#the frame-by-frame management of animation
def animate(self):
if self.active:
self.timer += 1
if self.timer >= self.own['rate']:
if not self.loop:
self.stop()
self.set_mesh()
else:
period = self.timer / self.own['rate'] * 10
self.current_frame = min(self.own['frames']-1, round(period * (self.own['frames']/10)))
self.set_mesh()
And a small example of how it is used, here is the code snippet from my control script that handles the player attack:
((own[‘ent’].hands is the Sprite() instance for the player’s FOV weapon object))
mouse = logic.mouse
LEFT_CLICK = logic.KX_INPUT_JUST_ACTIVATED == mouse.events[events.LEFTMOUSE]
H = own['ent'].hands
if LEFT_CLICK:
if H.timer <= 0:
H.start()
H.animate()
if H.current_frame == H.own['hit_frame'] and not own['STRIKE']:
own['ent'].fighter.attack()
own['STRIKE'] = True
if not H.active:
own['STRIKE'] = False
The player has a STRIKE bool property, which ensures the attack will only trigger the instant the two values become equal (otherwise, you end up doing like four attacks per click). This will probably be worked into the wrapper, as kind of ‘pulse’ control will probably be a common need.