Internship report
Titouan DESLANDES - ENJMIN 2015-2017. Internship from July 1st to August 26th 2016.
Table of content
# Introduction
# The studio
Bulwark Studios was founded in Angoulême on november 2012 by two developers willing to make quality video games for the booming market of smartphone and tablets.
After a few years of activity, the now 6 men studio has released 3 titles on PC, tablet and smartphones while offering their services as indie developers for freelance contracts.
Some of Bulwark Studios' original projects:
Ronin (November 2012, iOS, infinite runner)
Ronin is an infinite runner in which the player plays as a samurai running after a mysterious thief who robbed his last pay. Using intuitive gestures, the players has to avoid obstacles and ward off the threats on his way.
Spin Safari (June 2013, iOS / Android, puzzle game, match-3)
The game challenges the player to match animals with each other across more than a hundred levels spread in many different ecosystems of the world.
Crowntakers (November 2014, Steam / iOS / Android, tactical RPG)
Crowntakers combines turn-based strategy with RPG elements and takes you at the behest of the crown into a medieval fantasy world full of challenging encounters and epic adventures. With its randomly generated world, Crowntakers offers a varied gaming experience every time you set out to fight evil. Embark on challenging quests, gather vital resources and finally banish evil from the world.
# Space Arch
Being at Bulwark Studios, I had the opportunity to work on a yet to be announced game called Space Arch. It is a real time strategy game developed for PC and potentially iOS and Android.
The game features a group of humans who just survived a crash on an unknown planet. They will be confronted to the local flora and fauna, be it intelligent creatures or violent beasts.
As the player, you will have to take advantage of the different skills of your crew to gather resources, fight foes, build a shelter... in short: survive.
# Intentions
The following chapters describe from a technical point of view some of the problem we were confronted with. Most of the code described cannot be run as is because it depends on a complex architecture I cannot detail in such a short document, but you should be able to get the idea nevertheless.
# Prerequisite system
# Introduction
One of the first things we prototyped in the game was selecting units and giving them orders.
We've decided that every entity in the game would be a Node
, and we're giving them orders using NodeCommand
, following the command design pattern.
Every Node
has a list of all the NodeCommand
it has to execute, chaining them one after the other.
For instance if we select a Node
and then right click on the ground, we'll create a NodeCommandWalk
, add it to the Node
's command list and when the Node
executes this command, it will move to the destination we clicked.
We also have NodeBehaviour
which can be added to a Node
to give it the ability to perform actions, for example a Node
will be required to own a NodeBehaviourWalk
to be able to walk.
With this system in place, we started to expend it by writing more NodeCommand
, such as NodeCommandHarvest
that tells a Node
to harvest another one, or NodeCommandAttack
to fight foes.
Pretty quickly, the first few lines of every NodeCommand
started to look the same: a bunch of conditions to verify we're actually "authorized" to execute the command.
if (Node.HasComponent<NodeBehaviourHarvester> && Target.HasComponent<NodeBehaviourHarvestable>)
{
if (Utils.Distance(Node, Target) <= threshold))
{
// ...
}
}
Besides looking ugly, it felt like a design flaw to mix the command's logic with the prerequisites of the command. When the command is executed it should do it's job regardless of the context, but it shouldn't be executed in the first place if the context prevents it.
# Writing the prerequisite system
With this flaw in mind, let's sum up what we really want to achieve with these commands.
- Ideally we want to create a command and give it some prerequisites.
- We want those prerequisites to be met before executing the command, else there's no point in executing it because we're not supposed to.
- Furthermore we want each prerequisite to be able to solve itself if it's not met. For example, if I'm too far from a
Node
I want to harvest, I don't want to cancel my harvest command, I want to move to my target first and THEN start my harvest command.
Now that we know what we want, let's formalize it into something that fits our needs.
enum NODE_CONDITION_STATUS
{
VALID,
INVALID,
FIXABLE
}
interface INodeCommandCondition
{
NODE_CONDITION_STATUS Evaluate(Node node);
NodeCommand TrySolve(Node node);
}
This block declares:
- A
NODE_CONDITION_STATUS
. It can be VALID if the conditions are met. INVALID if they're not. Or FIXABLE if the conditions are not met but we know we can find a way to fulfill them. - An Evaluate method that returns a
NODE_CONDITION_STATUS
. This method will hold all the logic to tell if the conditions are met or not. - A TrySolve method that will return the command needed to validate the Evaluate method
We now want to adapt our NodeCommand
system to take into
account those conditions so that when we try to execute a command, we
actually verify the prerequisite first, fix them, and THEN execute the
command.
When the Node
executes it's NodeCommand
we'll just fire the prerequisite check & solve method. This method simply calls Evaluate()
on every NodeCommand
and stores the result. Then we run through the results and decide what to do.
// In the Node
public void TryExecute()
{
if(CurrentCommand.CheckAndSolvePrerequisites())
{
CurrentCommand.Execute();
}
}
// In the Command
protected bool CheckAndSolvePrerequisites()
{
// Create a dictionary that stores the status of the prerequisite for each prerequisite and populate it
Dictionary<INodeCommandCondition, NODE_CONDITION_STATUS> prerequisitesStatus = new Dictionary<INodeCommandCondition, NODE_CONDITION_STATUS>();
foreach (INodeCommandCondition prerequisite in prerequisites)
prerequisitesStatus.Add(prerequisite, prerequisite.Evaluate(node));
// If any of the prerequisite was INVALID
if (prerequisitesStatus.Any(status => status.Value == NODE_CONDITION_STATUS.INVALID))
{
// Complete the current command because we're not supposed to execute it
node.CompleteCommand();
return false;
}
// If all the prerequisites are VALID
if (prerequisitesStatus.All(status => status.Value == NODE_CONDITION_STATUS.VALID))
{
// We're legal, do the job
return true;
}
// Else if all the prerequisites are either VALID or FIXABLE
else if (prerequisitesStatus.All(status => status.Value == NODE_CONDITION_STATUS.FIXABLE || status.Value == NODE_CONDITION_STATUS.VALID))
{
// For each FIXABLE prerequisite try to solve it and add it to the node command queue
foreach (var fix in prerequisitesStatus)
{
if (fix.Value == NODE_CONDITION_STATUS.FIXABLE)
{
// When we solve a prerequisite it returns a Command
node.AddCommand(fix.Key.TrySolve(node));
}
}
// Add the current command to the end so it'll be executed after we've done all the prerequisites
node.AddCommand(this);
// Then skip the current command because it's not valid anymore since we needed fixes
node.CompleteCommand();
return false;
}
return false;
}
So at this point the Node
does the following:
- Receive
NodeCommand
and store them. - Go through each
NodeCommand
one by one and:- Verify it's prerequisites
- Solve them if needed by adding their solution to the command queue
- Re-queue the current command AFTER the solved prerequisites
- Execute the
NodeCommand
# Using the prerequisite system
Let's implement the interface we created earlier with a simple condition: we want the Node
to have a certain NodeBehaviour
attached to it.
public class ConditionHasNodeBehaviour<T> : INodeCommandCondition where T : NodeBehaviour
{
public NODE_CONDITION_STATUS Evaluate(Node node)
{
// If we own the required component the condition is VALID
if (node.HasComponent<T>() != null)
{
return NODE_CONDITION_STATUS.VALID;
}
// If we don't, it's INVALID
else
{
return NODE_CONDITION_STATUS.INVALID;
}
}
public NodeCommand TrySolve(Node node)
{
// This condition cannot be solved
return null;
}
}
Now when we create a NodeCommandWalk
we can simply pass the prerequisites in the constructor:
// Ctor signature: NodeCommandWalk(Node node, Transform destination, params INodeCommandCondition[] prerequisites) : base(node, prerequisites)
NodeCommandWalk command = new NodeCommandWalk(node, destination,
new ConditionHasNodeBehaviour<NodeBehaviourWalk>());
What about something we can fix? A distance check condition!
public class ConditionDistanceCheck : INodeCommandCondition
{
private Node targetNode;
private float minDistance;
public ConditionDistanceCheck(Node target, float minDistance = 10f)
{
this.targetNode = target;
this.minDistance = minDistance;
}
public NODE_CONDITION_STATUS Evaluate(Node node)
{
// If we're too far from the targetNode
if (Utils.Distance(node, targetNode) > minDistance)
{
// If the node has a walk behaviour it means that this condition is FIXABLE because we can walk
if (new ConditionHasNodeBehaviour<NodeBehaviourWalk>().Evaluate(node) == NODE_CONDITION_STATUS.VALID)
{
return NODE_CONDITION_STATUS.FIXABLE;
}
// We cannot move, the condition is INVALID
else
{
return NODE_CONDITION_STATUS.INVALID;
}
}
// We're close enough to the target, the condition is VALID
else
{
return NODE_CONDITION_STATUS.VALID;
}
}
public NodeCommand TrySolve(Node node)
{
// We should double check here that we have the right NodeBehaviour, distance, etc...
// Return the CommandWalk to move to destination
return new NodeCommandWalk(node, targetNode);
}
}
Now when we want to harvest a Node
we simply do:
NodeCommandHarvest command = new NodeCommandHarvest(node, targetNode,
new ConditionHasNodeBehaviour<NodeBehaviourHarvester>(),
new ConditionHasNodeBehaviour<NodeBehaviourHarvestable>(targetNode),
new ConditionDistanceCheck(targetNode, 5f));
What's great about this system is that the prerequisites are nestable.
A Node might want to go and mine something. To do so he needs a pickaxe, but he doesn't have one. So he solves this by searching for a pickaxe. But to search he needs to walk, creating a series of SearchCommand, WalkCommand, GrabCommand, etc... all from a single MineCommand.
# Item decorators
# Introduction
Every character in the game will be able to hold items in his inventory and eventually wield them. These items could be artifacts you found, unknown stuff you want to analyze, weapons, tools, etc... so we need a way to represent all the possible variations and what they imply.
To get things started we simply created an Inventory
class containing a list of InventoryItem
that each Node
could have.
We could have used polymorphism to create many item classes like so Tool : InventoryItem
, Pickaxe : Tool
, FastPickaxe : Pickaxe
,
... but that means we had to describe every single combination of item
and item effect. And we don't even know yet what kind of effect we'll
want to have on items in a few weeks.
That's where the decorator pattern came to our rescue. Wikipedia summarizes it like this:
The decorator pattern allows behavior to be added to an individual object without affecting the behavior of other objects from the same class.
This means that we can describe a very basic item with minimalist properties, such as a name and a description, and extend it's functionality by "decorating" it with new behaviors. Also it means we could have "decorator" that increases the movement speed and put it on any item without changing the item's class.
It sounds exactly like what we need.
# Architecture and implementation
The BaseInventoryItem
class is the uppermost class. It
has a Name, a Description and a GetLongDescription method that will
display... a long description of the item.
public abstract class BaseInventoryItem
{
public string itemName { get; set; }
public string description { get; set; }
public BaseInventoryItem(string itemName, string description)
{
this.itemName = itemName;
this.description = description;
}
public virtual string GetLongDescription()
{
return description;
}
}
The BaseInventoryItemDecorator
inherits from this class
and also has a reference to an instance of his parent. It implements a
few new methods and also overrides GetLongDescription by returning it's
own description AND calling the base item's method. That's where the
decorator pattern makes sense : by always calling the base item's
method, we recursively make use of all the decorators we chained.
public class BaseInventoryItemDecorator : BaseInventoryItem
{
protected BaseInventoryItem baseItem;
protected BaseInventoryItemDecorator(BaseInventoryItem item)
{
baseItem = item;
itemName = item.itemName;
description = "Empty decorator";
}
public virtual void OnEquip(Node owner) { baseItem.OnEquip(owner); }
public virtual void OnUnEquip(Node owner) { baseItem.OnUnEquip(owner); }
public override string GetLongDescription()
{
return string.Format("{0}, {1}", baseItem.GetLongDescription(), description);
}
}
Lastly, we have item decorators that inherits from BaseInventoryItemDecorator
. In this example a MiningDecorator
will increase the mining speed and a MovespeedDecorator
will allow it's owner to run faster.
public class MiningSpeedItemDecorator : BaseInventoryItemDecorator
{
private Buff speedBuff;
public MiningSpeedItemDecorator(BaseInventoryItem item, float increasedSpeed) : base(item)
{
description = "improved mining speed";
speedBuff = new Buff(BUFF_TYPE.ADDITIVE, increasedSpeed);
}
public override void OnEquip(Node owner)
{
baseItem.OnEquip(owner);
owner.miningSpeed.AddBuff(speedBuff);
}
public override void OnUnEquip(Node owner)
{
baseItem.OnUnEquip(owner);
owner.miningSpeed.RemoveBuff(speedBuff);
}
}
Now we can create some items!
InventoryItem pickaxe = new InventoryItem("Pickaxe", "a dull pickaxe");
MiningSpeedItemDecorator fastPickaxe = new MiningSpeedItemDecorator(pickaxe, +10f);
MovespeedItemDecorator shoePickaxe = new MovespeedItemDecorator(fastPickaxe, +10f);
In the few example I gave, the decorators were pretty simple but one can override the OnUse method to... teleport the owner next to his target, or increase multiple stats at once, or change his skin and add some particles.
What's really great about this system is that once we have a few decorators and a few items, we can procedurally create all sort of new items without touching the existing code. We can also decorate items on the fly, allowing the player to improve the items he already has, or lose a decorator because he broke it.
# Conclusion
I would like to thank Bulwark Studio's team for their friendly welcome, especially Emmanuel Monnereau, Jeremy Guery and Tatiana Barbesolle. Working with them was a pleasure and I've learned a lot during this internship. A big thank you too to François Rivoire and Alexandre Dewagnier for the fun we had working at the same place.