Jump to content

  • Log In with Google      Sign In   
  • Create Account






Getting combat back together

Posted by JTippetts, 26 October 2013 · 470 views

Goblinson Crusoe Urho3d turn-based rpg hex-based rpg
Posted Image

Did a little bit of work (in between real work and stuff) on getting the combat system put back together. It's still incomplete (it was incomplete even before) but it's getting there. Stuff can die now; it just doesn't... you know... die. It lives on, like some kind of weird zombie. I'm re-working the nuances of combat: stats and what they do, elemental types and resistances, buffs and debuffs, etc...

Working with Urho3D is pretty neat. Finding it was a bit of serendipity. If you have read my journal before, you might have come across an older article (here) about how Goblinson Crusoe worked. That article was written when GC was still an action-RPG, and the framework has been iterated on even since then (before being abandoned in favor of Urho3D), but the gist of it was that game objects were containers for components, a fairly basic object composition system.

Well, Urho3D is very similar to that. Nodes encapsulate transformations and a list of components. While Urho3D offers a serialization interface to/from XML, I've been using Lua to instance all of my objects. (I might end up having to use XML serialization for game save and load, but I haven't started re-adding that yet so I'm not sure.) To illustrate, here is how my TestFireball object works:

In GC, actions and spells are held in a Lua table that specifies simple data members indicating various attributes of the spell: can it be cast on self? on allies? does it require a target? does it have an area of effect? what is the range? how many uses/charges does it have? what UI icon represents it? and so on, and so on. One particular element of the spell structure is a function, execute(), which will cause the spell to fire. It is called from the component CombatCommandQueue, which stands at the heart of all acting combat objects. execute() is called when a spellcast command is issued, and when all the various checks have passed: do I have enough resources to cast? do I have enough movement points? am I paralyzed? and so forth. Once it makes it through the net of checks, execute() is handed a table full of data (target location, target unit ID (if any), caster ID, etc...) to do its thing.

Here is what execute() for TestFireball looks like:

execute=function(self,args)
	local targetpos=args.hexmap:calcTileCenter(args.targetx, args.targetz)
	local myx,myy,myz=args.caster:GetPositionXYZ()
	local scene=args.caster:GetScene()
	local desc=
	{
		components=
		{
			{type="ParticleEmitter", Material="Materials/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), TimeToLive=0.5, MinEmissionRate=150, MaxEmissionRate=200,
						ConstantForce=Vector3(0,1,0), Color=Color(0.5,0.25,0.125), SizeMul=Rect(0.02,0.02)
			},
			{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=0,z=targetpos.y}, speed=8, archeight=2}},
			{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,
					}
				}
			},
		}
	}
			
	InstanceObject(desc,scene)
	return true
end
Essentially, what TestFireball:exeute() is doing is constructing a table descriptor for an object, then instancing that object. The descriptor lists a few essential pieces of information: node transformation (omitted in the above, because the node's transformation in this case is handled by one of the object's components), an array of component descriptors, and an array of child node descriptors.

The components of the fireball are:

1) ParticleEmitter: A basic encapsulation of all the data to feed to an Urho3D ParticleEmitter component. Attached to a node, the ParticleEmitter emits particles. The manner, shape, behavior, color, etc... are all specified as table members. In this case, the data describes a basic particle emitter that emits flame-colored particles at a relatively high rate.

2) Light: A table describing the parameters of an Urho3D point light component. The fireball casts light as it travels, so it needs one of these.

3) SpawnedAction: A LuaScriptObject instance component. These components are implemented as Lua "classes" that provide function hooks that are called from the container component. Essentially, they are scriptable components. SpawnedAction is a utility component that when instanced will log a "tag" in the caster's CombatCommandQueue indicating that the caster has an active action in progress. When the component is destroyed, the tag is removed. This tagging system prevents an object from losing focus (ending its turn) while it has active actions in progress. If you cast a fireball, you can't end your turn until the fireball action completes. As soon as the fireball object is destroyed, then the tag is dropped and things can carry on. (Unless the object spawns another action when it is destroyed that captures focus.)

4) Projectile: Another ScriptObject instance component. This one causes the owning node to move in a projectile arc trajectory from the casting point to the target point. It is responsible for calculating the node's transformation at a given time, t, and for sending the object notice when the target point is reached. When this notice is sent, the node is queued for removal, and at the next game tick it will be destroyed.

5) TestFireballPayload: Another ScriptObject component. This one is classified as a "payload"; it is inert until the node is destroyed. When a ScriptObject component is destroyed, the system will first call the hook method Stop() on any ScriptObject components, to allow the object to do things upon removal. Payloads perform their actions when Stop() is called. In this case, TestFireballPayload (which I'll show in a little bit) spawns a fireball/flame at the target location. So when the Projectile reaches its end and despawns, another object is spawned to create a short-lived blaze of flame.

6) DamageSingleTargetPayload: Another ScriptObject payload component. This one, when Stop() is called, will generate a TakeDamage event sent to a single target at the target location. It will query the object tracker system for the ID of the object at the location, and if there is an object there (there might not be; TestFireball doesn't require a target object) then it will pass that target object a raw damage value of a specified type or types. In this case, it will pass 11 Fire damage. Other payload types might pass out damage to all objects in a certain radius, distribute healing, etc... depending on the needs of the spell.

So that's the TestFireball spell. The end result of all this is a glowing trail of particle fire that arcs from the caster to the target then disappears, triggering its payloads. DamageSingleTargetPayload merely generates a damage event, but TestFireballPayload generates a blaze. Here is it's Stop() method:

function TestFireballPayload:Stop()
	-- On projectile death, spawn an explosion; TODO: This following stuff should probably be moved into object creation
	local x,y,z=self.node:GetPositionXYZ()
	local hc=self.node:GetScene():GetScriptObject("HexmapContainer")
	if hc then
		local tile=hc.hexmap:calcPointTile(x,z)
		local coords=hc.hexmap:calcTileCenter(tile.x,tile.y)
		x,z=coords.x,coords.y
	end
	
	local desc=
	{
		Position={x=x,y=0,z=z},
		components=
		{
			{type="ParticleEmitter", Material="Materials/flame.xml", UpdateInvisible=false, NumParticles=2000, 
				Sorted=false, Relative=false, MinRotationSpeed=-60, MaxRotationSpeed=60, 
				MinVelocity=8, MaxVelocity=10, MinParticleSize=Vector2(0.25, 0.25), MaxParticleSize=Vector2(0.5,0.5), TimeToLive=0.5, MinEmissionRate=300, MaxEmissionRate=800,
				ConstantForce=Vector3(0,10,0), Color=Color(0.5,0.25,0.125), SizeMul=Rect(0.02,0.02), EmitterSize=Vector3(0.75,0.75,0.75),ActiveTime=3.25,
			},
			{type="ScriptObject", Classname="TimedDeath", parameters={ttl=4}},
		},
		
		children=
		{
			{
				Position={x=0,y=2,z=0},
				components=
				{
					{type="Light", LightType=LIGHT_POINT, Color={r=1.25,g=0.75,b=0.45}, Range=4, CastShadows=true},
					{type="ScriptObject", Classname="LightFlicker"},
				}
			}
		}
	}
	
	InstanceObject(desc,self.node:GetScene())
	self.ownernode=nil
end
Here, we have similar structure as the execute() function of TestFireball. An object description is set up and instanced. Some transformation state is set (here, the Position is set, since the explosion doesn't have anything like Projectile to govern its transformation), then components and children are instanced. The components are:

1) ParticleEmitter: Similar settings as the projectile particle system, but slightly tweaked to create more of a blaze than a fireball projectile.

2) TimedDeath: A ScriptObject component that handles killing an object after X time has passed. This utility component is useful for things that should stick around for only a given amount of time. Every update, it ticks down a counter until the counter hits 0, then it's curtains for the node and all its components and children.

The fire object also contains a child node, specified in the children array. Children nodes are held by their parent, and their transformation is specified as relative to the parent transformation. This is useful for building up an object from various pieces. In this case, only one child node is specified, and it is set so that it sits somewhat above (vertically) the parent node. It holds the components:

1) Light: An Urho3D light. The reason for sticking it in a child node is so that I can move it upward so that it shines down upon the object, rather than sitting at Y=0 where the parent node sits. Lights at Y=0 don't illuminate the scene very well.

2) LightFlicker: A ScriptObject component that listens for Update events and at each tick varies the range of any attached lights by a small randomized factor. This varying of the light radius causes a subtle flickering effect, as if it were actual firelight.

When instanced, this system will emit flame particles for a period of 3.25 seconds (specified by the ActiveTime member of the particle emitter) at which time particles will cease being spawned. This whole time, the light shines brightly from above, flickering gently to provide a warm firelight glow. At 4 seconds, the TimedDeath component triggers, causing the object to be despawned and all components destroyed. The particle fire fades away, then the light goes dark, and the fire spell is finished.

And that's pretty much how every single thing in the game works (roughly speaking). Pretty much every line of custom code I have written (outside of a few dozen lines of various state management and utility-type stuff, then various tables to hold data such as spell lists) lives in a component. Some components are parented to the scene itself, since of these types there should only ever be one instance. Others provide the behavior for objects such as fireballs, lootables, goblins, etc... By building up the behavior of each object from smaller pieces, manageable complexity is achieved.




PARTNERS