Coordinated Effects Example - Fireball
From Multiverse
| Client Scripting |
|
Overview • Coordinated Effects • Fireball Example • Terrain Decals • Compositors • About Shadows • Client Animation System • Creating Your Own Animations • Scripting Avatar Appearance |
| Reference |
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.
