• entries
    436
  • comments
    1179
  • views
    763134

Urho3D and Pyrotechnics

Sign in to follow this  
JTippetts

1279 views

Urho3D's architecture is based on a component system. You may have heard the term. Essentially, Urho3D provides a hierarchy of Nodes, where each Node comprises a set of transforms (position, rotation, scale), a list of children Nodes, and a list of components. Components provide the behavior and characteristics that turn a regular old Node into a game object, be it an enemy, a tree, a rock, or what have you.

Components consist of things such as static or animated models (to provide geometry to draw), lights, particle effects, sound sources, and so on. Urho3D also provides a special kind of component, called a ScriptObject, that allows custom functionality to be provided via Lua script. (Or AngelScript, if that's your fetish.)

Objects in Urho3D can communicate via events, where any object may subscribe to any number of events. An object can subscribe to an event globally, meaning it responds to the event regardless of where it originates, or it can subscribe to an event only from a specified object.

Together, this system of components and events provides a powerful framework for making fun and interesting things happen.

By default, Urho3D provides means for instancing objects from XML or JSON descriptions. These descriptions provide the transforms for a given Node, for any children, and descriptions of the components to be added to a node. In my personal code, I have taken object instancing a little bit further, in allowing objects to be instanced based on a description provided as a Lua table. This extension allows me to do some particular things that really come in handy.

With a composition system like this, intricate and complex behaviors can be implemented using a toolbox of relatively simple building blocks. To demonstrate how this can be, I'll talk about fireballs.

Earlier today, I implemented a Flame Shotgun spell, in response to a conversation being conducted in the gd.net chat with Washu. It was done as a joke. Nevertheless, implementing it only took a few minutes, because I already had the pieces in place. I just needed to rearrange them a little bit.

As I mentioned, game objects are made from Nodes with attached components. A fireball is no different. In Goblinson Crusoe I have a framework for casting spells. This framework is implemented as part of the turn-based combat system. A spell is implemented as a descriptor table populated with data that describes the spell. Does it have a range? Is it targeted or self-cast? What does it do? Stuff like that. When a spellcast is initiated, the spell executes its effect which, in the case of a fireball spell, includes spawning the actual fireball.

A fireball is spawned from an object descriptor (a Lua table) that describes the components that must be attached to a Node to make it work. Given an object descriptor, I can call the function InstanceObject(descriptor, scene) to spawn an instance of an object based on the descriptor and add it to a given scene. Here is an object descriptor for a basic fireball:

[spoiler]
desc= { Components= { {Type="ParticleEmitter", Material="Effects/flame.xml", UpdateInvisible=false, NumParticles=2000, Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, MinDirection=Vector3(-0.25,-1,-0.25), MaxDirection=Vector3(0.25,-1,0.25), MinVelocity=0.5, MaxVelocity=1, MinParticleSize=Vector2(0.5, 0.5), MaxParticleSize=Vector2(0.75,0.75), MinEmissionRate=400, MaxEmissionRate=550, ConstantForce=Vector3(0,1,0), SizeMul=Rect(0.5,0.5), FaceCamera=true, MinTimeToLive=0.5, MaxTimeToLive=1, --Color=Color(0.5,0.25,0.125), Colors= { {Color=Color(0.5,0.05,0.05), Time=0}, {Color=Color(0.05,0.5,0.05), Time=0.5}, {Color=Color(0.05,0.05,0.5), Time=1}, }, }, {Type="Light", LightType=LIGHT_POINT, Color={r=1.5,g=0.75,b=0.45}, Range=4, CastShadows=true}, {Type="ScriptObject", Classname="SpawnedAction", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="Projectile", Parameters={startpos={x=startpos.x,y=startpos.y+0.25,z=startpos.z}, endpos={x=endpos.x,y=endpos.y,z=endpos.z}, speed=8, archeight=2}}, {Type="ScriptObject", Classname="TestFireballPayload", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="DamageSingleTargetPayload", Parameters= { target=targetid, attacker=caster, attack="Fireball", damage= { fire=firedamage, siege=20, } } }, } }[/spoiler]

As you can see, the descriptor is a Lua table providing a list of components. The very first component listed is a ParticleEmitter. ParticleEmitter is a built-in component implemented by Urho3D that allows you to attach a particle system to a Node. The exact parameters describing the particle effect are, for the purposes of this discussion, mostly irrelevant. Just understand that the parameters described here create a glowing fireball effect.

The next component is a Light. Also a built-in component, a Light allows you to add a light source to a Node, and the light source will move with the node. Since this is a fireball, of course it needs to cast light. Here, I create a point light.

Next is a ScriptObject component. As I mentioned, ScriptObject components let you implement functionality in script and attach said functionality to a node. This ScriptObject is an instance of SpawnedAction. A SpawnedAction is a bookkeeping/management script object that is used in GC to prevent a combat entity from ending its turn or starting a new action while it already has an action in play. Adding a SpawnedAction component to an object, targeted to a given entity, means that the entity can not do anything as long as that object is alive. As soon as that object is destroyed, the SpawnedAction is removed, and the entity can continue to act or end its turn.

Next is a ScriptObject called Projectile. A fireball is a projectile that moves in an arc from one spot to another. The Projectile component adds this functionality. It's purpose is to listen for the Update event that is sent each frame, and move the Node along an arc path from start to end. Once the end position is reached, the Projectile component will fire aff an event, sent via the Node object (you can call node:SendEvent() to force an event to be sent from the given node) indicating that the projectile has reached its destination and it is time to deliver any payloads. After payloads are signalled, the projectile component queues the fireball node for destruction.

Without any payloads, the Projectile combined with the ParticleEmitter and Light will create a pretty light-show, a fiery glob arcing through the air then disappearing at the end with no other effect. However, by adding payloads to the object, I can cause stuff to happen when the projectile reaches its endpoint.

The next component, TestFireballPayload, is just such a payload. A payload is a ScriptObject that listens for the ProjectileEnd event, fired by the projectile. It listens for this event only if it originates with the owning Node; that way, it won't receive ProjectileEnd events originating with other projectiles. Only from its own. The purpose of the TestFireballPayload is to spawn a fireball explosion at the projectile's termination point. Upon receipt of the ProjectileEnd event, the component spawns another object from another descriptor table, this one creating a short-lived particle emitter simulating a flame burst.

The next component is called DamageSingleTargetPayload. This ScriptObject listens for the ProjectileEnd event and upon receipt of the event it fires off a damage event against the target of the fireball, if there is one. The amount and types of damage delivered are specified in a table.

Each one of these components provides its own little bit of functionality to the whole that makes it act as a fireball. When an object is spawned from this descriptor, it creates a node with a particle effect simulating flames. This node moves through the air in an arc from a start point (the caster's location) to an end point (the targeted area or unit). Upon reaching the endpoint, it spawns a flame blast visual effect and a damage event against the target, then despawns. You can see the behavior of this spell here:



With a few other additions, I can modify the effect of the fireball. For example, I can specify an object setup like this:

[spoiler]
spawn= { Components= { {Type="ParticleEmitter", Material="Effects/flame.xml", UpdateInvisible=false, NumParticles=2000, Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, MinDirection=Vector3(-0.25,-1,-0.25), MaxDirection=Vector3(0.25,-1,0.25), MinVelocity=0.5, MaxVelocity=1, MinParticleSize=Vector2(1.5, 1.5), MaxParticleSize=Vector2(2.75,2.75), MinEmissionRate=400, MaxEmissionRate=550, ConstantForce=Vector3(0,0,0), SizeMul=Rect(0.5,0.5), FaceCamera=true, MinTimeToLive=0.5, MaxTimeToLive=1, Color=Color(0.5,0.25,0.125), --[[Colors= { {Color=Color(0.05,0.005,0.005), Time=0}, {Color=Color(0.005,0.05,0.005), Time=0.5}, {Color=Color(0.005,0.005,0.05), Time=1}, },]] }, {Type="ScriptObject", Classname="SpawnedAction", Parameters={ownernode=args.caster}}, {Type="ScriptObject", Classname="SequentialApplicator", Parameters={targets=targets}}, {Type="ScriptObject", Classname="TestFireballPayload", Parameters={ownernode=args.caster}}, {Type="ScriptObject", Classname="DamageSingleTargetPayload", Parameters= { target=args.hexmap:getObjectID(args.targetx, args.targetz), attacker=args.caster, attack="test fireball", damage= { fire=10, siege=20, } } }, } } desc= { Components= { {Type="ParticleEmitter", Material="Effects/flame.xml", UpdateInvisible=false, NumParticles=2000, Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, MinDirection=Vector3(-0.25,-1,-0.25), MaxDirection=Vector3(0.25,-1,0.25), MinVelocity=0.5, MaxVelocity=1, MinParticleSize=Vector2(0.25, 0.25), MaxParticleSize=Vector2(0.5,0.5), MinEmissionRate=400, MaxEmissionRate=550, ConstantForce=Vector3(0,0,0), SizeMul=Rect(0.5,0.5), FaceCamera=true, MinTimeToLive=0.5, MaxTimeToLive=1, Color=Color(0.5,0.25,0.125), --[[Colors= { {Color=Color(0.05,0.005,0.005), Time=0}, {Color=Color(0.005,0.05,0.005), Time=0.5}, {Color=Color(0.005,0.005,0.05), Time=1}, },]] }, {Type="Light", LightType=LIGHT_POINT, Color={r=1.5,g=0.75,b=0.45}, Range=4, CastShadows=true}, {Type="ScriptObject", Classname="SpawnedAction", Parameters={ownernode=args.caster}}, {Type="ScriptObject", Classname="Projectile", Parameters={startpos={x=myx,y=myy+0.25,z=myz}, endpos={x=targetpos.x,y=targety,z=targetpos.y}, speed=8, archeight=2}}, {Type="ScriptObject", Classname="ObjectSpawnerPayload", Parameters={desc=spawn}}, } }[/spoiler]

This looks complicated, but essentially it is structured as 2 object descriptors. The first one describes an object to be spawned, and the second described the projectile. Look at the second descriptor. It looks much like the Fireball descriptor above, containing a light and a particle effect and so forth. But instead of containing the fireball payloads as before, instead this descriptor contains another component called ObjectSpawnerPayload. This is a generic script object that, upon receipt of the ProjectileEnd event, spawns another object from a specified object descriptor table. The object that is spawned is specified in the first object descriptor table.

This gets a little more complicated. Now, instead of spawning an explosion and generating a damage event, the fireball spawns a secondary object. Look at the descriptor for this secondary object. (The first descriptor above). It contains the familiar SpawnedAction component, to prevent further character actions. It contains the TestFireballPayload explosion component and DamageSingleTargetPayload component, for creating explosions and dealing damage. But it also contains another ParticleEmitter (this one creates a larger version of the fireball explosion) and a component called a SequentialApplicator.

Given a list of targets (selected via an area select when the spellcast is initiated) the SequentialApplicator will generate ProjectileEnd events for every target in the list. This means that if you have a damage payload, it will be applied to every target in the list. Same with the explosion payload. So now, we have a fireball spell that will trigger an explosion of flame and damage on every target in the area. You can see it in action here:



Apologies for targeting an area that is not super visible, but if you watch closely you can see that the fireball projectile arcs to its target, and upon termination it spawns a large fire explosion at the target location, then applies smaller explosions to all objects within a specified radius of the target. This is the sequential applicator at work. Any payload can be delivered to an entire list of targets this way.

But we're not done! This is pretty much where I had left the fireball tests until today, having moved on to other things. In response to a convo regarding wasps, wasp nests and the preferred disposal technique thereof, I implemented a third variety of fireball spell: the Flame Shotgun. This spell is actually just a whole bunch of regular old fireball projectiles, one spawned for each target in the list. It required no additional components to implement, just a slight modification to target acquisition. This spell obtains its target list from a cone function that specifies a given angle and range. You can see it in action here:



As you can see, one fireball is spawned for each enemy in the field of target, and all projectiles are spawned at the same time. But I'm still not done!

Here is the descriptor for yet another type of spell:

[spoiler]
function Fireball(caster, startpos,endpos,targetid,firedamage) local desc= { Components= { {Type="ParticleEmitter", Material="Effects/flame.xml", UpdateInvisible=false, NumParticles=2000, Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, MinDirection=Vector3(-0.25,-1,-0.25), MaxDirection=Vector3(0.25,-1,0.25), MinVelocity=0.5, MaxVelocity=1, MinParticleSize=Vector2(0.5, 0.5), MaxParticleSize=Vector2(0.75,0.75), MinEmissionRate=400, MaxEmissionRate=550, ConstantForce=Vector3(0,1,0), SizeMul=Rect(0.5,0.5), FaceCamera=true, MinTimeToLive=0.5, MaxTimeToLive=1, --Color=Color(0.5,0.25,0.125), Colors= { {Color=Color(0.5,0.05,0.05), Time=0}, {Color=Color(0.05,0.5,0.05), Time=0.5}, {Color=Color(0.05,0.05,0.5), Time=1}, }, }, {Type="Light", LightType=LIGHT_POINT, Color={r=1.5,g=0.75,b=0.45}, Range=4, CastShadows=true}, {Type="ScriptObject", Classname="SpawnedAction", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="Projectile", Parameters={startpos={x=startpos.x,y=startpos.y+0.25,z=startpos.z}, endpos={x=endpos.x,y=endpos.y,z=endpos.z}, speed=8, archeight=2}}, {Type="ScriptObject", Classname="TestFireballPayload", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="DamageSingleTargetPayload", Parameters= { target=targetid, attacker=caster, attack="Fireball", damage= { fire=firedamage, siege=20, } } }, } } return descendfunction Bouncer(caster,startpos,endpos,targetid,firedamage,list) if list.size==0 then return Fireball(caster,startpos,endpos,targetid) else local nextid=list:pop() local nextend=caster:GetScene():GetNode(nextid).position return { Components= { {Type="ParticleEmitter", Material="Effects/flame.xml", UpdateInvisible=false, NumParticles=2000, Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, MinDirection=Vector3(-0.25,-1,-0.25), MaxDirection=Vector3(0.25,-1,0.25), MinVelocity=0.5, MaxVelocity=1, MinParticleSize=Vector2(0.5, 0.5), MaxParticleSize=Vector2(0.75,0.75), MinEmissionRate=400, MaxEmissionRate=550, ConstantForce=Vector3(0,1,0), SizeMul=Rect(0.5,0.5), FaceCamera=true, MinTimeToLive=0.5, MaxTimeToLive=1, --Color=Color(0.5,0.25,0.125), Colors= { {Color=Color(0.5,0.05,0.05), Time=0}, {Color=Color(0.05,0.5,0.05), Time=0.5}, {Color=Color(0.05,0.05,0.5), Time=1}, }, }, {Type="Light", LightType=LIGHT_POINT, Color={r=1.5,g=0.75,b=0.45}, Range=4, CastShadows=true}, {Type="ScriptObject", Classname="SpawnedAction", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="Projectile", Parameters={startpos={x=startpos.x,y=startpos.y+0.25,z=startpos.z}, endpos={x=endpos.x,y=endpos.y,z=endpos.z}, speed=8, archeight=2}}, {Type="ScriptObject", Classname="TestFireballPayload", Parameters={ownernode=caster}}, {Type="ScriptObject", Classname="ObjectSpawnerPayload", Parameters={desc=Bouncer(caster,endpos,nextend,nextid,firedamage*2,list)}}, {Type="ScriptObject", Classname="DamageSingleTargetPayload", Parameters= { target=nextid, attacker=caster, attack="Fireball", damage= { fire=firedamage, siege=20, } } }, } } end end[/spoiler]

For simplicity, I implemented this descriptor as 2 functions: one function that returns a standard fireball, and one function that returns a Bouncer fireball. The bouncer fireball is similar to a standard fireball but with one large change: the addition of an ObjectSpawnerPayload component that takes as its parameter another fireball descriptor.

The way the Chain Fireball works is that a list of targets is provided, neatly sorted based on distance from caster. The first target is obtained, and a projectile is spawned by calling Bouncer(). Bouncer is recursive. It generates either a standard fireball (if there is no more targets left in the list) or another Bouncer (if there is at least one more target in the list.) Aside from the spawner payload, the Bouncers also come standard with the damage and explosion payloads to bring the hurt.

The result is a recursive object definition that, when instance, spawns a projectile which arcs to target, spawns another projectile which arcs to target and spawns another, etc... until the target list is depleted. Each target is hit only once. Note that the Bouncer function accepts a firedamage parameter, and each call to Bouncer multiplies fire damage by 2. This means that with every hop, the fire damage dealt doubles. The guy at the end? Yeah, he's toast. You can see the effect in action here:



As you can see, projectiles are spawned in sequence, hopping from one object to another until the end is reached. Objects are derived from a conical target area, just as the flame shotgun.

All of this tasty fireball functionality was provided as a result of combining smaller, specific tasks implemented via components. Once you understand how such a system can work, it can be quite elegant and flexible.
Sign in to follow this  


1 Comment


Recommended Comments

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now