Coordinated Effects Example - Fireball

From Multiverse

Jump to: navigation, search
The Client Scripting API is under development. During this phase of development, we are focusing on functionality and features, rather than maintaining 100% backwards compatibility for each release. If you write scripts using this API, be prepared to make change them until the Client Scripting API is more stable.



Contents

Introduction

The fireball is a complex Coordinated Effect that uses character animation, node animation, property animation, particle systems, and lighting effects. To see a video of this effect in action, click on the following link and play in your video player: http://update.multiverse.net/public/Fireball-Effect.wmv.

The complete Sampleworld asset repository includes the code for this effect in the Scripts/TestProjectile.py file. Use the key binding (Ctrl-Alt-P) to invoke the effect.

For information on how to link a coordinated effect to an ability, see Invoking a Coordinated Effect with an Ability.

Script Walkthrough

The fireball effect consists of three phases:

  • Casting
  • Projectile
  • Blast

The effect script sets up each phase, and then uses the yield statement to wait for that phase to complete before setting up the next phase. This effect makes use of node and property animations to update the position and properties of objects.

Constructor and setup

The effect script imports the ClientAPI module so that it can access the Client scripting API.

import ClientAPI

The constructor saves the instance object ID, which is useful for making unique names for various objects.

class TestProjectile:

def __init__(self, oid):
    # save the instance oid for this instance of the effect
    self.OID = oid

The CancelEffect() and UpdateEffect() methods are required but not currently used, so they each have only a pass statement:

def CancelEffect(self):
    pass

def UpdateEffect(self):
    pass

Next comes the ExecuteEffect() method which actually does all the work:

def ExecuteEffect(self, targetOID, sourceOID):

For this effect, ExecuteEffect() takes arguments for the object ID of the target and the object ID of the caster of the fireball spell. The method uses object IDs because they are the handles the server uses to refer to objects when it is communicating with the client.

The next two lines convert the object IDs sent from the server to the WorldObjects required by the client:

    target = ClientAPI.GetObjectByOID(targetOID)
    caster = ClientAPI.GetObjectByOID(sourceOID)

The method GetObjectByOID() returns a WorldObject for the specified object ID.

The next line specifies the name of the attachment point on the target that the projectile will hit:

    targetSlot = 'primaryWeapon'

The targetSlot variable holds the name of the attachment point on the target object toward which the fireball will fly. The primaryWeapon attachment point is used because most of the Multiverse sample models have it. A chest attachment point might make a better choice if your model has one.

The next line creates a unique name for the projectile particle system:

    projectileParticlesName = 'projectileParticles' + str(self.OID)

Because the name is required to be unique, this statement appends the unique object ID for this effect instance to the base name string. A simple string is not sufficient, because an error would occur if multiple instances of the effect were playing at the same time. There are several other places in the code that use the same technique to create unique names.

Casting phase

The casting phase handles the effects when the caster is casting the spell.

These lines create a particle system instance of the 'fireball-projectile' particle system, and attach the particle effect to caster's hand (the 'primaryWeapon' attachment point):

    projectileParticles = ClientAPI.ParticleSystem.ParticleSystem(projectileParticlesName, 'fireball-projectile')
    caster.AttachObject('primaryWeapon', projectileParticles)

The following code creates a red point light for the fireball so that it casts light on nearby objects, and then attaches the light to the primaryWeapon slot (right hand) of the caster.

    # create a point light to go along with the particle effect
    light = ClientAPI.Light.Light('movingLight' + str(self.OID))
    light.Diffuse = ClientAPI.ColorEx.Red
    light.AttenuationRange = 1000000
    light.AttenuationConstant = 0
    light.AttenuationLinear = 0.001
    light.Type = ClientAPI.Light.LightType.Point

    # attach the light to the caster
    caster.AttachObject('primaryWeapon', light)

Then this line plays the casting animation, in this case "attack", since the current sample human models don't have a special spell-casting animation.

    caster.QueueAnimation("attack")

Then the yield statement causes the effect to save its state and exit. The coordinated effect system will restart the effect from this point after 0.3 seconds (300 millisends). During this time the client will continue to run, and the animations and particle systems that have been started by this script will be updated every frame. The caster's 3D model will begin playing its casting animation, the particle system will start emitting particles, and both the particle system and light will move along with the point on the caster's model that they are attached to.

    # wait for the animation to reach the right point
    yield 300

After the yield, the casting phase of the effect is complete, and the projectile phase starts.

The code to play the sound is temporarily commented out:

    # attach and play the sound
    #sound = ClientAPI.GetSoundSource('LaunchFireball.ogg', caster.Position)
    #caster.AttachSound(sound)
    #sound.Play()

Projectile phase

During the projectile phase, the particle system and light travel from the caster's hand to the target. The variables startLoc and endLoc are the starting point and ending point of the path of the projectile. The starting point is the current world location of the attachment point to which the particle system (and light) are attached, which is the current position of the caster's hand as it moves through its casting animation. The ending location is the current world location of the target attachment point on the target object. Later, you will see how to update this location to change the path of the projectile while it is in flight as the target moves.

    # compute the start and end points of the projectile path
    startLoc = projectileParticles.ParentNode.DerivedPosition
    endLoc = target.AttachmentPointPosition(targetSlot)

The following code creates a SceneNode at the current location of the particle system and light, detaches the particle system from the caster, and attaches them to the SceneNode.

    # create new scene node for the projectile, which will move from the caster to the target
    projectileNodeName = 'projectileNode' + str(self.OID)
    projectileNode = ClientAPI.SceneNode.SceneNode(projectileNodeName)
    projectileNode.Parent = ClientAPI.RootSceneNode
    projectileNode.Position = startLoc
    
    # detach the particle system and light from the caster's hand
    caster.DetachObject(projectileParticles)
    caster.DetachObject(light)
    
    # now attach the particle system and the light to the node that we will animate
    projectileNode.AttachObject(projectileParticles)
    projectileNode.AttachObject(light)

This code creates and enables a two second long animation. This animation moves the SceneNode from the caster to the target over a period of two seconds, once it is configured, as shown here:

    # specify the length of the projectile flight in seconds
    animationLength = 2
    
    # create a new animation for the path of the projectile
    animName = 'projectilePath' + str(self.OID)
    anim = ClientAPI.Animation.Animation(animName, animationLength)
    
    # enable the animation
    anim.Enabled = True

Then the following line creates a node animation track associated with the projectile SceneNode. Each node or property to be manipulated by an animation needs its own animation track. In this case it is just one SceneNode, so only one animation track is required. Using multiple animation tracks, it is possible to create a single animation that can manipulate multiple SceneNodes and object properties.

    # create an animation track for the SceneNode
    track = anim.CreateNodeTrack(projectileNode)

Each animation track can have multiple key frames. Each key frame defines a time and a particular value for that time. In this case, the value is the location of the scene node. Here we create two key frames. One for the starting location at time zero, and one at the ending location at the end time. It is possible to make more complicated paths by using more key frames.

    # create the starting key frame, which is at time 0, and the starting 
    # location of the projectile path
    k1 = track.CreateKeyFrame(0)
    k1.Translate = startLoc
    
    # create the ending key frame, which is at the end time, and the ending
    # location of the projectile
    k2 = track.CreateKeyFrame(animationLength)
    k2.Translate = endLoc

Here we save the second (ending) key frame in a variable in the object instance, so that it is accessible by other methods in the class. In this case, we will adjust the location of the end key frame in an event handler if the target moves during the animation.

    # save the ending key frame in the object instance so that it is accessible
    # by the movement event handler
    self.targetKeyFrame = k2

The following code derives a SceneNode from the target attachment point on the target object. Then, it registers an event handler for the Updated event on that SceneNode, so that every time the Client updates the position of the SceneNode, it calls the event handler. The event handler updates the position of the ending keyframe as the target object moves.

    targetNode = target.AttachNode(targetSlot)
    
    targetNode.RegisterEventHandler('Updated', self.targetMoveHandler)

The event handler tracks the movements of the target node. It updates the ending keyframe of the animation as the target moves around, so that the projectile will end up hitting the target even if it moves.

The event handler, targetMoveHandler, is near the bottom of the script file:

def targetMoveHandler(self, position, orientation, scale):
    self.targetKeyFrame.Translate = position

The projectile phase finishes by playing the node animation previously created, and then calling yield to pause the script for the length of the animation. When execution continues following the yield, the animation will have completed, and the projectile and light will have traveled to the target point.

    # run the animation
    anim.Play()
    
    # wait for the animation to move the projectile to the target
    yield animationLength * 1000

Then the following code cleans up from the projectile phase of the effect. It removes the event handler on the temporary target node, frees the node and the animation created for the projectile path, detaches the light and particle system from the projectile SceneNode, and frees the SceneNode.

    # remove the event handler
    targetNode.RemoveEventHandler('Updated', self.targetMoveHandler)
    
    # remove the temporary target node
    target.DetachNode(targetNode)
    
    # free the animation
    anim.Dispose()

    # detach the particle system and light from the projectile node
    projectileNode.DetachObject(projectileParticles)
    projectileNode.DetachObject(light)
    
    # free the projectile node
    projectileNode.Dispose()

Blast phase

The blast phase occurs when the fireball strikes the target.

These lines attach light and particle system to the target slot:

    # attach the particle system and light to the target attachment point
    target.AttachObject(targetSlot, projectileParticles)
    target.AttachObject(targetSlot, light)

Then, this code creates another particle system and attaches it to the target. This particle system implements a blast effect from the fireball hitting its target.

    # create a new particle system 
    blastName = 'blastParticles' + str(self.OID)
    blastParticles = ClientAPI.ParticleSystem.ParticleSystem(blastName, 'fireball-blast')
    target.AttachObject(targetSlot, blastParticles)

These lines queue an animation on the target WorldObject, which is the recoil from being hit with the fireball.

    # check to see which animation is available for the target being hit.
    # we have to do this because the current Multiverse models don't have consistent
    # animation names.
    if 'recoil' in target.Model.AnimationNames:
    target.QueueAnimation('recoil')
    elif 'hurt' in target.Model.AnimationNames:
    target.QueueAnimation('hurt')

Next, the code creates a new one second long animation that will be used to control properties of the light. It sets the interpolation mode of the animation to Linear. The default interpolation mode is Spline.

    # Create a property animation to change the light attenuation and color.
    # This will change the color of the light and increase the area that it
    # affects during the final blast effect.
    lightAnimLength = 1.0
    lightAnim = ClientAPI.Animation.Animation('light' + str(self.OID), lightAnimLength)
    lightAnim.InterpolationMode = ClientAPI.Animation.InterpolationMode.Linear
    lightAnim.Enabled = True

The following lines create two property animation tracks: One to control the diffuse color of the light, and one to control the attenuation. Changing the attenuation value adjusts the radius of the light being cast into the scene.

    # create animation tracks for the two properties we want to animate
    lightColorTrack = lightAnim.CreatePropertyTrack(light.CreateAnimableValue('Diffuse'))
    lightAttenuationTrack =  lightAnim.CreatePropertyTrack(light.CreateAnimableValue('AttenuationLinear'))

This code creates key frames that change the color of the light from its current value (red) to orange, and then fade to black:

    # create key frames for color animation track
    keyFrame = lightColorTrack.CreateKeyFrame(0)
    keyFrame.PropertyValue = light.Diffuse
    keyFrame = lightColorTrack.CreateKeyFrame(lightAnimLength/2)
    keyFrame.PropertyValue = ClientAPI.ColorEx.Orange
    keyFrame = lightColorTrack.CreateKeyFrame(lightAnimLength)
    keyFrame.PropertyValue = ClientAPI.ColorEx.Black

This code creates several key frames that change the linear attenuation value of the light to first increase the radius of the area affected by the light, and then to decrease it as the light fades out. The combination of these two property animations creates an effect of the light getting brighter and larger, and then fading out as the blast occurs.

    # create key frames for attenuation animation track
    keyFrame = lightAttenuationTrack.CreateKeyFrame(0)
    keyFrame.PropertyValue = light.AttenuationLinear
    keyFrame = lightAttenuationTrack.CreateKeyFrame(lightAnimLength/2.0)
    keyFrame.PropertyValue = 0.0001
    keyFrame = lightAttenuationTrack.CreateKeyFrame(lightAnimLength*0.8)
    keyFrame.PropertyValue = 0.001
    keyFrame = lightAttenuationTrack.CreateKeyFrame(lightAnimLength)
    keyFrame.PropertyValue = 1

Finally, these lines play the new property animation, and then yield for one second while the animation and the particle effects play:

    # play the light property animation
    lightAnim.Play()
    
    # wait for the explosion to dissipate    
    yield 1000

Cleanup

Finally detach and dispose of all the created objects, and then write a log message to indicate that the effect completed successfully.

    # remove both particle effects
    target.DetachObject(projectileParticles)
    target.DetachObject(blastParticles)
    target.DetachObject(light)
    
    # free the light, its animation, and the particle systems
    lightAnim.Dispose()
    light.Dispose()
    projectileParticles.Dispose()
    blastParticles.Dispose()
    
    ClientAPI.Write('Finished projectile effect')

Register the effect

ClientAPI.RegisterEffect("TestProjectile", TestProjectile)

Finally, don't forget to register the effect with the coordinated effects system. The call to ClientAPI.RegisterEffect() must be in the module scope (rather than the class scope) so that it is executed when the module is imported by Startup.py.

Bind a hotkey to invoke the effect

Now that you have written the script to create the coordinated effect, you need to bind a hotkey, so the player can invoke the effect.

First, add the following to the Interface/FrameXML/Bindings.xml file:

<Binding name="PROJECTILETEST" header="ACTIONS">
         ClientAPI.InvokeEffect( "TestProjectile", 
                                 ClientAPI.GetLocalOID(), 
                                 {'targetOID': ClientAPI.GetCurrentTarget().OID,
                                  'sourceOID':ClientAPI.GetPlayerObject().OID} )
 </Binding>

This creates a user interface action called "PROJECTILETEST" that invokes the coordinated effect.

Then, add the following line to the Interface/FrameXML/bindings.txt file

ALT-CTRL-P PROJECTILETEST

Now the user can press ctrl-alt-p to invoke the cooridnated effect.

Personal tools