Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Using the Composite Design Pattern for an RPG Attributes System

$
0
0

Intelligence, Willpower, Charisma, Wisdom: besides being important qualities you should have as a game developer, these are also common attributes used in RPGs. Calculating the values of such attributes — applying timed bonuses and taking into account the effect of equipped items — can be tricky. In this tutorial, I’ll show you how to use a slightly modified Composite Pattern to handle this, on the fly.

Note: Although this tutorial is written using Flash and AS3, you should be able to use the same techniques and concepts in almost any game development environment.


Introduction

Attributes systems are very commonly used in RPGs to quantify characters’ strengths, weaknesses, and abilities. If you’re not familiar with them, skim the Wikipedia page for a decent overview.

To make them more dynamic and interesting, developers often improve these systems by adding skills, items and other things that affect the attributes. If you want to do this, you’ll need a good system that can calculate the final attributes (taking into consideration every other effect) and handle the addition or removal of different types of bonuses.

In this tutorial, we will explore a solution for this problem by using a slightly modified version of the Composite design pattern. Our solution will be able to handle bonuses, and will work on any set of attributes you define.


What Is the Composite Pattern?

This section is an overview of the Composite design pattern. If you are already familiar with it, you might want to skip to Modeling Our Problem.

The Composite Pattern is a design pattern (a well-known, reusable, general design template) for subdividing something big into smaller objects, in order to create a bigger group by handling only the small objects. It makes it easy to break big chunks of information into smaller, more easily treatable, chunks. Essentially, it’s a template for using a group of a particular object as though it was a single object itself.

We’re going to use a widely used example to illustrate this: think of a simple drawing application. You want it to let you draw triangles, squares and circles, and treat them differently. But you also want it to be able to handle groups of drawings. How can we easily do that?

The Composite Pattern is the perfect candidate for this job. By treating a “group of drawings” as a drawing itself, one could easily add any drawing inside this group, and the group as a whole would still be seen as a single drawing.

In terms of programming, we would have one base class, Drawing, which has the default behaviors of a drawing (you can move it around, change layers, rotate it, and so on), and four subclasses, Triangle, Square, Circle and Group.

In this case, the first three classes will have a simple behavior, requiring only the user input of the basic attributes of each shape. The Group class, however, will have methods for adding and removing shapes, as well as doing an operation on all of them (for example, changing the color of all shapes in a group at once). All four subclasses would still be treated as a Drawing, so you don’t have to worry about adding specific code for when you want to operate on a group.

To take this into a better representation, we can view each drawing as a node in a tree. Every node is a leaf, except for Group nodes, which can have children — which are in turn drawings inside that group.


A Visual Representation of the Pattern

Sticking with the drawing app example, this is a visual representation of the “drawing application” we thought about. Note that there are three drawings in the image: a triangle, a square and a group consisting of a circle and a square:

Using the Composite Design Pattern for an RPG Attributes System

And this is the tree representation of the current scene (the root is the drawing application’s stage):

Using the Composite Design Pattern for an RPG Attributes System

What if we wanted to add another drawing, which is a group of a triangle and a circle, inside the group we currently have? We would just add it as we would add any drawing inside a group. This is what the visual representation would look like:

Using the Composite Design Pattern for an RPG Attributes System

And this is what the tree would become:

Using the Composite Design Pattern for an RPG Attributes System

Now, imagine that we’re going to build a solution to the attributes problem we have. Obviously, we’re not going to have a direct visual representation (we can only see the end result, which is the calculated attribute given the raw values and the bonuses), so we’ll start thinking in the Composite Pattern with the tree representation.


Modeling Our Problem

In order to make it possible to model our attributes in a tree, we need to break each attribute into the smallest parts we can.

We know we have bonuses, which can either add a raw value to the attribute, or increase it by a percentage. There are bonuses which add to the attribute, and others that are calculated after all those first bonuses are applied (bonuses from skills, for example).

So, we can have:

  • Raw bonuses (added to the raw value of the attribute)
  • Final bonuses (added to the attribute after everything else has been calculated)

You may have noticed that we are not separating bonuses that add a value to the attribute from bonuses that increase the attribute by a percentage. That’s because we are modeling each bonus to be able to change either at the same time. This means we could have a bonus that adds 5 to the value and increases the attribute by 10%. This will all be handled in the code.

These two kind of bonuses are only the leaves of our tree. They are pretty much like the Triangle, Square and Circle classes in our example from before.

We still haven’t created an entity that will serve as a group. These entities will be the attributes themselves! The Group class in our example will be simply the attribute itself. So we will have an Attribute class that will behave like any attribute.

This is how an attribute tree could look:

Using the Composite Design Pattern for an RPG Attributes System

Now that everything is decided, shall we start our code?


Creating the Base Classes

We will be using ActionScript 3.0 as the language for the code in this tutorial, but don’t worry! The code will be fully commented on afterwards, and everything that is unique to the language (and the Flash platform) will be explained and alternatives will be provided — so if you are familiar with any OOP language, you will be able to follow this tutorial without problems.

The first class we need to create is the base class for any attribute and bonuses. The file will be called BaseAttribute.as, and creating it is very simple. Here is the code, with comments afterwards:

package  
{
	public class BaseAttribute 
	{
		private var _baseValue:int;
		private var _baseMultiplier:Number;
		
		public function BaseAttribute(value:int, multiplier:Number = 0) 
		{
			_baseValue = value;
			_baseMultiplier = multiplier;
		}
		
		public function get baseValue():int
		{
			return _baseValue;
		}
		
		public function get baseMultiplier():Number
		{
			return _baseMultiplier;
		}
	}
}

As you can see, things are very simple in this base class. We just create the _value and _multiplier fields, assign them in the constructor, and make two getter methods, one for each field.

Now we need to create the RawBonus and FinalBonus classes. These are simply subclasses of BaseAttribute, with nothing added. You can expand on it as much as you want, but for now we will only make these two blank subclasses of BaseAttribute:

RawBonus.as:

package  
{
	public class RawBonus extends BaseAttribute 
	{
		public function RawBonus(value:int = 0, multiplier:Number = 0) 
		{
			super(value, multiplier);
		}
	}
}

FinalBonus.as:

package  
{
	public class FinalBonus extends BaseAttribute 
	{
		public function FinalBonus(value:int = 0, multiplier:Number = 0) 
		{
			super(value, multiplier);
		}
	}
}

As you can see, these classes have nothing in them but a constructor.


The Attribute Class

The Attribute class will be the equivalent of a group in the Composite Pattern. It can hold any raw or final bonuses, and will have a method for calculating the final value of the attribute. Since it is a subclass of BaseAttribute, the _baseValue field of the class will be the starting value of the attribute.

When creating the class, we will have a problem when calculating the final value of the attribute: since we’re not separating raw bonuses from final bonuses, there is no way we can calculate the final value, because we don’t know when to apply each bonus.

This can be solved by making a slight modification to the basic Composite Pattern. Instead of adding any child to the same “container” within the group, we will create two “containers”, one for the raw bonuses and other for the final bonuses. Every bonus will still be a child of Attribute, but will be in different places to allow the calculation of the final value of the attribute.

With that explained, let’s get to the code!

package  
{
	public class Attribute extends BaseAttribute 
	{
		private var _rawBonuses:Array;
		private var _finalBonuses:Array;
		
		private var _finalValue:int;
		
		public function Attribute(startingValue:int) 
		{
			super(startingValue);
			
			_rawBbonuses = [];
			_finalBonuses = [];
			
			_finalValue = baseValue;
		}
		
		public function addRawBonus(bonus:RawBonus):void
		{
			_rawBonuses.push(bonus);
		}
		
		public function addFinalBonus(bonus:FinalBonus):void
		{
			_finalBonuses.push(bonus);
		}
		
		public function removeRawBonus(bonus:RawBonus):void
		{
			if (_rawBonuses.indexOf(bonus) >= 0)
			{
				_rawBonuses.splice(_rawBonuses.indexOf(bonus), 1);
			}
		}
		
		public function removeFinalBonus(bonus:RawBonus):void
		{
			if (_finalBonuses.indexOf(bonus) >= 0)
			{
				_finalBonuses.splice(_finalBonuses.indexOf(bonus), 1);
			}
		}
		
		public function calculateValue():int
		{
			_finalValue = baseValue;
			
			// Adding value from raw
			var rawBonusValue:int = 0;
			var rawBonusMultiplier:Number = 0;
			
			for each (var bonus:RawBonus in _rawBonuses)
			{
				rawBonusValue += bonus.baseValue;
				rawBonusMultiplier += bonus.baseMultiplier;
			}
			
			_finalValue += rawBonusValue;
			_finalValue *= (1 + rawBonusMultiplier);
			
			// Adding value from final
			var finalBonusValue:int = 0;
			var finalBonusMultiplier:Number = 0;
			
			for each (var bonus:FinalBonus in _finalBonuses)
			{
				finalBonusValue += bonus.baseValue;
				finalBonusMultiplier += bonus.baseMultiplier;
			}
			
			_finalValue += finalBonusValue;
			_finalValue *= (1 + finalBonusMultiplier);
			
			return _finalValue;
		}
		
		public function get finalValue():int
		{
			return calculateValue();
		}
	}
}

The methods addRawBonus(), addFinalBonus(), removeRawBonus() and removeFinalBonus() are very clear. All they do is add or remove their specific bonus type to or from the array that contains all bonuses of that type.

The tricky part is the calculateValue() method. First, it sums up all the values that the raw bonuses add to the attribute, and also sums up all the multipliers. After that, it adds the sum of all raw bonus values to the starting attribute, and then applies the multiplier. Later, it does the same step for the final bonuses, but this time applying the values and multipliers to the half-calculated final attribute value.

And we’re done with the structure! Check the next steps to see how would you use and extend it.


Extra Behavior: Timed Bonuses

In our current structure, we only have simple raw and final bonuses, which currently have no difference at all. In this step, we will add extra behavior to the FinalBonus class, in order to make it look more like bonuses that would be applied through active skills in a game.

Since, as the name implies, such skills are only active for a certain period of time, we will add a timing behavior on the final bonuses. The raw bonuses could be used, for example, for bonuses added through equipment.

In order to do this, we will be using the Timer class. This class is native from ActionScript 3.0, and all it does is behave like a timer, starting at 0 seconds and then calling a specified function after a specified amount of time, resetting back to 0 and starting the count again, until it reaches the specified number of counts again. If you don’t specify them, the Timer will keep running until you stop it. You can choose when the timer starts and when it stops. You can replicate its behavior simply by using your language’s timing systems with appropriate extra code, if needed.

Let’s jump to the code!

package  
{
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	
	public class FinalBonus extends BaseAttribute 
	{
		private var _timer:Timer;
		
		private var _parent:Attribute;
		
		public function FinalBonus(time:int, value:int = 0, multiplier:Number = 0) 
		{
			super(value, multiplier);
			
			_timer = new Timer(time);
			_timer.addEventListener(TimerEvent.TIMER, onTimerEnd);
		}
		
		public function startTimer(parent:Attribute):void
		{
			_parent = parent;
			
			_timer.start();
		}
		
		private function onTimerEnd(e:TimerEvent):void
		{
			_timer.stop();
			
			_parent.removeFinalBonus(this);
		}
	}
}

In the constructor, the first difference is that final bonuses now require a time parameter, which will show for how long they last. Inside the constructor, we create a Timer for that amount of time (assuming the time is in milliseconds), and add an event listener to it.

(Event listeners are basically what will make the timer call the right function when it reaches that certain period of time – in this case, the function to be called is onTimerEnd().)

Notice that we haven’t started the timer yet. This is done in the startTimer() method, which also requires a parameter, parent, which must be an Attribute. This function requires the attribute which is adding the bonus to call that function in order to activate it; in turn, this starts the timer and tells the bonus which instance to ask to remove the bonus when the timer has reached its limit.

The removal part is done in the onTimerEnd() method, which will just ask the set parent to remove it and stop the timer.

Now, we can use final bonuses as timed bonuses, indicating that they will last only for a certain amount of time.


Extra Behavior: Dependant Attributes

One thing commonly seen in RPG games are attributes that depend on others. Let’s take, for example, the “attack speed” attribute. It’s not only dependant on the type of weapon you use, but almost always on the character’s dexterity too.

In our current system, we only allow bonuses to be children of Attribute instances. But in our example, we need to let an attribute be a child of another attribute. How can we do that? We can create a subclass of Attribute, called DependantAttribute, and give this subclass all the behavior we need.

Adding attributes as children is very simple: all we have to do is create another array to hold attributes, and add specific code for calculating the final attribute. Since we do not know whether every attribute will be calculated in the same way (you might want to first use dexterity to change the attack speed, and then check the bonuses, but first use bonuses to change magical attack and then use, for example, intelligence), we will also have to separate the calculation of the final attribute in the Attribute class in different functions. Let’s do that first.

In Attribute.as:

package  
{
	public class Attribute extends BaseAttribute 
	{
		private var _rawBonuses:Array;
		private var _finalBonuses:Array;
		
		protected var _finalValue:int;
		
		public function Attribute(startingValue:int) 
		{
			super(startingValue);
			
			_rawBbonuses = [];
			_finalBonuses = [];
			
			_finalValue = baseValue;
		}
		
		public function addRawBonus(bonus:RawBonus):void
		{
			_rawBonuses.push(bonus);
		}
		
		public function addFinalBonus(bonus:FinalBonus):void
		{
			_finalBonuses.push(bonus);
		}
		
		public function removeRawBonus(bonus:RawBonus):void
		{
			if (_rawBonuses.indexOf(bonus) >= 0)
			{
				_rawBonuses.splice(_rawBonuses.indexOf(bonus), 1);
			}
		}
		
		public function removeFinalBonus(bonus:RawBonus):void
		{
			if (_finalBonuses.indexOf(bonus) >= 0)
			{
				_finalBonuses.splice(_finalBonuses.indexOf(bonus), 1);
			}
		}
		
		protected function applyRawBonuses():void
		{
			// Adding value from raw
			var rawBonusValue:int = 0;
			var rawBonusMultiplier:Number = 0;
			
			for each (var bonus:RawBonus in _rawBonuses)
			{
				rawBonusValue += bonus.baseValue;
				rawBonusMultiplier += bonus.baseMultiplier;
			}
			
			_finalValue += rawBonusValue;
			_finalValue *= (1 + rawBonusMultiplier);
		}
		
		protected function applyFinalBonuses():void
		{
			// Adding value from final
			var finalBonusValue:int = 0;
			var finalBonusMultiplier:Number = 0;
			
			for each (var bonus:RawBonus in _finalBonuses)
			{
				finalBonusValue += bonus.baseValue;
				finalBonusMultiplier += bonus.baseMultiplier;
			}
			
			_finalValue += finalBonusValue;
			_finalValue *= (1 + finalBonusMultiplier);
		}
		
		public function calculateValue():int
		{
			_finalValue = baseValue;
			
			applyRawBonuses();
			
			applyFinalBonuses();
			
			return _finalValue;
		}
		
		public function get finalValue():int
		{
			return calculateValue();
		}
	}
}

As you can see by the highlighted lines, all we did was create applyRawBonuses() and applyFinalBonuses() and call them when calculating the final attribute in calculateValue(). We also made _finalValue protected, so we can change it in the subclasses.

Now, everything is set for us to create the DependantAttribute class! Here’s the code:

package  
{
	public class DependantAttribute extends Attribute 
	{
		protected var _otherAttributes:Array;
		
		public function DependantAttribute(startingValue:int) 
		{
			super(startingValue);
			
			_otherAttributes = [];
		}
		
		public function addAttribute(attr:Attribute):void
		{
			_otherAttributes.push(attr);
		}
		
		public function removeAttribute(attr:Attribute):void
		{
			if (_otherAttributes.indexOf(attr) >= 0)
			{
				_otherAttributes.splice(_otherAttributes.indexOf(attr), 1);
			}
		}
		
		public override function calculateValue():int
		{
			// Specific attribute code goes somewhere in here
			
			_finalValue = baseValue;
			
			applyRawBonuses();
			
			applyFinalBonuses();
			
			return _finalValue;
		}
	}
}

In this class, the addAttribute() and removeAttribute() functions should be familiar to you. You need to pay attention to the overriden calculateValue() function. Here, we don’t use the attributes for calculating the final value — you need to do it for every dependant attribute!

This is an example of how you would do that for calculating the attack speed:

package  
{
	public class AttackSpeed extends DependantAttribute 
	{
		public function AttackSpeed(startingValue:int) 
		{
			super(startingValue);
		}
		
		public override function calculateValue():int
		{
			_finalValue = baseValue;
			
			// Every 5 points in dexterity adds 1 to attack speed
			var dexterity:int = _otherAttributes[0].calculateValue();
			
			_finalValue += int(dexterity / 5);
			
			applyRawBonuses();
			
			applyFinalBonuses();
			
			return _finalValue;
		}
	}
}

In this class, we assume that you have added the dexterity attribute already as a child of AttackSpeed, and that it’s the first in the _otherAttributes array (that’s a lot of assumptions to make; check the conclusion for more info). After retrieving the dexterity, we simply use it to add more to the final value of the attack speed.


Conclusion

With everything finished, how would you use this structure in a game? It’s very simple: all you need to do is create different attributes and assign each of them an Attribute instance. After that, it’s all about adding and removing bonuses to it through the already created methods.

When an item is equipped or used and it adds a bonus to any attribute, you need to create a bonus instance of the corresponding type and then add it to the character’s attribute. After that, simply recalculate the final attribute value.

You can also expand on the different types of bonuses available. For example, you could have a bonus that changes the added value or multiplier over time. You can also use negative bonuses (which the current code can already handle).

With any system, there’s always more you can add. Here are a few suggested improvements you could make:

  • Identify attributes by names
  • Make a “centralized” system for managing the attributes
  • Optimize the performance (hint: you do not always have to calculate the final value entirely)
  • Make it possible for some bonuses to attenuate or strengthen other bonuses

Thanks for reading!


Viewing all articles
Browse latest Browse all 728

Trending Articles