Liquid Fire
Liquid Fire
Liquid Fire
I’ve decided to start a new section of my blog where I create working game samples and then blog
about them. I have been working on a Tactics RPG game for awhile now and you can see the
results so far in this sample video:
In this project we will focus on the creation of an RPG game engine. I spent a lot of time looking at
various games in the Final Fantasy series for inspiration and then decided to focus on Final
Fantasy Tactics (Game Boy Advance). I came to this conclusion because much of the elements in
a Tactics game (job-based stat growth, abilities, etc) could also be used in a non-tactics game, but
the tactics game adds additional complexity due to the game board (path nding, etc) that
wouldn’t have been addressed otherwise.
Prerequisites
This series contains intermediate to advanced level material, therefore I will assume you are
familiar with C# and Unity. If you are new to C# I would recommend following along with my own
tutorial series here.
I will also make use of code and ideas written in previous posts. I recommend you read over the
following:
At the time of this writing I am using Unity 5.0.0f4 Personal edition (one of my goals is to keep
everything completely compatible with the free version). You can download Unity here.
Download and try out the project using this link. Note that development on this project is
currently on-going and is VERY likely to be cleaned up and or refactored if not just completely
changed over time, particularly as I actually start writing about each part.
It would be bene cial to play the game before following along so you can get a better grasp of all
the features we will be adding. Some of the features I have added so far include:
Dynamic UI – menus which take advantage of Unity’s new UI tools. This includes UI which
displays the selected unit and its stats, a command menu, a coversation panel, etc.
Game State Machine – allows the ability to play a complete battle by entering commands to
move units, select and con rm actions, etc. Moves can also be undone.
3D non-square battle maps. Battle maps can be randomly generated and or modi ed by hand.
They can take any shape and the height of each tile can be modi ed as desired. This also
affects game play such as by the jump-height restriction on a unit’s movement.
Camera system – smoothly follows selected units and cursor movement.
Flexible navigation system – Units can have different means of path nding and moving
around the game board such as by walking, ying or teleporting.
Job system – used for leveling up units with a good variety of stats. Theses stats have an
affect on gameplay such as the speed at which units get turns, or the amount of damage they
in ict or receive from various attacks.
Ability system – used for attacking, etc.
Animation system – UI animates into and out of the screen, units tween from tile to tile along
their movement path, etc.
The Liquid Fire
Game Programming Blog
In this project we will create and con gure a new project to hold our Tactics RPG game engine.
We will add folders for organization, import some existing assets, and create a few more assets
which we will begin implementing in future lessons.
Project Creation
Open Unity and choose the New Project button from the initial dialog. Alternatively if Unity was
already open you can use the menu bar File->New Project…. Provide an appropriate project name
and location in the dialog which appears, leave the default mode to 3D, and then click the Create
project button to con rm your settings.
Folder Setup
Project organization is very important, and becomes increasingly important as your project
grows. Whenever I create a new Unity project, I usually begin by creating a few folders I know I
will use in order to help encourage proper habits.
To create a folder you can either right-click inside of the Project pane and choose Create->Folder
from the context menu, or use the button just beneath the tab at the top of the project pane.
Editor
Materials
Prefabs
Resources
Scenes
Scripts
Settings
Textures
For the most part you can name your folders and organize your project however you like, but it is
worth pointing out that two of these folders, Editor and Resources are special to Unity. Click Here
for a more complete list of Unity’s special folders and their purposes.
Tip:
If you accidentally nest the folders when creating them, you can click and drag the
folder back out to the root level from within the project pane. While dragging, a thin blue
line will indicate moving a folder parallel to another folder and a thick blue line over a
folder will indicate you would be making it a sub folder.
Import Assets
It is relatively common to start a project using existing assets or plugins. I have created a few to
help get this project started.
02_TacticsRPG_Textures.zip – this .zip le contains a few textures we can use for our UI and for
the game board tiles. I added a folder inside of Textures called UI and placed all textures there
with the exception of Dirt.png which I left in the root of the Textures folder.
Note that you can use Unity’s Asset Packages for exporting and importing assets, and they will
maintain things such as folder hierarchy, import settings etc. However, I decided not to use them
just in case forward compatibility might be an issue.
Multi-select all of the UI textures we just imported. Change the Texture Type from Texture to
Sprite (2D and UI). This step is required in order to use our textures in Unity’s new UI components
such as Image.
Set the Packing Tag to UI. This causes all of the images to be saved to a single texture called a
Sprite Atlas for optimization sake. Note that this feature is currently not compatible with textures
located in the Resources folder.
Tip:
You can view the atlases which Unity has created to verify that everything is working as
you intended. From the menu bar choose Window->Sprite Packer. The atlases are auto
created when entering play mode or by creating a build of your project, but you can also
trigger their creation by clicking the Pack button in the top left of this window. There
should be an atlas for each of the packing tags you have speci ed, and you can view the
corresponding atlas from the View atlas: pull down menu.
Several of these textures will have an Image Type set to Sliced which allows the textures to
stretch in speci ed areas, while keeping the corners unstretched. You can modify the slice
locations by selecting a single sprite and then clicking the Sprite Editor button from the inspector
pane. In the Sprite Editor dialog which appears, adjust the Border values L, T, R, and B which stand
for Left, Top, Right, and Bottom. As you manipulate the values you should see green lines overlay
your sprite indicating where the slice will occur. Following is a list of images which need slicing
data, and the values used in L,T,R,B order:
In the project pane, select the Materials folder and then from the Create button choose Material.
This should create a new material inside of the appropriate folder. Name the material Dirt. There
is a round mark to the left of Albedo – tap this mark to open up the Select Texture dialog, and
select Dirt. Since Dirt isn’t shiny, set the Metallic Smoothness to 0.
The Dirt material will be used as a placeholder on the board tiles we create. It is a simple brown
texture with a darker border to help visually differentiate where tiles are located.
Next we will create two materials which will help identify the hero units vs the enemy units.
Name them Ally and Enemy and drop the Metallic Smoothness to 0.25. Give the Ally a green color
on Albedo such as RGB {86, 200, 88}. Give the Enemy a red color on Albedo such as RGB {183, 43,
61}.
We will let our units share a material representing their eyes (we will use this to help determine
which way our units are facing). So create another material called Eyes.
Finally, we will use one more material on an object which represents which tile is currently
selected, as well as for a facing indicator. Create a new material called Selection and give it a
yellow color on Albedo such as RGB {255, 255, 0}
Create Objects
First let’s create the board tile. From the menu bar choose GameObject->3D Object->Cube. Name
the object Tile and assign the Dirt material to the Mesh Renderer in the inspector (or by drag and
drop in the scene view). Now drag and drop the Tile GameObject from the Hierarchy pane onto
the Prefabs folder in the Project pane. This creates a prefab of our Tile. Now you can delete the
Tile instance from the Scene (by right-clicking the Tile in the Hierarchy pane and choosing
Delete). You will be able to recreate new instances (copies) of your Tile prefab at run time.
Next let’s create a generic unit for our heroes. Start by creating an empty GameObject – from the
menu bar choose GameObject->Create Empty. Name the object Hero. This root object will be used
as a handle to position the unit on the board. Add another empty GameObject as a child (make
one object the child of another by dragging it onto the object in the Hierarchy pane), and name it
Jumper. This object will be used to animate the vertical arc of a jump when a Unit traverses
between non-level ground. Now create a sphere as a child of the Jumper GameObject – From the
menu bar choose GameObject->3D Object->Sphere. Change the Scale of the sphere to XYZ {0.8, 0.8,
0.8}. Assign the Ally material to this sphere. Create another sphere as a child of this sphere to
represent an eye. Scale the eye to XYZ {0.25, 0.25, 0.25} and set its Position to XYZ {0.17, 0.32, 0.3}.
Assign the Eyes material to this sphere. Duplicate the eye (from the menu bar choose Edit-
>Duplicate) and move the copy to -0.17 on the Position’s X axis.
Duplicate the Hero and rename the copy to Monster. Change the material on the body from Ally
to Enemy. Now drag both the Hero and Monster objects to the Prefabs folder one at a time in order
to create Prefabs for each. Delete the original instances from the scene when you are done.
The last object we will make will be an object which marks the currently selected board tile. It will
look like there are angle brackets at each of the four corners of a square unit. Start by making an
empty GameObject named Tile Selection Indicator. Then add a Cube as a child. Assign this cube
the Selection material. Remove the Box Collider Component (Right click its title and choose
Remove Component). Change the Scale of the cube to XYZ{0.1, 0.1, 0.3}. Now clone the cube until
there are 8 total. Use the following transform values for the cubes:
After modifying assets in your project, you should always save the project itself. From the menu
bar choose File->Save Project.
Summary
In this lesson we created our project and got several assets ready for use in the next lessons. We
discussed the importance of a well organized project and learned how to create and modify folder
hierarchies. We covered how to import existing assets into our project and modify import
settings as necessary. We discussed atlases, sprites and sliced sprites for 9-slice stretching in the
UI. Finally we created materials and prefabs to represent our game board, units, and a tile
selection indicator.
The Liquid Fire
Game Programming Blog
In this lesson we will focus on creating one of our pre-production tools. We will create a scene
from which we can generate boards to ght on. Along the way we will create an editor script for
enhancing the inspector which will allow us to both randomly generate and hand modify the end
result. Finally we will use scriptable objects to persist our data.
Before we get started, I want to mention that I have decided to share a repository for this project
here. Each commit to the master branch will re ect a “release” (a blog post), so by checking out
those commits you can see what the project looked like before or after each week’s tutorial. The
completed version from last week, this week, and (if you are impatient) next weeks is already
there.
Point Struct
The location of each tile on the board will be represented by a struct holding two int values. There
is no data structure in Unity (or native C#) which matches our need, so we need to create one
ourselves. We will call it a Point.
Create a sub-folder called Model under the Scripts folder. The use of the word “Model” here refers
to an architectural pattern called “MVC” which stands for “Model View Controller” and should not
be confused with a “Model” in the sense of a 3D mesh. A “Model” in this pattern is an object which
holds data – and that’s pretty much it. Create a new script named Point.cs located within our
Model folder. Remove the template code Unity provided for us (the Start and Update methods) –
we wont be needing them here.
Just above the declaration of the Point class, add the tag:
[System.Serializable]
so that this data container will be able to display properly in the inspector.
I will be creating a lot of Point instances (while path nding, etc.) but I dont want to make a ton of
new allocations and add to the burden of garbage collection. Therefore, I will change this from a
class to a struct. Change the declaration from this:
public class Point : MonoBehaviour
to this:
If you aren’t familiar with structs and their pro’s, cons’s and gotcha’s, I would recommend you
read my post on them here
Our struct contains two int elds. By convention these values will be named x and y, much like
you may have seen with using a Vector2. However, in our board the x and y point values will
actually be referring to x and z coordinates in 3D space. Hopefully that is not too confusing. Add
the following eld declarations to our script.
1. public int x;
2. public int y;
Rather than having to create instances of our struct and then assign elds one at a time like
this…
Add the following code (our constructor) beneath our eld variables
It would be convenient to be able to work with our Point struct like other value types. For
example, add points using the ‘+’ or check equality using ‘==’. This can be done through
something called operator overloading. Add the following methods benath our constructor so
that we can easily add, subtract, and compare equality or inequality:
Because we have implemented the equality operator, it is expected that we will also override
Equals and GetHashCode. You can read more on these topics at the links provided below:
As a side bonus of following these guidelines, your code will run faster! The default
implementation of Equals works through re ection which is not as ef cient as our direct
implementation. Add the following methods to our Point struct beneath the operator overloads.
Also note that I added the simple version of GetHashCode, but there are faster versions which are
better suited for avoiding collisions (read the link I provided above for more).
When developing something complex like this particular project, I often nd myself liberally
using Debug.Log messages all over my code. Rather than having to manually print out the x and y
values of our struct, it would be nice to be able to just log the Point and let it determine how to
print out its own contents. We can do this by overriding the ToString method as follows:
Tile Script
Not everything ts well into the “MVC” architectural pattern. Sometimes it makes more sense to
blur the lines between the model and view. Actually there is another pattern called “MVVM”
which stands for Model-View-ViewModel which might feel a bit more aligned with our next
script. To be most correct I am just creating a “Component” which is a great architectural pattern
all by itself, but because I can have controllers that are components etc, it feels a little confusing
to organize by that word.
For the organization of our next script I decided on creating a sub-folder of Scripts called View
Model Component but if you dont like that, feel free to rename and organize however you wish.
Create a new script called Tile.cs. In the Project pane, select the Tile prefab and then in the
inspector click the Add Component button. Type Tile and select our new script when it appears.
Because we have modi ed a prefab, make sure to save the project.
Open the Tile script so we can begin editing it. I dont want the tiles on the board to be as tall as
they are wide. I would like four steps to be the equivalent height of the width of a tile. Of course
it’s possible I will change my mind on this height during development, so I will make use of a
const which I can modify later if necessary. Add the following to your script:
I also want the tile to be able to track its own position and height, so add the following elds:
Next I will add a convenience property called center which lets me place other objects in the
center of the top of the tile:
1. public Vector3 center { get { return new Vector3(pos.x, height * stepHeight, pos.y);
}}
Anytime the position or height of a tile is modi ed, I will want it to be able to visually re ect its
new values. Add the following method:
1. void Match ()
2. {
3. transform.localPosition = new Vector3( pos.x, height * stepHeight / 2f, pos.y );
4. transform.localScale = new Vector3(1, height * stepHeight, 1);
5. }
Our board creator will create the boards by randomly growning and or shrinking tiles. Let’s create
methods to allow the data to be changed and the view to update at the same time:
Finally I will add another method called Load but overload it to accept various parameter types.
This will make it easy for me to persist the Tile data as a Vector3 later.
The data that represents a gameboard will be saved to a Scriptable Object called LevelData. All I
need to store is the position and height of each board tile, so a List of Vector3 would be suf cient
to store my data without requiring me to create another data type.
Create a new script called LevelData.cs in the Scripts/Model folder. Because this script is so
simple I will just post it in its entirety:
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class LevelData : ScriptableObject
6. {
7. public List<Vector3> tiles;
8. }
Board Creator
For organization sake, start out by adding a sub-folder to the Scripts folder called PreProduction.
This helps me separate things which are used to create the game, but not used in the game itself.
Inside the PreProduction folder create a new script called BoardCreator.cs.
I want the board creator to be a visual editor, so I will need to provide it a reference to our Tile
prefab. I will also want to be able to hand modify the board one tile at a time. To show the selected
Tile, we will provide a reference to our Tile Selection Indicator prefab. Add the following elds to
our script:
I used the tag [SerializeField] to expose these elds in the inspector without them being
visible to any other script, although it would have been ok to just leave them as public instead.
The instances of tiles will be created and destroyed by custom buttons we will add to the
inspector that cause tiles to grow and shrink. However, there wont be a trigger to create the
selection indicator. In order to make sure it exists whenever I need to use it, I will make use of
something called Lazy Loading. This pattern is implemented through a getter of our property
which checks whether or not the object has been instantiated, and if necessary it will instantiate
it. Add the following to our script:
1. Transform marker
2. {
3. get
4. {
5. if (_marker == null)
6. {
7. GameObject instance = Instantiate(tileSelectionIndicatorPrefab) as GameObject;
8. _marker = instance.transform;
9. }
10. return _marker;
11. }
12. }
13. Transform _marker;
Next I create a Dictionary which maps from a Point struct to a Tile instance. Our board will be
non-square and could have holes, etc, so this will make it easy to determine whether or not our
board contains a tile based on a speci ed coordinate position, as well as to grab the reference to
it. Add the following line:
System.Collections.Generic; at the top of your script. Alternatively, you can right click on
Dictionary and then choose Resolve->using System.Collections.Generic; to have MonoDevelop
automatically add the dependency.
The next three elds specify the maximum ranges or extents that the board should reach. The
width eld maps to world space X units. The depth eld maps to wold space Z units. The height
eld maps to step units as de ned by the Tile script (smaller than world units). The concept of a
step will have an affect on a units ability to traverse the terrain based on their stats (such as jump
height). Add the following elds to your script:
Let’s add another eld called pos to represent where individual modi cations to the game board
would be made (regardless of whether or not a tile exists there yet).
We will want our script to be able to load previously saved boards so we can edit them again later.
In order to facilitate this requirement we will expose a eld which accepts a LevelData:
The quickest way to rough out a level will be through Growing and Shrinking tiles in a random
area. The methods will be triggered through an Editor Inspector script which we will write next,
but in order for that to work, the methods we want triggered should be public. Add the following
methods:
The RandomRect method call generates a Rect struct somewhere within the region we speci ed
by our max extents elds above. Add the following:
1. Rect RandomRect ()
2. {
3. int x = UnityEngine.Random.Range(0, width);
4. int y = UnityEngine.Random.Range(0, depth);
5. int w = UnityEngine.Random.Range(1, width - x + 1);
6. int h = UnityEngine.Random.Range(1, depth - y + 1);
7. return new Rect(x, y, w, h);
8. }
The GrowRect and ShrinkRect then loop through the range of positions speci ed by the randomly
generated rect area, growing or shrinking a single speci ed Tile at a time. Add the following
methods:
To grow a single tile, I must rst get a reference to the Tile from the tiles Dictionary. If the Tile
does not exist, I instantiate one from the prefab we provided earlier. Add the following methods:
1. Tile Create ()
2. {
3. GameObject instance = Instantiate(tileViewPrefab) as GameObject;
4. instance.transform.parent = transform;
5. return instance.GetComponent<Tile>();
6. }
7.
8. Tile GetOrCreate (Point p)
9. {
10. if (tiles.ContainsKey(p))
11. return tiles[p];
12.
13. Tile t = Create();
14. t.Load(p, 0);
15. tiles.Add(p, t);
16.
17. return t;
18. }
19.
20. void GrowSingle (Point p)
21. {
22. Tile t = GetOrCreate(p);
23. if (t.height < height)
24. t.Grow();
25. }
To shrink a single tile, I must check to see if the Tile exists (but not create one if it doesn’t).
Whenever shrinking existing tiles to a height less than or equal to zero, the tile is destroyed:
Occasionally you may wish to hand modify a level a single tile at a time. Add some methods
which allow you to Grow or Shrink a single tile based on the pos Point eld.
It will be nice to see which tile will be modi ed, so lets expose a method for updating the position
of our Tile Selection Indicator:
If the user wants to clear the board and start over, or before loading a pre-existing LevelData set,
we will need a way to quickly clear the board and reset everything. Add a method to Clear our
data – it will loop through all of the tiles we have created and destroy them and then clear our
Dictionary of references.
When the user manages to create a satisfactory level, we will need to provide a way to persist the
data. We will be creating our LevelData as a ScriptableObject using the LevelData script we
de ned earlier. It stores each of the tile’s position and height data in a list of Vector3. Add the
following methods:
The AssetDatabase reference requires using UnityEditor; and the Directory reference requires
Finally, should the user wish to restore a LevelData which they had previously saved, they will
need to link up the reference in the inspector, and then Load it with the following method:
Inspector Script
Now it’s time to add the custom inspector script which will allow us to create buttons we can use
during edit time. Inside the Editor folder create BoardCreatorInspector.cs and open it for editing.
Since we are creating an Editor script we need to make sure and include the correct namespace:
1. using UnityEditor;
We will also need to tell Unity what type of script our custom inspector is targeting. Add the
following line immediately before the class declaration:
1. [CustomEditor(typeof(BoardCreator))]
The selected object is available through a property called target. Let’s wrap that in a property that
is cast to the correct type:
The method we need to override in order to modify the appearance of the inspector is
OnInspectorGUI. To include the default implementation (allow Unity to show all of our serialized
elds) we use DrawDefaultInspector. I provide a button for each of the public methods de ned in
the BoardCreator script to trigger them. Then I check for changes and when a value has changed I
make sure the Marker is positioned correctly. Add the following:
Create a new scene called BoardCreator. Add an Empty GameObject by the same name, and
attach our script. Connect the relevant prefabs from the Project pane to the elds on our script in
the Hierarchy pane. Save the scene and project.
Note that you don’t need to press play in order to use this tool. You can simply start pressing
some buttons and see the results in the scene view.
Try clicking the Grow Area button a few times to get a basic mound of tiles. Then change the pos
coordinates of the selected tile by dragging left and right on the x or y elds and notice how the
indicator automatically updates and appears at the correct place. Now try Growing or Shrinking a
single tile. When you are happy, Save your board. You will see a Scriptable Object (your LevelData)
saved as Resources/Levels/Board Creator – it picked the name based on the name of the
GameObject the script was attached to. Keep that in mind so that you don’t accidentally overwrite
other les.
Clear your board and then assign the LevelData to the exposed eld in the inspector. Then reload
the board and see that you can continue editing from where you left off!
Summary
This was a HUGE lesson, and may have been better broken up into a few parts, but I wanted to
make sure you had something new to look at by the end. Over the course of this post we learned
about making custom classes or structs Serializable, operator overloading, guidelines on
overriding Equals and GetHashCode, and Lazy Loading. We created a custom Scriptable Object
which we used for persisting our level data. We also created a custom inspector script to provide
editor time functionality and access to trigger our public methods. I used the Directory class to
determine whether or not proper folder hierarchy had been set up, and AssetDatabase to create
folders when necessary. The end result of all our hard work is a nice little re-usable tool to
visually create the levels of our Tactics RPG game.
The Liquid Fire
Game Programming Blog
MENU
In this lesson we will be writing a component to manage user input. We will work with Unity’s
Input Manager so that your game should work across a variety of input devices (keyboard,
controller, etc). The component we write will be reusable so that any script requiring input can
receive and act on these events.
It is common to use an EventHandler when posting an event. Using this delegate pattern you
must pass along the sender of the event, and an EventArgs (or subclass) as well. When we post
input events, it is handy to pass along information such as what button was pressed, or what
direction is the user trying to apply. Most of the time, all I ever need to pass is a single eld of
data. Rather than creating a custom subclass of EventArgs for each occasion, we can create a
generic version. Create a new folder within the Scripts folder called EventArgs. Then create a
script within this folder called InfoEventArgs and use the following implementation:
1. using UnityEngine;
2. using System;
3. using System.Collections;
4. using System.Collections.Generic;
5.
6. public class InfoEventArgs<T> : EventArgs
7. {
8. public T info;
9.
10. public InfoEventArgs()
11. {
12. info = default(T);
13. }
14.
15. public InfoEventArgs (T info)
16. {
17. this.info = info;
18. }
19. }
This is a pretty simple class which can hold a single eld of any data type named info. I created
two constructors, an empty one which inits itself using the default keyword (this keyword
handles both reference and value types), and one which allows the user to specify the intial value.
Unity provides an Input Manager to help simplify the… well, the management of input – that was
obvious. From the menu bar choose Edit->Project Settings->Input. Look in the inspector and you
will be able to see the various mappings of input concepts to input mechanisms. Expand the Axes
(if it isnt already open) and you should see several entries such as: “Horizontal”, “Fire1”, and
“Jump”. There are actually several entries for most. One entry for “Horizontal” monitors keyboard
input from the arrow keys or the ‘a’ and ‘d’ keys. Another entry for “Horizontal” monitors keyboard
input for Joystick axis input. In your code, you can check if there is “Horizontal” input from any of
those sources with a single reference to that name.
Unity has done most of the heavy lifting for us, however, one of my own personal complaints
with this manager (and several of their other systems) is a lack of support for events. You must
check the status of Input every frame (through an Update method or Coroutine) in order to make
sure you dont miss anything. As you may have guessed, this is not terribly ef cient, and can be a
bit cumbersome to re-implement everywhere you need input. Therefore, I will do this process
only once, and then share the results via events with any other interested script.
Create another subfolder of Scripts called Controller. Inside this folder create our script,
InputController.cs and open it for editing.
We will be using the “Horizontal” and “Vertical” inputs for a variety of things such as moving the
tile selection cursor around the board (to select a move location or attack target) or to change the
selected item in a UI menu. As I mentioned before, we will need to check for input on every
frame, so let’s go ahead and take advantage of the Update method. Add the following code to your
script:
1. void Update ()
2. {
3. Debug.Log(Input.GetAxis("Horizontal"));
4. }
Save your script, attach it to any gameobject in a new scene, and press play. Every frame, a new
debug log will print to the console (Make sure Collapse is disabled so they appear in the correct
time-wise order). Watch what happens to the value when your press the left or right arrow keys,
or the ‘a’ and ‘d’ keys.
Pressing right or ‘d’ causes the output to raise toward positive one, and pressing left or ‘a’ causes
the output to lower toward negative one. If you aren’t pressing in either direction, the output will
ease back to zero. With this function, Unity has smoothed the input for us. If I were making a
game where a character could move freely through the world such as an FPS, then that easing
would help movement look a little more natural.
For our game, I don’t want any of the smoothing on directional input. Since we are snapping to
cells on a board or between menu options, etc. a very obvious on/off tap of a button will be better
for us. In this case there is another method we can try:
1. void Update ()
2. {
3. Debug.Log(Input.GetAxisRaw("Horizontal"));
4. }
Save the script and run the scene again. Now the keyboard presses result in jumps immediately
from zero to one or negative one depending on the direction you press.
You may have noticed that some games allow input both through pressing, and through holding.
For example, as soon as I press an arrow key, the tile might move on the board. If I keep holding
the arrow, after a short pause, the tile might continue moving at a semi-quick rate.
I want to add this “repeat” functionality to our script, but since I will need it for multiple axis, it
makes sense to track each one as a separate object so that we can reuse our code. I will add
another class inside this script – normally I dont like to do that, but this second class is private
and will only be used by our input controller, so it is an exception.
1. class Repeater
2. {
3. const float threshold = 0.5f;
4. const float rate = 0.25f;
5. float _next;
6. bool _hold;
7. string _axis;
8.
9. public Repeater (string axisName)
10. {
11. _axis = axisName;
12. }
13.
14. public int Update ()
15. {
16. int retValue = 0;
17. int value = Mathf.RoundToInt( Input.GetAxisRaw(_axis) );
18.
19. if (value != 0)
20. {
21. if (Time.time > _next)
22. {
23. retValue = value;
24. _next = Time.time + (_hold ? rate : threshold);
25. _hold = true;
26. }
27. }
28. else
29. {
30. _hold = false;
31. _next = 0;
32. }
33.
34. return retValue;
35. }
36. }
At the top of the Repeater class I de ned two const values. The threshold value determines the
amount of pause to wait between an intial press of the button, and the point at which the input
will begin repeating. The rate value determines the speed that the input will repeat.
Next, I added a few private elds. I use _next to mark a target point in time which must be passed
before new events will be registered – it defaults and resets to zero, so that the rst press is
always immediately registered. I use _hold to indicate whether or not the user has continued
pressing the same button since the last time an event red. Finally, I use _axis to store the axis
that will be monitored through Unity’s Input Manager. This value is assigned via the class
constructor.
After the constructor, I have an Update method. Note that this class is not a MonoBehaviour, so
the Update method wont be triggered by Unity – we will be calling it manually. The method
returns an int value, which will either be -1, 0, or 1. Values of zero indicate that either the user is
not pressing a button, or that we are waiting for a repeat event.
Inside the Update method, I declare a local variable called retValue which is the value which will
be returned from the function. It will only change from zero under special circumstances. Next
we get the value this object is tracking from the Unity’s Input Manager using GetAxisRaw as we
did earlier. I put the method inside of another method which rounds the result and casts it to an
int value type.
The if condition basically asks if there is user input or not. When the value eld is not zero the
user is providing input. Inside this body we do another if condition check which veri es that
suf cient time has passed to allow an input event. On the rst press of a button, Time.time will
always be greater than _next which will be zero at the time. Inside of the inner condition body,
we set the retValue to match the value reported by the Input Manager, and then set our time
target to the current time plus an additional amount of time to wait. This means that subsequent
calls into this method will not pass the inner condition check until some time in the future. Some
of you may not be familiar with the conditional operator (?:) used here – it is very similar to an if
condition where the condition to validate is to the left of the question mark, the value to the right
of the question mark is used when the condition is true, and the value after the colon is used
when the condition is false. Finally, I mark _hold as being true.
The rst (outer) if condition has an else clause – whenever the user is NOT providing input, this
will mark our _hold value as false and reset the time for future events to zero so that they can
immediately re with the next press of the button.
Now its time to put our Repeater class to good use. Add two elds inside the InputController class
as follows:
Whenever our Repeaters report input, I will want to share this as an event. I will make it static so
that other scripts merely need to know about this class and not its instances. We will implement
this EventHandler using generics so that we can specify the type of EventArgs – we will use our
InfoEventArgs and specify its type as a Point. Don’t forget that you will need to add a using
statement for the System namespace in order to use the EventHandler.
We will need to tie our repeaters into Unity’s Update loop, and actually re the event we just
declared at the appropriate time:
1. void Update ()
2. {
3. int x = _hor.Update();
4. int y = _ver.Update();
5. if (x != 0 || y != 0)
6. {
7. if (moveEvent != null)
8. moveEvent(this, new InfoEventArgs<Point>(new Point(x, y)));
9. }
10. }
Next I want to add events which watch for the various Fire button presses. I don’t need these to
repeat, because I wont consider the input as complete until it is actually released. I will use one
Fire button for con rmation, one for cancellation and will add a third, just in case I think of a
reason to have it.
The event we send for these will also use InfoEventArgs but instead of passing a Point struct for
direction, it will just pass an int representing which Fire button was pressed
Add a string array to your class to hold the buttons you wish to check for:
In our Update loop after we check for movement, let’s add the following to loop through each of
our Fire button checks:
Now that we’ve completed the Input Controller, let’s test it out. Create a temporary script
somewhere in your project and add it to an object in the scene. You will also need to make sure to
add the Input Controller to an object in the scene. I created a script called Demo in the root of the
Scripts folder.
I usually connect to events in OnEnable and disconnect from events in OnDisable. Remember
that cleanup is very important – particularly when using static events, because they maintain
strong references to your objects. This means they keep the objects from going out of scope and
being truly destroyed, and could for example trigger events on scripts whose GameObject’s are
destroyed.
1. void OnEnable ()
2. {
3. InputController.moveEvent += OnMoveEvent;
4. InputController.fireEvent += OnFireEvent;
5. }
6.
7. void OnDisable ()
8. {
9. InputController.moveEvent -= OnMoveEvent;
10. InputController.fireEvent -= OnFireEvent;
11. }
When you have added statements like this, but have not yet implemented the handler, you can
have MonoDevelop auto-implement them for you with the correct signatures. Right-click on the
OnMoveEvent and then choose Refactor->Create Method. A line will appear indicating where the
implementation will be inserted which you can move up or down with the arrow keys, and then
con rm the placement by hitting the return key. You should see something like the following:
Use the same trick to implement the OnFireEvent handler and use a Debug message to indicate
which button index was used.
Run the scene and trigger input and watch the console to verify that everything works.
Summary
In this lesson we reviewed making a custom, generic subclass of EventArgs to use with our
EventHandler based events. We discussed Unity’s Input Manager and how it can provide uni ed
input across multiple devices, and then we wrapped it up with a new Controler class to listen for
special input events speci c to our game. In the end I showed a simple implementation that
would listen to the events and take an action.
The Liquid Fire
Game Programming Blog
This week we are going to create a State Machine which, over time, will handle all of the states
which ultimately control our game’s logic. Initially I will create a state which is responsible for
initialization (creating the game board, etc.) and then we will add another state which allows us
to move the tile selection indicator around the board using events from our Input Controller. We
will also add a simple Camera Rig to make sure that the game camera is always looking at
something relevant.
State Machine
A state machine is a very useful idea, particularly in game programming. Put simply, it is a
controller which manages different states. To help illustrate why it is useful, I will show an
example of one way programmers have tried to work WITHOUT it… you may have come across a
design pattern (or implemented it yourself) where you de ne an enum with various state names
and then have logic which operates differently depending on a variable which is set to one of
those. For example:
1. enum State
2. {
3. Loading,
4. Playing,
5. GameOver
6. }
7. State _state;
8.
9. void CheckState ()
10. {
11. switch (_state)
12. {
13. case State.Loading:
14. // Loading Logic here
15. break;
16. case State.Playing:
17. // Playing Logic here
18. break;
19. case State.GameOver:
20. // GameOver Logic here
21. break;
22. }
23. }
When you only have a few states, and very simple logic per state, it can be tempting to use a
pattern like this. However, this pattern doesn’t grow very well. Every time you want to add
another state, you must add an entry to the State enum, and then verify that it will be checked in
any method which relies on the current state. In this example we only had one method, called
CheckState, but in a more realistic example, you might end up needing different logic per state in
multiple methods, such as after special game events or after user input, etc. In these cases, it can
be easy to miss adding the entry to one of your switch statements, or if…else conditions, etc and
may lead to some confusing bugs.
A better approach would be to change the state from a mere ag to an actual object which can
support its own variables and methods etc. Applied to our previous example you would see a few
immediate bene ts:
You don’t need to keep adding states to the State enum (could become a very long list), you
simply create a new state class in a new script whenever you want.
You don’t need a switch statement or complex if condition to execute the logic for a current
state. You simply maintain a reference to the current state and tell it to update as necessary.
You can avoid creating a monolithic class. All of your logic will be able to be composed only of
relevant bits of data and will be much easier to read and maintain.
State
The scripts we will be creating will be very re-usable, so let’s organize accordingly. Add a sub-
folder called State Machine to the Scripts/Common folder. Now create a new script State.cs and
use the following:
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class State : MonoBehaviour
5. {
6. public virtual void Enter ()
7. {
8. AddListeners();
9. }
10.
11. public virtual void Exit ()
12. {
13. RemoveListeners();
14. }
15.
16. protected virtual void OnDestroy ()
17. {
18. RemoveListeners();
19. }
20.
21. protected virtual void AddListeners ()
22. {
23.
24. }
25.
26. protected virtual void RemoveListeners ()
27. {
28.
29. }
30. }
It is a very simple script so I only have a few notes. I chose to make the script abstract because I
want to help illustrate that other programmers should be creating concrete subclasses in order to
use it. The basic use-case will be that a State Machine will determine what state, if any, is current
– and as it changes state it will call Exit and Enter on the states as they change from the current
state or change to the current state respectively.
I’ve use the Enter and Exit methods as an opportunity to Add and Remove event listeners, but just
to be safe I also Remove listeners in OnDestroy. I could have marked the AddListeners and
RemoveListeners methods abstract instead of virtual since they are currently implemented with
an empty body. However, had I implemented them with abstract, then all subclasses would be
required to implement them (whether they needed them or not).
As an example, we could (and will later) register for the InputController events. By only ‘listening’
to events while the state is ‘active’ we dont have to worry about con icting code. This helps to
protect us from scenarios like accidentally moving a tile position on the board at the sime time as
changing a menu selection.
I often go back and forth when trying to decide whether or not I want my states and state
machines to inherit from MonoBehaviour or not. Being able to create objects using constructors
(and without needing GameObjects, Transforms etc) certainly has some bene ts, but I also like
patterns which appear from the use of MonoBehaviours, such as the ability to easily see data in
the inspector (great for debugging), or have convenient methods like OnEnable and OnDisable.
Ultimately I decided to go with the MonoBehaviour because I think it is the easier of the two
patterns and provides the most bene ts.
State Machine
Create a new script called StateMachine.cs in the Scripts/Common/State Machine/ path. The
only purpose of this script is to maintain a reference to the current state, and handle switching
from one state to another.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class StateMachine : MonoBehaviour
5. {
6. public virtual State CurrentState
7. {
8. get { return _currentState; }
9. set { Transition (value); }
10. }
11. protected State _currentState;
12. protected bool _inTransition;
13.
14. public virtual T GetState<T> () where T : State
15. {
16. T target = GetComponent<T>();
17. if (target == null)
18. target = gameObject.AddComponent<T>();
19. return target;
20. }
21.
22. public virtual void ChangeState<T> () where T : State
23. {
24. CurrentState = GetState<T>();
25. }
26.
27. protected virtual void Transition (State value)
28. {
29. if (_currentState == value || _inTransition)
30. return;
31.
32. _inTransition = true;
33.
34. if (_currentState != null)
35. _currentState.Exit();
36.
37. _currentState = value;
38.
39. if (_currentState != null)
40. _currentState.Enter();
41.
42. _inTransition = false;
43. }
44. }
This script is also relatively simple. It tracks a single instance of State as its current state. This is
held in the protected eld called _currentState and is exposed through a property called
CurrentState. The property supports both getting and setting a value, although the setter has
some additional logic via the Transition method:
1. If you try to set the current state to the state it already is, then it just exits early (instead of
exiting and re-entering the same state).
2. You can not set the state during a transition (for example, you cant have one state cause
another state to become current in its Exit or Enter method). I chose to force this issue in case
I want to post events such as when a state changes. Otherwise, in a case where the transition
process doesn’t complete before switching to a new state, you would end up seeing multiple
posted events showing the same newest event as current (rather than getting one event per
state), which could cause unexpected bugs.
3. Mark the beginning of a transition…
4. If the previous state is not null, it is sent a message to exit
5. The backing eld is set to the value passed along in the setter
6. If the new state is not null, it is sent a message to enter
7. …mark the end of a transition
I also added a few Convenience methods. I wanted an easy way to tell the StateMachine to
change state, based on the type of the state in a generic method call. This way, I did not have to
hard code references to the instances of the state I want to swap to. I did this with the
ChangeState method using a constraint that the generic parameter must be a type of State. Inside
the method, it uses another generic method called GetState which passes along the generic type.
The GetState method attempts to get a state for you using Unity’s GetComponent call, and when
that fails, performs an AddComponent.
Battle Controller
Our battle controller will be a subclass of the StateMachine class. I will also be assigning
references which might be useful to the states. The states will have a reference to their owner by
which they will be able to easily reach any of the references they need. Create a new script
named BattleController.cs in the Scripts/Controller folder path and implement it with the
following:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class BattleController : StateMachine
5. {
6. public CameraRig cameraRig;
7. public Board board;
8. public LevelData levelData;
9. public Transform tileSelectionIndicator;
10. public Point pos;
11.
12. void Start ()
13. {
14. ChangeState<InitBattleState>();
15. }
16. }
Battle State
It can be very convenient to add reusable code to a common base class, which we will do for our
states. Things like registering/unregistering for events, hooking up references or properties, etc
are things which may be applied pretty consistently across them all. Add another folder inside of
the Scripts/Controller folder called Battle States (there will be many). Then create our abstract
base class state called BattleState.cs. All of the states used by BattleController will be subclasses
of this class.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class BattleState : State
5. {
6. protected BattleController owner;
7. public CameraRig cameraRig { get { return owner.cameraRig; }}
8. public Board board { get { return owner.board; }}
9. public LevelData levelData { get { return owner.levelData; }}
10. public Transform tileSelectionIndicator { get { return owner.tileSelectionIndicator;
}}
11. public Point pos { get { return owner.pos; } set { owner.pos = value; }}
12.
13. protected virtual void Awake ()
14. {
15. owner = GetComponent<BattleController>();
16. }
17.
18. protected override void AddListeners ()
19. {
20. InputController.moveEvent += OnMove;
21. InputController.fireEvent += OnFire;
22. }
23.
24. protected override void RemoveListeners ()
25. {
26. InputController.moveEvent -= OnMove;
27. InputController.fireEvent -= OnFire;
28. }
29.
30. protected virtual void OnMove (object sender, InfoEventArgs<Point> e)
31. {
32.
33. }
34.
35. protected virtual void OnFire (object sender, InfoEventArgs<int> e)
36. {
37.
38. }
39.
40. protected virtual void SelectTile (Point p)
41. {
42. if (pos == p || !board.tiles.ContainsKey(p))
43. return;
44.
45. pos = p;
46. tileSelectionIndicator.localPosition = board.tiles[p].center;
47. }
48. }
This simple script does a few important things. First, it has a reference to its owner, which is the
BattleController (a subclass of StateMachine which determines when this state will be ‘active’).
The owner reference is connected in the Awake call which is triggered by Unity.
The BattleController holds references to items in the scene which the states will need to perform
their own logic. These references are accessible through dot notation, but instead of having to say
owner.whatever all over the place, I decided to wrap its elds in properties. This way, I am not
adding duplicate pointers (if the BattleController reference changes or updates, the states will all
still be pointing to the correct entity) but because of the convenience property I still dont have to
use the longer form of reference. In this way it feels more like the state is a natural extension of
the state machine class.
I added the event handlers for the Input events, and like before, I decided to implement them as
virtual with an empty body. This way, concrete subclasses are not required to override it, unless
they actually want to modify the functionality.
Finally, I added a SelectTile method, which sets the selected tile of the Game Board, assuming the
board contains a tile at the speci ed location. It will update the eld as well as moving the
tileSelectionIndicator. I chose to add this method to the base class because I imagine that several
states will make use of setting the selected tile. As we see other functionality required by
multiple states, it can be added as well.
Board
Let’s add a script which can load our LevelData and create a game board level at run-time. Create
a new script named Board.cs in the Scripts/View Model Component folder path. Add the
following code:
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Board : MonoBehaviour
6. {
7. [SerializeField] GameObject tilePrefab;
8. public Dictionary<Point, Tile> tiles = new Dictionary<Point, Tile>();
9.
10. public void Load (LevelData data)
11. {
12. for (int i = 0; i < data.tiles.Count; ++i)
13. {
14. GameObject instance = Instantiate(tilePrefab) as GameObject;
15. Tile t = instance.GetComponent<Tile>();
16. t.Load(data.tiles[i]);
17. tiles.Add(t.pos, t);
18. }
19. }
20. }
There shouldn’t be anything unusual to you here. We use a reference of a tile prefab to instantiate
all of the board tiles. Then we save the board tiles into a dictionary based on the Point (location)
just like we did with the BoardGenerator. This will be useful in later lessons when we need to do
path nding, etc.
Camera Rig
Now let’s add a script which will cause the camera to follow our tile selection indicator when we
move it around the board.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class CameraRig : MonoBehaviour
5. {
6. public float speed = 3f;
7. public Transform follow;
8. Transform _transform;
9.
10. void Awake ()
11. {
12. _transform = transform;
13. }
14.
15. void Update ()
16. {
17. if (follow)
18. _transform.position = Vector3.Lerp(_transform.position, follow.position, speed *
Time.deltaTime);
19. }
20. }
We expose a speed parameter which modi es the rate at which the rig will move from where it is
toward where it wants to be. Note that this is really a smoothing rate, not a speed in units per
second, etc. It works by using a LERP in the update loop. Have you ever heard that if you try to
cross a distance by only moving forward half of the remaining distance at a time, that you will
never reach the destination? That exercise in logic helps illustrate what’s going on here. When
you interpolate from one point to another by a certain fraction of the space, the initial ground
covered will be noticeable. As you continue to approach the destination, the fractions of the
distance remaining are much smaller, and gives the move a nice ‘ease’ to its animation.
Let’s go ahead and create our rst concrete subclass of BattleState. This is the state which the
BattleController begins with. It will create things which need to be created, load things which
need to be loaded, etc, and then will trigger the next state when it is complete. Add a new le
inside of the Scripts/Controller/Battle States/ folder named InitBattleState.cs.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class InitBattleState : BattleState
5. {
6. public override void Enter ()
7. {
8. base.Enter ();
9. StartCoroutine(Init());
10. }
11.
12. IEnumerator Init ()
13. {
14. board.Load( levelData );
15. Point p = new Point((int)levelData.tiles[0].x, (int)levelData.tiles[0].z);
16. SelectTile(p);
17. yield return null;
18. owner.ChangeState<MoveTargetState>();
19. }
20. }
We have overriden the Enter method to allow us to add additional logic. Note that it is important
to call the base class’s Enter method or else you can miss important functionality. In this case,
forgetting would mean that the method for adding listeners would not be called. Since I am not
using any events it wouldn’t actually be that big of a deal. Still, you should practice good habits
and call base anyway – who knows, in the future I might add additional logic in there that
WOULD be important for this subclass to get, or I might decide to use events and wonder why the
listeners were ignored.
Note that I have prevented the ability to change state during a transition, and both the Enter and
Exit methods occur inside of a transition. Therefore in order to get the Init state to change to the
next state, I wait one frame by using a coroutine.
Now let’s create the script which will allow you to interact with the board through our Input
Controller. Add a new le inside of the Scripts/Controller/Battle States/ folder named
MoveTargetState.cs.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MoveTargetState : BattleState
5. {
6. protected override void OnMove (object sender, InfoEventArgs<Point> e)
7. {
8. SelectTile(e.info + pos);
9. }
10. }
This is only a partial implementation of what the state will do in its nal version, but I just
wanted to show a demo of moving the tile around via Input events.
Scene Setup
Create a new scene in the Scenes folder called Battle. Create an empty GameObject at the root of
the scene called Battle Controller and attach the similarly named script to it. This controller will
be the main organizing container for all of the game objects in the scene. You can also add an
instance of the InputController to this object.
Create a new empty GameObject named Camera Rig and attach the similarly named script to it.
Make it a child of the Battle Controller. I want our camera to use an Isometric projection style
angle. I nd it easy to achieve this using a hierarchy of objects. Create a child object for the
Camera Rig’s object named Heading. Reset the transform (zero out the position and rotation, and
use all one’s for scale), except the Rotation Y value should be 45. Create another empty
GameObject named Pitch as a child of Heading. Reset the transform, except the Rotation X value
should be 35.264. Now make the Main Camera scene object a child of the Pitch object. Reset the
Main Camera’s transform, except the Position Z should be -10. Set the Main Camera’s Projection to
Orthographic and the Size to 5. Here is a screen shot and comp of what your Camera Rig’s
hierarchy and transform settings will look like:
Create a new Empty GameObject named Board and attach the similarly named script. Make it a
child of the Battle Controller.
Create an instance of our Tile Selection Indicator prefab by dragging it from the project pane into
the hierachy pane. Add it as a child of the Battle Controller.
Now our scene holds everything we need for the demo. Make sure to hook up the references for
each of the elds of our BattleController. You will also need to set the CameraRig to follow the
instance of the tileSelectionIndicator and set the Board‘s reference to the Tile Prefab. You can use
any LevelData which you have saved from the Board Generator, or use one from my Repository if
you skipped that lesson. They are located in Resources/Levels and you can link directly to the
item in the Project pane.
Save your scene. Press Play and use the arrow keys to move the cursor around the board. Enjoy!
Summary
We had a lot to cover again this week. We started out by discussing how a StateMachine design
pattern can help to make your code more elegant. Then we implemented the pattern with
MonoBehaviours in Unity and created a few sample states to play with. We made the states
register for Input Events, and then created a GameBoard and CameraRig which could be
controlled by them. Finally we connected everything together in a scene to create a nice demo.
The Liquid Fire
Game Programming Blog
Path nding can be a relatively advanced task, mostly because the logic takes a moment to grasp.
We will be using a form of path nding to highlight all of the tiles that a unit can reach. When one
of those tiles is selected the unit will follow the best path to the target. To make it more
interesting, I will add three different movement types: a walking unit which must go around
enemy units and tiles with too large a jump delta, a ying unit, and a teleporting unit.
Directions
I want to add an enum which keeps track of cardinal directions. This will be used to indicate
what direction a character on the board is facing. I need to know because I will want to make the
characters turn in the direction of the path they are following. Later it can factor into damage
formulas based on an angle of attack.
Create a subfolder of the Scripts folder called Enums and then create a script there named
Directions.
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Directions
5. {
6. North,
7. East,
8. South,
9. West
10. }
Directions Extensions
Recently I have been experimenting with Extension Methods as a means to help keep my classes
decoupled from each other, and also as a way to keep related functionality better organized.
If you are unfamiliar with extension methods, they provide a way to extend the functionality of
another class without actually modifying the other class itself. However, you can add methods
which are accessed from instances of the class through dot-notation just as if it were a native
method of the class. You can only add extensions as a static method in a static class, and the
target to be extended is listed as the rst parameter entry in the method with a this keyword.
When you invoke the extension method, you treat it as if that rst paramter was not there,
because it is determined based on the object you call it from.
Create a subfolder of the Scripts folder called Extensions and then create a script there named
DirectionsExtensions.
1. using UnityEngine;
2. using System.Collections;
3.
4. public static class DirectionsExtensions
5. {
6. public static Directions GetDirection (this Tile t1, Tile t2)
7. {
8. if (t1.pos.y < t2.pos.y)
9. return Directions.North;
10. if (t1.pos.x < t2.pos.x)
11. return Directions.East;
12. if (t1.pos.y > t2.pos.y)
13. return Directions.South;
14. return Directions.West;
15. }
16.
17. public static Vector3 ToEuler (this Directions d)
18. {
19. return new Vector3(0, (int)d * 90, 0);
20. }
21. }
With these methods I can get a cardinal direction based off of the relationship between two tiles.
For example, if I had two references to tiles named t1 and t2 I could determine which direction
you would need to travel from the rst tile in order to reach the second tile like this:
1. Directions d = t1.GetDirection(t2);
In addition I can convert from a Directions enum to a Vector3. This will come in handy for
rotating characters on the board. The following code shows how to convert a Directions enum to
a Vector3.
1. Directions d = Directions.North;
2. Vector3 r = d.ToEuler();
Tile
The Tile component was created in an earlier lesson, but will need to be modi ed to show that it
can hold something. For now it will only ever hold one of our game characters, but later we might
want other content like traps, trees or some other non-traversable entity. Add the following eld
to the Tile script.
In addition, I will add a few elds which will be useful for our path nding and pathfollowing code.
I want the elds to be public, but I dont want them to appear in the inspector so I will use a special
tag to hide them. The rst eld prev stores the tile which was traversed to reach it. You can loop
through the tiles for as long as the prev eld is not null to determine the entire path taken to
reach any given location. The second eld distance stores the number of tiles which have been
crossed to reach this point.
Unit
Let’s add a component named Unit to our Hero and Monster prefabs. In the future this component
will also hold a reference to all of the data we might need such as stats, etc. but for now all we
need to track is where the Unit is placed on the board and what direction it is facing. The code for
this class is below:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Unit : MonoBehaviour
5. {
6. public Tile tile { get; protected set; }
7. public Directions dir;
8.
9. public void Place (Tile target)
10. {
11. // Make sure old tile location is not still pointing to this unit
12. if (tile != null && tile.content == gameObject)
13. tile.content = null;
14.
15. // Link unit and tile references
16. tile = target;
17.
18. if (target != null)
19. target.content = gameObject;
20. }
21.
22. public void Match ()
23. {
24. transform.localPosition = tile.center;
25. transform.localEulerAngles = dir.ToEuler();
26. }
27. }
Board
The core of our path nding code will exist in the Board script. If any of you have worked with
path nding before, you may wonder why I didn’t make use of a more ef cient algorithm like A*
(read as A star). With that sort of algorithm you must know where you are and where you want to
go. The ow of this game creates a scenario where you pick where you want to go based on the
knowledge of where you can reach. In other words, we wont yet know which of the tiles the unit
wishes to move to.
We will rst gather a list of all tiles within range of the moving unit while marking tiles in such a
way as to know the path taken to reach it. Afterwards, a tile from the list is chosen, and we will
already know the path and be able to simply return it. Because the algorithm is a bit complex I
created the following picture to help visually walk through a sample process.
Key:
Orange Tiles – tiles which are added to the queue for checking now.
Green Tiles – the tile which is currently being analyzed.
Red Tiles – tiles which are added to the queue for checking in the future.
Grey Tiles – tiles which have already been processed.
Plus Icon – indicates a tile which will be added to the queue for checking in the future
No Icon – indicates a tile which is skipped since it has already been visited.
Steps:
1. We prime our search by deciding which tile to start from. The tile is added to a queue of tiles
for checking now. This rst tile has a distance of 0, and no prev tile which indicates that it is
the beginning of the path.
2. In a loop, we dequeue a tile from the queue of tiles to check this round. Then we grab a
reference to the tiles in each cardinal direction from the current tile and add them to a queue
for checking in the future. Any tiles which are added have their distance set to 1 greater than
the current tile’s distance. The current tile is also set as their prev tile reference.
3. The current tile is marked as analyzed. There are no more tiles in the queue for checking now,
so we will swap queues.
4. This is a repeat of step 2, except that now you can observe the skipping of already visited
tiles.
5. There are additional tiles to check in the current queue so we move on and the loop
continues.
6. Same basic loop.
7. Same basic loop.
8. This is a repeat of step 3.
9. This is a repeat of step 2 and 4.
Now let’s implement the algorithm in code. Add the following method stub:
This method will return a list of Tiles, starting from a speci c tile, which meet a certain criteria.
The criteria to be met is passed along as a delegate via the generic Func delegate which takes as
parameters the segment of a potential path (where you would move from and where you would
move to) and returns a bool indicating whether or not to allow the movement. At a minimum, the
criteria will make sure that the distance to the tile is within the movement range of the unit.
Other factors could include checking for blocking entities on the tile. Note that we will need to
add a using System; statement at the top of our script in order to use the Func delegate type.
The very rst thing we need to do when starting a new search will be to clear the results of any
previous search. To take care of this we will use a separate method named ClearSearch. This
method loops through all of the board’s tiles and resets their relevant path nding elds. The prev
tile reference is set to null indicating that it is not currently part of a path. The distance is set to
the largest value that an int value type can hold, which means that reaching the tile from any
other tile will be able to succeed as a more ef cient path.
1. void ClearSearch ()
2. {
3. foreach (Tile t in tiles.Values)
4. {
5. t.prev = null;
6. t.distance = int.MaxValue;
7. }
8. }
As a side note, fast enumeration causes extra allocations which can lead to memory issues in
your game. Because I am only using it sporadically, and because it allows a very easy and
readable way to loop over the tiles in my dictionary, I decided it would be okay for use. In most
other cases you should use a regular for loop.
I left a comment indicating where to continue adding code in the Search method. Add a
statement which calls our ClearSearch method there. We will also declare two Tile queues: one
for tiles which need to be checked now, and one for tiles we will check in the future.
1. ClearSearch();
2. Queue<Tile> checkNext = new Queue<Tile>();
3. Queue<Tile> checkNow = new Queue<Tile>();
Now let’s prime the system, by setting correct values on the start tile and adding it to the list of
tiles which need to be checked.
1. start.distance = 0;
2. checkNow.Enqueue(start);
Next add the main loop, which dequeues a tile and will perform logic on it. The loop continues for
as long as the checkNow queue contains tiles. It wont loop forever, because within every loop we
dequeue one of the tiles it contains.
Now, back inside the while loop where I left the comment, we will add a nested for loop which
gets the tiles in each direction from the currently selected tile.
Within the inner for loop, we will rst verify that we actually got a tile reference (remember that
we are on non-square boards, which could have holes, etc.) and if so, we will compare the
distance which it has marked. We only need to consider tiles for which we can offer a more
ef cient (shorter distance) path – remember that if the tile has not been touched it should still
hold its reset value of int.MaxValue.
Now we will perform a criteria check on the tile. A search of the board can only continue from
that tile if it passes the check, otherwise getting to tiles beyond it would require going around it. If
it passes the check, it will be added to the list of tiles to check in the future, as well as be added to
the list of tiles returned by our search. We will also make sure to keep track of the path data
inlcuding which tile we came from and how far we have traveled.
1. if (addTile(t, next))
2. {
3. next.distance = t.distance + 1;
4. next.prev = t;
5. checkNext.Enqueue(next);
6. retValue.Add(next);
7. }
Just after the inner for loop, but still inside of the while loop, add another check to see if we have
now cleared our queue. If so we will swap the references of the queues so that checkNow points
to the tiles we had queued for checking in the future, and checkNext points to the empty queue
we have just cleared out.
1. if (checkNow.Count == 0)
2. SwapReference(ref checkNow, ref checkNext);
When implementing the game states, we will want to be able to highlight the tiles a player can
move to. Let’s add a few elds to represent a highlight and default color:
Following are the methods which can loop through the tiles and highlight them or un-highlight
them.
Movement
Create a subfolder of Scripts/View Model Component named Movement and add our base class
for the movement types which is also called Movement. The concrete subclasses of this
component will be added to our board units. In the future it could be added directly to the prefab –
a model with wings would obviously get the ying version. For now, we are reusing a sphere as
placeholder art so the components will be added dynamically to make sure we get a sample of
each.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public abstract class Movement : MonoBehaviour
6. {
7.
8. }
The movement component will know how far the unit is allowed to move (as the number of board
tiles) as well as how high the unit can jump. In the future this data may be set from stats which
the Unit component would have a reference to, or it may just be a property which conveniently
wraps the value directly. For the sake of this weeks lesson, the values will be assigned test values.
We will also need a reference to the Unit component so that we can update its placement. Finally
we will need a reference to the jumper Transform in order to assist with animation during the
traversal of a path. The elds are implemented as follows:
We will have a public method which can determine what tiles are reachable on a given board. The
method will provide its own criteria for the board’s search method and return that result.
The ExpandSearch method will be overridable but also offer a base implementation which
compares the distance traveled against the range of the character.
The Filter method will also be overridable while offering a base implementation. It loops through
the list of tiles returned by a board search, and removes any which hold blocking content. This
step is required because some search criteria may have allowed the unit to travel over tiles which
had content (like an ally) but should not be allowed to stop there. In the future this check may be
more complex, for example, we may want to allow a unit to occupy the same location as a trap,
but for now, any content will be considered an obstacle.
We will also have a public method which tells the component to handle the animation of actually
traversing a path. It will be left as abstract in the base class requiring all concrete subclasses to
provide their own implementation.
Walk Movement
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class WalkMovement : Movement
6. {
7. // Add code here
8. }
We will start by overriding the ExpandSearch method. We will want to retain the base
implementation (a distance check) while adding a check for the distance between tile heights –
to make sure the character can jump that far. We will also add a check to see if the tile has any
sort of blocking content. In the future, when we have data showing the alliance of a unit, we can
allow a character to path through teammates, but for this demo we will assume that any content
at all is a blocker.
1. protected override bool ExpandSearch (Tile from, Tile to)
2. {
3. // Skip if the distance in height between the two tiles is more than the unit can
jump
4. if ((Mathf.Abs(from.height - to.height) > jumpHeight))
5. return false;
6.
7. // Skip if the tile is occupied by an enemy
8. if (to.content != null)
9. return false;
10.
11. return base.ExpandSearch(from, to);
12. }
Next we must provide the implementation of the animation along a path. I created it sequentially
using nested coroutines.
Fly Movement
Create another concrete subclass of Movement called FlyMovement. We wont need to override
the ExpandSearch method this time, because any obstacles can be own over. All we need to
implement is the animation for traversing the path:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class FlyMovement : Movement
5. {
6. public override IEnumerator Traverse (Tile tile)
7. {
8. // Store the distance between the start tile and target tile
9. float dist = Mathf.Sqrt(Mathf.Pow(tile.pos.x - unit.tile.pos.x, 2) +
Mathf.Pow(tile.pos.y - unit.tile.pos.y, 2));
10. unit.Place(tile);
11.
12. // Fly high enough not to clip through any ground tiles
13. float y = Tile.stepHeight * 10;
14. float duration = (y - jumper.position.y) * 0.5f;
15. Tweener tweener = jumper.MoveToLocal(new Vector3(0, y, 0), duration,
EasingEquations.EaseInOutQuad);
16. while (tweener != null)
17. yield return null;
18.
19. // Turn to face the general direction
20. Directions dir;
21. Vector3 toTile = (tile.center - transform.position);
22. if (Mathf.Abs(toTile.x) > Mathf.Abs(toTile.z))
23. dir = toTile.x > 0 ? Directions.East : Directions.West;
24. else
25. dir = toTile.z > 0 ? Directions.North : Directions.South;
26. yield return StartCoroutine(Turn(dir));
27.
28. // Move to the correct position
29. duration = dist * 0.5f;
30. tweener = transform.MoveTo(tile.center, duration, EasingEquations.EaseInOutQuad);
31. while (tweener != null)
32. yield return null;
33.
34. // Land
35. duration = (y - tile.center.y) * 0.5f;
36. tweener = jumper.MoveToLocal(Vector3.zero, 0.5f, EasingEquations.EaseInOutQuad);
37. while (tweener != null)
38. yield return null;
39. }
40. }
Teleport Movement
Create our third and nal concrete subclass of Movement called TeleportMovement. As with
ying, the only thing we need to implement is the animation for path traversal.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class TeleportMovement : Movement
5. {
6. public override IEnumerator Traverse (Tile tile)
7. {
8. unit.Place(tile);
9.
10. Tweener spin = jumper.RotateToLocal(new Vector3(0, 360, 0), 0.5f,
EasingEquations.EaseInOutQuad);
11. spin.easingControl.loopCount = 1;
12. spin.easingControl.loopType = EasingControl.LoopType.PingPong;
13.
14. Tweener shrink = transform.ScaleTo(Vector3.zero, 0.5f,
EasingEquations.EaseInBack);
15.
16. while (shrink != null)
17. yield return null;
18.
19. transform.position = tile.center;
20.
21. Tweener grow = transform.ScaleTo(Vector3.one, 0.5f, EasingEquations.EaseOutBack);
22. while (grow != null)
23. yield return null;
24. }
25. }
Battle Controller
Let’s give the BattleController script a referenc to our Hero prefab so that we can instantiate a few
heroes on the board. After modifying the script make sure to open the scene and actually connect
the reference. This is prototype code which I expect not to keep, so I wont bother wrapping it in
the BaseBattleState. A more complete implementation for spawning our characters would load
the correct models through a Resources.Load call. Let’s also add a eld to keep track of the
currently selected unit and a property to wrap the currently selected tile.
Lets have the InitBattleState instantiate three copies of our Hero prefab and place them on the
board. This code is only placeholder and will be removed later. But for now, it allows us to test
each of the three movement types. Invoke the following method just before the Init method yields
for a frame, and then have it change state to SelectUnitState.
1. IEnumerator Init ()
2. {
3. board.Load( levelData );
4. Point p = new Point((int)levelData.tiles[0].x, (int)levelData.tiles[0].z);
5. SelectTile(p);
6. SpawnTestUnits(); // This is new
7. yield return null;
8. owner.ChangeState<SelectUnitState>(); // This is changed
9. }
10.
11. void SpawnTestUnits ()
12. {
13. System.Type[] components = new System.Type[]{ typeof(WalkMovement),
typeof(FlyMovement), typeof(TeleportMovement) };
14. for (int i = 0; i < 3; ++i)
15. {
16. GameObject instance = Instantiate(owner.heroPrefab) as GameObject;
17.
18. Point p = new Point((int)levelData.tiles[i].x, (int)levelData.tiles[i].z);
19.
20. Unit unit = instance.GetComponent<Unit>();
21. unit.Place(board.GetTile(p));
22. unit.Match();
23.
24. Movement m = instance.AddComponent(components[i]) as Movement;
25. m.range = 5;
26. m.jumpHeight = 1;
27. }
28. }
For the purposes of this demo, we are adding and inserting a state which wont be part of the nal
game ow. In the real game, Units will take turns based on their speed. For now, you will be able
to select any of our demo units that you wish by moving the cursor onto them and then selecting
them using the “Fire” button input. Add a new script named SelectUnitState to the Scripts/Battle
States folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class SelectUnitState : BattleState
5. {
6. protected override void OnMove (object sender, InfoEventArgs<Point> e)
7. {
8. SelectTile(e.info + pos);
9. }
10.
11. protected override void OnFire (object sender, InfoEventArgs<int> e)
12. {
13. GameObject content = owner.currentTile.content;
14. if (content != null)
15. {
16. owner.currentUnit = content.GetComponent<Unit>();
17. owner.ChangeState<MoveTargetState>();
18. }
19. }
20. }
Now let’s give the MoveTargetState a more complete implementation. This state becomes active
after the game has decided what Unit should take its turn. It begins by highlighting the tiles
within the Unit’s movement range and exits when a valid move location is chosen via the “Fire”
button input. The highlighted tiles are un-highlighted before the state exits.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class MoveTargetState : BattleState
6. {
7. List<Tile> tiles;
8.
9. public override void Enter ()
10. {
11. base.Enter ();
12. Movement mover = owner.currentUnit.GetComponent<Movement>();
13. tiles = mover.GetTilesInRange(board);
14. board.SelectTiles(tiles);
15. }
16.
17. public override void Exit ()
18. {
19. base.Exit ();
20. board.DeSelectTiles(tiles);
21. tiles = null;
22. }
23.
24. protected override void OnMove (object sender, InfoEventArgs<Point> e)
25. {
26. SelectTile(e.info + pos);
27. }
28.
29. protected override void OnFire (object sender, InfoEventArgs<int> e)
30. {
31. if (tiles.Contains(owner.currentTile))
32. owner.ChangeState<MoveSequenceState>();
33. }
34. }
Let’s add one more state to complete this demo. In this state, we already know the current unit,
and where the unit should move. This state triggers the path traversal animation and waits for it
to complete before looping back to the unit selection state. By making this phase of the game its
own state, I dont have to worry about additional user input causing bugs while the character is
moving.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MoveSequenceState : BattleState
5. {
6. public override void Enter ()
7. {
8. base.Enter ();
9. StartCoroutine("Sequence");
10. }
11.
12. IEnumerator Sequence ()
13. {
14. Movement m = owner.currentUnit.GetComponent<Movement>();
15. yield return StartCoroutine(m.Traverse(owner.currentTile));
16. owner.ChangeState<SelectUnitState>();
17. }
18. }
Summary
There was a lot to cover in this lesson, with lots of little setup material. Extension methods were
introduced for the rst time. The path nding algorithm was covered in detail, and made good use
of passing along a delegate as a parameter for search validation. Finally we added and modi ed
several game states to allow a simple demo game loop of selecting and moving units around the
board so you can watch all of the different traversal modes.
The Liquid Fire
Game Programming Blog
The user interface (UI) is one of those areas you always end up spending a lot of time
implementing, and every game needs one in some form or fashion. I have built up a variety of
reusable libraries in the past, but with Unity’s new UI tools I nd myself starting over again. If
you’re like me, the anchor and pivot system provided by a RectTransform may have been a bit
confusing. I like working with it pretty well in the inspector, because I can modify the anchors
and pivot to any corner for easy placement. In code, it wasn’t quite as easy, so this lesson is
dedicated to the creation of a few reusable components which will, hopefully, make all our lives
easier for awhile.
Layout Anchor
The rst component I want to create will provide an easy way to move a RectTransform in
relationship to its parent RectTransform, as easily as I am able to via the inspector. The process
needs to be as easy as setting text alignment – in fact, I reuse a TextAnchor enum for this
purpose. I want to be able to snap it into place, or animate it, and maintain full control over timing
and easing curves, etc.
Create a new subfolder in Scripts/Common/ called UI and add a new script there called
LayoutAnchor. Because this script will operate on a RectTransform, we can use a tag to make it a
required component:
1. using UnityEngine;
2. using System.Collections;
3.
4. [RequireComponent(typeof(RectTransform))]
5. public class LayoutAnchor : MonoBehaviour
6. {
7.
8. }
Everything our new script will do is based on its own RectTransform and its parent
RectTransform, so we will want to add elds for both, and then assign them in the Awake method.
In the event that this object is not part of a view hierarchy, I throw an error message as a warning.
1. RectTransform myRT;
2. RectTransform parentRT;
3.
4. void Awake ()
5. {
6. myRT = transform as RectTransform;
7. parentRT = transform.parent as RectTransform;
8. if (parentRT == null)
9. Debug.LogError( "This component requires a RectTransform parent to work.",
gameObject );
10. }
When positioning our RectTransform, we will need to know the general offsets to use based on
the location of the anchor we want and the size of the RectTransform’s rect. Let’s make a method
which allows us to get this information from either of the RectTransforms we might want to pass
to it:
I am doing something here that some of the beginners may not understand – I am intentionally
not using a break statement between each of the case statements in my switch statement. This
allows cases to fall through each other until a break is reached. In other words, any of the Vertical
Center anchor settings will modify the return values ‘x’ value by half, and any of the Vertical
Right anchor settings will modify the return value by the full width of the RectTransforms rect.
The next method is a bit confusing, I apologize in advance, but I’m not sure I understand the pivot
and anchor system 100% myself – I just kept ddling with it until I got something which worked.
It’s purpose is to nd the value you would use to make a RectTransform appear in the correct
place based on the anchor points you specify. I wanted this to work regardless of the
RectTransform’s own pivot and anchor settings, which is why this method is more complex than
it could be. For example, if you could assume that both the parent and current RectTrasnform had
values of zero for all anchor and pivot settings then the calcuations would have been very easy to
determine. However such an assumption doesnt allow for things like anchors which stretch a UI
element based on the parent canvas, screen aspect ratio, etc. If any of my brilliant readers out
there knows a way to simplify this any further, please let me know:
Now that we can determine where to place our RectTransform, lets add a convenient method to
actually do it. The value might be useful by itself in some cases, but most of the time I imagine we
will just want it to take care of itself:
Let’s add one last option which will allow us to animate moving the RectTransform into position.
Note that this bit of code wont compile until you add some more animation extensions which we
will add next. Because the method returns a Tweener you can modify all aspects of the animation
such as how long it should take or what kind of animation curve to use. You could even register
for animation completion events etc.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class RectTransformAnchorPositionTweener : Vector3Tweener
5. {
6. RectTransform rt;
7.
8. protected override void Awake ()
9. {
10. base.Awake ();
11. rt = transform as RectTransform;
12. }
13.
14. protected override void OnUpdate (object sender, System.EventArgs e)
15. {
16. base.OnUpdate (sender, e);
17. rt.anchoredPosition = currentValue;
18. }
19. }
Animation Extensions
When using my animation libraries, I prefer having animation extensions that allow whatever it
is being animtated, to animate itself. It’s not necessary, but I nd it makes my code more readable:
1. using UnityEngine;
2. using System;
3. using System.Collections;
4.
5. public static class RectTransformAnimationExtensions
6. {
7. public static Tweener AnchorTo (this RectTransform t, Vector3 position)
8. {
9. return AnchorTo (t, position, Tweener.DefaultDuration);
10. }
11.
12. public static Tweener AnchorTo (this RectTransform t, Vector3 position, float
duration)
13. {
14. return AnchorTo (t, position, duration, Tweener.DefaultEquation);
15. }
16.
17. public static Tweener AnchorTo (this RectTransform t, Vector3 position, float
duration, Func<float, float, float, float> equation)
18. {
19. RectTransformAnchorPositionTweener tweener =
t.gameObject.AddComponent<RectTransformAnchorPositionTweener> ();
20. tweener.startValue = t.anchoredPosition;
21. tweener.endValue = position;
22. tweener.easingControl.duration = duration;
23. tweener.easingControl.equation = equation;
24. tweener.easingControl.Play ();
25. return tweener;
26. }
27. }
Test Drive
Let’s take our new components out for a test drive! Create a new scene (from the menu bar
choose File->New Scene. Add a Panel (from the menu bar choose GameObject->UI->Panel). Select
the Panel and add the Layout Anchor component.
Create a temporary script (place it wherever you like – perhaps a Temp folder) named
AnchorTests. This script will loop through all of the combinations of anchors and snap or move
(animated) the panel accordingly and allow you a chance to watch and make sure that
everything works as expected. Attach the AnchorTests script to the same Panel which had the
LayoutAnchor.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AnchorTests : MonoBehaviour
5. {
6. [SerializeField] bool animated;
7. [SerializeField] float delay = 0.5f;
8.
9. IEnumerator Start ()
10. {
11. LayoutAnchor anchor = GetComponent<LayoutAnchor>();
12. while (true)
13. {
14. for (int i = 0; i < 9; ++i)
15. {
16. for (int j = 0; j < 9; ++j)
17. {
18. TextAnchor a1 = (TextAnchor)i;
19. TextAnchor a2 = (TextAnchor)j;
20. Debug.Log(string.Format("A1:{0} A2:{1}", a1, a2));
21. if (animated)
22. {
23. Tweener t = anchor.MoveToAnchorPosition( a1, a2, Vector2.zero );
24. while (t != null)
25. yield return null;
26. }
27. else
28. {
29. anchor.SnapToAnchorPosition(a1, a2, Vector2.zero);
30. }
31. yield return new WaitForSeconds(delay);
32. }
33. }
34. }
35. }
36. }
Reset the Panel’s RectTransform values, a good 100×100 panel is perfect for watching the snapped
positions (but note that you can experiment with different values for the Anchors and Pivot and it
should still work as expected). I also chose to give our Panel a solid red color in its Image
component so it was easier to see.
Play the scene. I like to have a Scene view and Game view up at the same time, so I can also
watch the positions which move the panel outside of the camera area. Keep the scene around
because we will add another test in a bit.
Panel
Let’s add another very reusable component inside the Scripts/Common/UI folder called Panel. It
wont hurt my feelings if you prefer a different name to avoid confusion with a Unity Panel (which
is really just a GameObject with a RectTransform – no Panel component here). I think Panel is a
tting name though and it isn’t used (via a class name) so I think it’s fair game.
The purpose of this script will be to de ne target positions and then work with our LayoutAnchor
to snap or move to them when necessary. For example, you may want to de ne which anchors
and offsets to use when the panel is supposed to be On-Screen and different anchors and offsets
when it is Off-Screen. If you were doing some sort of Navigation View Stack you might have a few
different Off-Screen positions (one for when it is not part of the stack, and one for when it is). To
maintain exibility I didn’t force any position naming system, and allowed the user to de ne how
many there will be, and what names to use. The script will also be able to tell you what position it
is currently in, and whether or not a transition is active.
Because the Panel component requires the LayoutAnchor component to work, let’s add the
RequireComponent tag:
1. using UnityEngine;
2. using System;
3. using System.Collections;
4. using System.Collections.Generic;
5.
6. [RequireComponent(typeof(LayoutAnchor))]
7. public class Panel : MonoBehaviour
8. {
9.
10. }
The target positions this panel can hold will be implemented as a Serializable class. I am de ning
this class within the Panel class because that is the only context in which I intend for it to be
used. Also, I can imagine a name like Positions being generic enough that I might want to use it
again elsewhere. Note that because the class is serializable you will be able to see it appear in the
inspector.
1. [Serializable]
2. public class Position
3. {
4. public string name;
5. public TextAnchor myAnchor;
6. public TextAnchor parentAnchor;
7. public Vector2 offset;
8.
9. public Position (string name)
10. {
11. this.name = name;
12. }
13.
14. public Position (string name, TextAnchor myAnchor, TextAnchor parentAnchor) :
this(name)
15. {
16. this.myAnchor = myAnchor;
17. this.parentAnchor = parentAnchor;
18. }
19.
20. public Position (string name, TextAnchor myAnchor, TextAnchor parentAnchor, Vector2
offset) : this(name, myAnchor, parentAnchor)
21. {
22. this.offset = offset;
23. }
24. }
I want a way to precon gure a list of target positions via the inspector. However, I dont want the
list to be used anywhere else in code – it is only for the initial implementation. So let’s add this
property using the SerializeField tag. Ideally, I want to access Positions via a dictionary where a
string (name) points to an instance of Position. This too will be private, but will be used internally
in my script. If Unity knew how to show Dictionaries in the inspector I wouldn’t need the list, but
alas, that wish has not yet been granted (outside of paying for plugins). Add the following elds
(to our Panel class now – not inside the Position class), and then implement them in the Awake
method.
Now let’s add a few properties. I can imagine wanting to know the current position, whether or
not we are in a transition, and if so, to be able to access the Tweener. I also want a way to get
access to a Position instance using a string name. I will do this using an indexer since I kept the
Dicitionary private.
It’s possible that a user may wish to add or remove Positions dynamically so let’s add a few
methods to handle this:
The real purpose of this script though, is to actually move the Panel to one of its speci ed
positions. I will support setting a position based both on a string name and a reference to a
position instance.
If no Position has been set by the time we reach the Start method, I’ll go ahead and assign the rst
Position in our list as the default position. I’ll also cause it to Snap into the correct position.
1. void Start ()
2. {
3. if (CurrentPosition == null && positionList.Count > 0)
4. SetPosition(positionList[0], false);
5. }
Test Drive 2
Now let’s modify our test scene to make use of the Panel component. Remove the AnchorTests
script from our Panel object and then add the Panel script. Using the inspector we will pre-
con gure the rst two positions. In most use-cases I would imagine that all UI will have its
positions pre-con gured and saved as part of a prefab.
Create and attach another test script named PanelTests to our Panel object.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class PanelTests : MonoBehaviour
5. {
6. Panel panel;
7. const string Show = "Show";
8. const string Hide = "Hide";
9. const string Center = "Center";
10.
11. void Start ()
12. {
13. panel = GetComponent<Panel>();
14. Panel.Position centerPos = new Panel.Position(Center, TextAnchor.MiddleCenter,
TextAnchor.MiddleCenter);
15. panel.AddPosition(centerPos);
16. }
17.
18. void OnGUI ()
19. {
20. if (GUI.Button(new Rect(10, 10, 100, 30), Show))
21. panel.SetPosition(Show, true);
22. if (GUI.Button(new Rect(10, 50, 100, 30), Hide))
23. panel.SetPosition(Hide, true);
24. if (GUI.Button(new Rect(10, 90, 100, 30), Center))
25. {
26. Tweener t = panel.SetPosition(Center, true);
27. t.easingControl.equation = EasingEquations.EaseInOutBack;
28. }
29. }
30. }
This script shows how to add a third position in code. It also makes a few simple legacy GUI
buttons so we can toggle between the positions of the panel and watch it move based on our
input. When moving to our dynamically added position, it also shows how to intercept the
Tweener and modify it. Play the scene and try moving the panel around!
Quick Note – the use of OnGUI here is only for the sake of a simpler and quicker demo. The
equivalent setup using the new UI would have required more steps including extra scene object
setup. The use of OnGUI is by no means required as part of the implementation of the
components we created. Furthermore, I don’t recommend the use of OnGUI in a live project, and
in my tactics project I won’t be using OnGUI anywhere.
Summary
In this lesson we created a few very reusable components to help make the positioning and
animation of UI elements much easier. We created individual tests which showed off the
component’s functionality, and provided some ideas about how you could use them in code later
on. If you watched the Series Intro video, then you will know I will need to show a variety of UI
such as stat panels for the attacker and target as well as a menu which allows you to choose an
action or skill. I’ve also created a UI which shows dialog to the user for conversations before and
after a battle. All of these UI pieces will be able to make great use of our new components,
allowing us to save quite a bit of effort in code.
The Liquid Fire
Game Programming Blog
This week we will implement the UI components from the previous lesson in a Conversation UI
element which would appear as part of a cut-scene before and/or after battles. The panels will
hold a little bit of text along with a sprite showing who is speaking. A bouncing arrow will
indicate when there is more to read and using the input “Fire” event will cause the conversation
to move on to the next message or the next character who will speak, etc. These panels can
appear in any corner of the screen (which could indicate the direction of a speaking character
relative to the player), and will animate in and out of the screen as needed.
Speaker Data
One of the rst things we will need is a data model. Create a new class named SpeakerData in the
Scripts/Model folder. I didn’t specify a class to inherit from, and I marked the class Serializable so
that we can see and con gure it using the Editor’s inspector. Model classes can often be quite
simple – like this one. I have merely declared a few elds:
A list of string called messages will contain all of the “pages” of text that any one character
will speak.
A reference to a sprite will be used to show who is speaking.
A TextAnchor enum is used to determine which corner of the screen the panel will display in.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. [System.Serializable]
6. public class SpeakerData
7. {
8. public List<string> messages;
9. public Sprite speaker;
10. public TextAnchor anchor;
11. }
Conversation Data
Since this lesson isn’t about creating a monologue, let’s create another script to hold a sequence
(list) of Speaker Data instances. This way, we can have multiple different people speaking and
interacting with each other. Create a script named ConversationData and place it in the
Scripts/Model folder.
This time our class will inherit from something. Even though you can see Serializeable classes in
the editor, you can only see them when they are attached to an asset. It just so happens that you
can make project assets out of ScriptableObject so we will specify it here.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ConversationData : ScriptableObject
6. {
7. public List<SpeakerData> list;
8. }
Prototype Approach
Initially, you are unlikely to have a full grasp of all of the features your project will need. You may
want to just create a few simple assets to test with and see if you like your architecture decisions.
In this sort of scenario, it would be really handy to just be able to create an instance of a
scriptable object using a menu action, just like when you want to create any other kind of
GameObject.
The Unity Community has provided a handy little script for that called ScriptableObjectUtility,
which you should download and add to the project. Make sure to stick it in an Editor folder,
because it uses the UnityEditor namespace.
This script can’t do anything by itself- we must add another script which actually triggers it and
tells it what kind of asset we want to make. So, create another script called AssetCreator and
place it alongside the utility script in the Editor folder.
1. using UnityEngine;
2. using UnityEditor;
3.
4. public class YourClassAsset
5. {
6. [MenuItem("Assets/Create/Conversation Data")]
7. public static void CreateConversationData ()
8. {
9. ScriptableObjectUtility.CreateAsset<ConversationData> ();
10. }
11. }
Now that we have these two scripts, we can create a ConversationData asset by using the le
menu (Assets->Create->Conversation Data). Go ahead and create an instance of our Conversation
Data and place it in the Resources/Conversations folder (Create the Conversations subfolder).
Name the asset IntroScene and give it some sort of implementation:
Note that in this example, I duplicated the Avatar sprite which already existed in the project and
then recolored it and saved it as EvilAvatar. This way I could test out the dynamic speaker image
feature of my panel.
Production Approach
In a fully realized project, I would actually want to be creating my conversation data in some
external form – this could be anything you want, but ideally it should allow you to easily see and
edit all of your text, have a spell-checker, etc. The inspector in Unity doesn’t have these sorts of
features. Imagine how tedious it could be to try to track down a spelling mistake if you had
hundreds of Conversation assets, each having multiple speakers with multiple pages. Or suppose
you changed a commonly used character or location name and needed to propogate the change
across the entire game’s script. If all of the text existed in an external source, like a .csv for
example, then it would be as easy as running a Find and Replace command.
If you are interested in the full production-level approach, I have written a separate post called
Bestiary Management and Scriptable Objects which should be able to help you get a good head
start on the process.
Conversation Panel
Now that we have a data model to store our conversation, we need a view to display it to a user.
Add a new script named ConversationPanel to the Scripts/View Model Component folder. The
implementation follows:
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System.Collections;
4.
5. public class ConversationPanel : MonoBehaviour
6. {
7. public Text message;
8. public Image speaker;
9. public GameObject arrow;
10. public Panel panel;
11.
12. void Start ()
13. {
14. Vector3 pos = arrow.transform.localPosition;
15. arrow.transform.localPosition = new Vector3(pos.x, pos.y + 5, pos.z);
16. Tweener t = arrow.transform.MoveToLocal(new Vector3(pos.x, pos.y - 5, pos.z),
0.5f, EasingEquations.EaseInQuad);
17. t.easingControl.loopType = EasingControl.LoopType.PingPong;
18. t.easingControl.loopCount = -1;
19. }
20.
21. public IEnumerator Display (SpeakerData sd)
22. {
23. speaker.sprite = sd.speaker;
24. speaker.SetNativeSize();
25.
26. for (int i = 0; i < sd.messages.Count; ++i)
27. {
28. message.text = sd.messages[i];
29. arrow.SetActive( i + 1 < sd.messages.Count );
30. yield return null;
31. }
32. }
33. }
This is also a pretty simple script. We need a reference to the Text component so we can update it
with dynamic messages from our conversation. We need a reference to the speaker so we can
show who is speaking. We will need a reference to the arrow so we can turn it on or off based on
whether or not more “pages” of text exist in the current speaker’s dialog. Finally we need a
reference to the Panel component so we can make the view tween into or out of the screen.
In the Start method we get the current local position of the arrow. Using that as a base point, we
move it up by a xed amount and then tell it to tween down just as far – this way the animation
is centered around the original point. After completing the tween the animation will play in
reverse so that you see it animate back up to where it started (due to the PingPong setting). I set
the tween to loop in nitely by setting the loopCount to -1. This little bit of animation should help
indicate to the user that there is more to read.
I created an IEnumerator method called Display. This will not be the target of a MonoBehaviour’s
StartCoroutine. Instead, we will manually move through the method based on “Fire” input events.
This process may be new for many of you, but it is fairly simple to use, and can be a very powerful
feature.
Conversation Controller
The real meat of this lesson is found in the Conversation Controller. Its job is to handle the
process of a conversation including making sure that an appropriate panel is used and positioned
correctly, tweening the panel into view, showing the speaker and the speaker’s messages one at a
time based on user input, tweening the panel out when the speaker is nished speaking, and then
tweening in another panel (if necessary) for a new speaker, etc. The complex part of this is
deciding how to maintain state between all of the different speakers and pages of a speaker’s
dialogue.
1. using UnityEngine;
2. using System;
3. using System.Collections;
4.
5. public class ConversationController : MonoBehaviour
6. {
7.
8. }
I usually begin something complex like this by de ning my elds and properties. I like to know
what is going to be needed rst and then implement the details later. First, I have decided to
create two separate Conversation Panels – one for display on the left side of the screen, and one
for display on the right side of the screen. It would have been possible to use a single panel, and
simply modify assets as necessary to show it on the different sides, but this is simpler.
I will also want the ability to turn on and off the canvas based on whether or not a conversation is
actually taking place. Disabling the canvas when it is not used should allow the rendering speed
to run at peak performance.
1. Canvas canvas;
I will maintain a reference to an IEnumerator which steps through all of the speakers and their
messages in a conversation. This will make it easy to maintain state without adding a bunch of
other properties.
1. IEnumerator conversation;
Finally I will maintain a reference to a Tweener, which is used to animate the current panel into
or out of the screen. I maintain this reference, because while the transitions are active, I dont
want the user to be able to advance the conversations position.
1. Tweener transition;
If you remember from the previous lesson, you are able to specify panel positions using string
names. These names will be set in the inspector, but in code, it generally isn’t a good idea to use
strings (due to typo errors etc). One common practice is to de ne constants at the top of your
script so that you know you will be using the same text anywhere the constant itself is used. This
practice can help to alleviate potentially confusing bugs.
In the start method, I will connect some references, set the default off screen position for both
panels, and then disable the canvas (until it is needed).
1. void Start ()
2. {
3. canvas = GetComponentInChildren<Canvas>();
4. if (leftPanel.panel.CurrentPosition == null)
5. leftPanel.panel.SetPosition(HideBottom, false);
6. if (rightPanel.panel.CurrentPosition == null)
7. rightPanel.panel.SetPosition(HideBottom, false);
8. canvas.gameObject.SetActive(false);
9. }
The public interface for this script will have two main options. Initially you will tell it to Show and
will pass along an instance of ConversationData as a parameter. This method will set everything
up and bring out the rst panel and display the initial message of the rst speaker. From then on,
new messages and speakers must be manually triggered using a Next method call. This process
will continue until the nal message has been dismissed. When the panel has completed its
animation offscreen an event will be posted.
I could have allowed the conversation panel itself to receive input and manage the entire
sequence itself, but in the end I decided it would make the most sense to keep all of the input
routed through a single source – the game state, which displays the conversation in the rst
place.
Add the following event for when the conversation controller has nished:
The Sequence call is to a method with an IEnumerator return type. This is a bit of a lengthy
method, but essentially, it loops over all of the speakers in a conversation, and in a nested loop,
iterates over each of the speaker’s messages via another IEnumerator from the current panel.
For each speaker, I determine what entry and exit point to use for the panel based on the
SpeakerData’s anchor setting. I store those in a temporary local variable so that later I can just tell
the panel to show or hide.
Before I animate a panel on screen, I rst snap it offscreen (no animation). This way if it had
previously been in a different vertical location, you wont see it tween on the Y axis (it would enter
along a diagonal animation path).
Once I have animated a panel onto the screen, I create a pause in the sequence using a yield
statement. All of the state will be preserved at this spot until I tell it to continue moving via the
MoveNext function (called by the public Next method I exposed earlier).
As the user provides input, I will loop through the messages of the speaker in a while loop. When
the presenter has no further yield statements the while loop will terminate. At this point I will
move the current panel off screen. I use an anonymous delegate to automatically continue the
conversation once it completes. The yield statement immediately after it is the one it will skip.
Once all of the speakers have completed their dialogue, the canvas is disabled, and the event is
red.
In order to see our conversation as a part of the game, we will add another game state called
CutSceneState in the Scripts/Controller/Battle States folder. This script loads the sample
conversation asset we created earlier and displays it. When the animation is complete it moves
on to the next state of the game. Of course, a more complete implementation would not hard-
code the conversation to load. There would probably be additional data, perhaps in some sort of
Mission data model which contained all sorts of information, like what conversations to play,
what enemies to load, what rewards you get for winning, etc.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class CutSceneState : BattleState
6. {
7. ConversationController conversationController;
8. ConversationData data;
9.
10. protected override void Awake ()
11. {
12. base.Awake ();
13. conversationController = owner.GetComponentInChildren<ConversationController>();
14. data = Resources.Load<ConversationData>("Conversations/IntroScene");
15. }
16.
17. protected override void OnDestroy ()
18. {
19. base.OnDestroy ();
20. if (data)
21. Resources.UnloadAsset(data);
22. }
23.
24. public override void Enter ()
25. {
26. base.Enter ();
27. conversationController.Show(data);
28. }
29.
30. protected override void AddListeners ()
31. {
32. base.AddListeners ();
33. ConversationController.completeEvent += OnCompleteConversation;
34. }
35.
36. protected override void RemoveListeners ()
37. {
38. base.RemoveListeners ();
39. ConversationController.completeEvent -= OnCompleteConversation;
40. }
41.
42. protected override void OnFire (object sender, InfoEventArgs<int> e)
43. {
44. base.OnFire (sender, e);
45. conversationController.Next();
46. }
47.
48. void OnCompleteConversation (object sender, System.EventArgs e)
49. {
50. owner.ChangeState<SelectUnitState>();
51. }
52. }
To nish plugging in our Cut Scene, we need to make it the target of the InitBattleState. Change
the last line of the Init method to the following:
1. owner.ChangeState<CutSceneState>();
Scene Setup
Open the game’s Battle scene. Create an empty GameObject named Conversation Controller.
Make this object a child of Battle Controller. This object will be made into a special prefab to hold
and manage our conversation panels. An example of the nal result appears below:
This prefab has several gameobjects arranged in a particular hierarchy as shown in the image
below:
Add our ConversationController script to the root Conversation Controller object and make sure
you dont forget to set its dependencies when you nish creating them in a bit:
Create and add a Canvas object as indicated by the hierachy example above. Note that I
customized the Canvas Scaler component so that my UI elements can scale with different screen
sizes. Even with different sizes and aspect ratios, our setup will always work as expected.
Create an empty GameObject named Right Edge Conversation Panel. Add the Panel component
to this object and it will automatically make the Transform a RectTransform and add the Layout
Anchor for us. Make this object a child of the Canvas. Next, add the ConversationPanel script.
Make sure you dont forget to set its dependencies when you nish creating them. The image
below, as well as all of the next series of images show the parallel settings (one for the left panel
and one for the right panel). I would recommend creating just the right side rst, and then
duplicating it and then just make the few changes necessary.
Create an image object for the Background:
At this point you should be able to run the scene and test everything out. The conversation will
automatically appear and wait for your input to skip from message to message. When the
conversation completes, you should be able to select and move a unit on the board as you were
able to do before.
Summary
In this lesson we implemented the UI components from the previous lesson in a Conversation UI
element which appears as part of a cut-scene before and/or after battles. We made use of
Scriptable objects to store the conversation messages and avatar sprites. We used a free library to
easily create project assets out of our Scriptable Object which we could con gure in the inspector.
We also used an IEnumerator natively (not through a StartCoroutine call) to see how easy it can
be to drive the conversation updates through events.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
Ability Menu
JULY 13, 2015 ~ ADMIN
In this post we will continue to esh out the UI by adding the Ability Menu. This menu will allow
the user to determine what phase of a turn is active- such as moving, attacking, etc. as well as
what to do during a turn- such as what kind of skill to use during an attack. We will actually
implement the menu where possible (Move), and anything we haven’t gotten to yet will use
placeholder content (Attack, Magic etc). We will also see how to support canceling a move and be
able to restore an earlier state.
Object Pooling
You can pool just about anything – including menu elements. Our Ability Menu will have the
ability to grow or shrink based on the list of entries you tell it to display. Because of that, it makes
sense to be able to pool those entries. So before we get started, check out my post on Object
Pooling and import the nal implementation scripts into this project.
This component handles the display of a single entry in the menu. Each entry can have various
ags applied and based on the ags which are set, the entry will assume one of three visible
states: Default, Selected, or Locked. Each of the different states will have visible changes such as
different colors for the text, stroke, bullet, etc.
Add a script named AbilityMenuEntry to Scripts/View Model Component. Add a using statement
for UI because we will be editing some UI components.
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System.Collections;
4.
5. public class AbilityMenuEntry : MonoBehaviour
6. {
7. // Add Code Here
8. }
We will need a reference to an Image which we can set to use 1 of 3 different sprites (set
depending on state ags). We will also need a reference to a Text component to show what the
menu entry is for, and nally we will get a reference to the Outline component on the Text
component object so we can also change its color. We will manually link up all but the Outline,
which will be connected during Awake:
1. [System.Flags]
2. enum States
3. {
4. None = 0,
5. Selected = 1 << 0,
6. Locked = 1 << 1
7. }
Let’s add a Property and backing eld to hold the current State, and then add a few convenience
properties which allow me to get and set whether or not the ags are set – without other classes
ever needing to know about the States enum in the rst place.
1. public bool IsLocked
2. {
3. get { return (State & States.Locked) != States.None; }
4. set
5. {
6. if (value)
7. State |= States.Locked;
8. else
9. State &= ~States.Locked;
10. }
11. }
12.
13. public bool IsSelected
14. {
15. get { return (State & States.Selected) != States.None; }
16. set
17. {
18. if (value)
19. State |= States.Selected;
20. else
21. State &= ~States.Selected;
22. }
23. }
24.
25. States State
26. {
27. get { return state; }
28. set
29. {
30. if (state == value)
31. return;
32. state = value;
33.
34. if (IsLocked)
35. {
36. bullet.sprite = disabledSprite;
37. label.color = Color.gray;
38. outline.effectColor = new Color32(20, 36, 44, 255);
39. }
40. else if (IsSelected)
41. {
42. bullet.sprite = selectedSprite;
43. label.color = new Color32(249, 210, 118, 255);
44. outline.effectColor = new Color32(255, 160, 72, 255);
45. }
46. else
47. {
48. bullet.sprite = normalSprite;
49. label.color = Color.white;
50. outline.effectColor = new Color32(20, 36, 44, 255);
51. }
52. }
53. }
54. States state;
Finally, let’s add a public method to Reset the ags all at once:
Next, let’s add the script which will handle displaying the menu and the menu entries, manage
skipping locked entries, etc. It should be able to show and hide itself, and when not used, disable
its canvas for ef ciency. Add a new script named AbilityMenuPanelController to
Scripts/Controller. Like before, we will need to add a using Statement for UI. We will also use
Generics.
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System.Collections;
4. using System.Collections.Generic;
5.
6. public class AbilityMenuPanelController : MonoBehaviour
7. {
8. // Add Code Here
9. }
As we have done before, we will use const string declarations for safety sake when working with
the Panel and Pool Manager. Add the following consts:
We will need to expose a few elds such as: a reference to the menu entry prefab (for
instantiating our pooled objects), a reference to the Heading’s title label (to show context), a
reference to the Panel (for Toggling visibility and for a container for menu entries), a reference to
the canvas (so we can disable it when not used), a list holding all of the active menu entries, and a
value representing the currently selected index in the menu.
During MonoBehaviour’s Awake, we will con gure the Pool Manager so that it can generate the
menu entries for us and have them ready.
1. void Awake ()
2. {
3. GameObjectPoolController.AddEntry(EntryPoolKey, entryPrefab, MenuCount,
int.MaxValue);
4. }
To get menu entries and return them to the pool manager, I added some convenient methods:
1. AbilityMenuEntry Dequeue ()
2. {
3. Poolable p = GameObjectPoolController.Dequeue(EntryPoolKey);
4. AbilityMenuEntry entry = p.GetComponent<AbilityMenuEntry>();
5. entry.transform.SetParent(panel.transform, false);
6. entry.transform.localScale = Vector3.one;
7. entry.gameObject.SetActive(true);
8. entry.Reset();
9. return entry;
10. }
11.
12. void Enqueue (AbilityMenuEntry entry)
13. {
14. Poolable p = entry.GetComponent<Poolable>();
15. GameObjectPoolController.Enqueue(p);
16. }
Anytime I want to clear the menu, I will want to make sure and loop through each entry and
Enqueue it.
1. void Clear ()
2. {
3. for (int i = menuEntries.Count - 1; i >= 0; --i)
4. Enqueue(menuEntries[i]);
5. menuEntries.Clear();
6. }
In MonoBehaviour’s Start, we will make sure the Panel has hidden itself and then disable the
canvas until we need it.
1. void Start ()
2. {
3. panel.SetPosition(HideKey, false);
4. canvas.SetActive(false);
5. }
When we are ready to show and hide the Menu through game events, we will want to animate it
into position. I added a method to catch the Tweener and specify a consistent duration and
easing equation.
The menu itself will always highlight a single entry as Selected. Since entries can be locked, we
will need to know whether or not we are allowed to select any given entry. If we can’t select an
entry, then we will need to try to select something else instead.
We will expose two public methods which will allow the menu controller to try to select the next
or previous entry in its list. Since the adjacent entry or entries could in theory be locked, I need to
try setting the selection in a loop. I use a for loop which, at most, will loop enough to test every
entry in the menu list, potentially coming back to where it started. It may not be intuitive how to
achieve this using a for loop, since to check each entry you will need the index value to wrap. The
solution for wrapping is the modulus operator.
To initially load and display the menu, I’ve exposed a method called Show where you pass along
the title to display in the header, as well as a list of string which are the text to show for each
entry in the menu.
After loading the menu, you may wish to specify that some of the menu entries are locked. In this
game, you are allowed to Move and or take an Action once in each turn. If you move, then the
next time you see the command menu on the same turn, the move option will be locked – this
way you aren’t allowed to move again until you get another turn.
1. public void SetLocked (int index, bool value)
2. {
3. if (index < 0 || index >= menuEntries.Count)
4. return;
5.
6. menuEntries[index].IsLocked = value;
7. if (value && selection == index)
8. Next();
9. }
When the user con rms a menu selection (using the Fire1 input) then we can dismiss the panel.
Turn
To help display the funcionality of our new menu, I feel that it would be a good time to go ahead
and model a Turn. Each turn I want a unit to be able to Move and or Attack, but to do so in any
order. Moving should be undo-able, but not attacking, otherwise you could undo and repeat an
attack until whatever kind of random modi ers in play would occur the way you desired. In
addition, if you move and then attack, you should not be able to undo your move because you
could cheat the system by moving in close, attacking, undoing your move, and then moving out
of reach of a counter attack.
The process of updating the turn data, handing undo, etc will all come through Game States, but
the relevant data itself will be held in a model. Add a script named Turn to the Scripts/Model
folder.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Turn
6. {
7. public Unit actor;
8. public bool hasUnitMoved;
9. public bool hasUnitActed;
10. public bool lockMove;
11. Tile startTile;
12. Directions startDir;
13.
14. public void Change (Unit current)
15. {
16. actor = current;
17. hasUnitMoved = false;
18. hasUnitActed = false;
19. lockMove = false;
20. startTile = actor.tile;
21. startDir = actor.dir;
22. }
23.
24. public void UndoMove ()
25. {
26. hasUnitMoved = false;
27. actor.Place(startTile);
28. actor.dir = startDir;
29. actor.Match();
30. }
31. }
Battle Controller
We will need to add a few more properties to the Battle Controller, so go ahead and open it up for
editing. We will add an instance of Turn, a list of all the units in battle (dont forget to add a using
System.Collections.Generic), and of course the AbilityMenuPanelController.
Since we have now modeled a Turn, and the turn knows what unit is currently selected, we dont
need the reference to currentUnit in the BattleController. Go ahead and remove it, and x the
references which will now be broken (see MoveSequenceState and MoveTargetState).
Battle State
We are likely to want to use the properties we just added to the Battle Controller in our Battle
States, so lets wrap them for convenience.
Since we added a List of all the Units to the battle controller, we need to populate it with
something. The units are currently instantiated in the Init state, so open that script and add the
following line at the end of the for loop in the SpawnTestUnits method.
1. units.Add(unit);
The SelectUnitState was a temporary implementation we added before which allowed you to
select what unit to move. In the real game, the units will be selected from a turn order based on
their speed. We are not going to add that feature completely yet, but I will simulate something
which is closer – we will have the units get turns in a simple linear fashion, one after another. I
want to do this because it helps show the turn sequence more clearly since you have no control
over whose turn it is.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class SelectUnitState : BattleState
5. {
6. int index = -1;
7.
8. public override void Enter ()
9. {
10. base.Enter ();
11. StartCoroutine("ChangeCurrentUnit");
12. }
13.
14. IEnumerator ChangeCurrentUnit ()
15. {
16. index = (index + 1) % units.Count;
17. turn.Change(units[index]);
18. yield return null;
19. owner.ChangeState<CommandSelectionState>();
20. }
21. }
Explore State
Now that we have removed the ability to move the cursor freely around the board from the
SelectUnitState, we need to add a way to return that ability to the user. Sometimes you want to
get a good idea of the layout of the board to plan your movement route, or to see what enemies
are on the board and where they are located. This state will be accessable by canceling from the
command menu.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class ExploreState : BattleState
5. {
6. protected override void OnMove (object sender, InfoEventArgs<Point> e)
7. {
8. SelectTile(e.info + pos);
9. }
10.
11. protected override void OnFire (object sender, InfoEventArgs<int> e)
12. {
13. if (e.info == 0)
14. owner.ChangeState<CommandSelectionState>();
15. }
16. }
There will be several game states which are very similar – basically I will model a state for each
page and sub-page of the menu. Their shared functionality will be to load and display the menu
on Enter, dismiss the menu on Exit, use button presses to con rm or cancel, and use keyboard /
joystick input for changing the menu selection. Because of the shared functionality, I created an
abstract base class so that I only need to implement the parts of the code which are different.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public abstract class BaseAbilityMenuState : BattleState
6. {
7. protected string menuTitle;
8. protected List<string> menuOptions;
9.
10. public override void Enter ()
11. {
12. base.Enter ();
13. SelectTile(turn.actor.tile.pos);
14. LoadMenu();
15. }
16.
17. public override void Exit ()
18. {
19. base.Exit ();
20. abilityMenuPanelController.Hide();
21. }
22.
23. protected override void OnFire (object sender, InfoEventArgs<int> e)
24. {
25. if (e.info == 0)
26. Confirm();
27. else
28. Cancel();
29. }
30.
31. protected override void OnMove (object sender, InfoEventArgs<Point> e)
32. {
33. if (e.info.x > 0 || e.info.y < 0)
34. abilityMenuPanelController.Next();
35. else
36. abilityMenuPanelController.Previous();
37. }
38.
39. protected abstract void LoadMenu ();
40. protected abstract void Confirm ();
41. protected abstract void Cancel ();
42. }
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class CommandSelectionState : BaseAbilityMenuState
6. {
7. // Add Code Here
8. }
The only code we need to add is the implementation of the abstract methods listed in the base
class. For loading the menu, we will always have the same three options, so it is ne just to hard-
code them. However, the options can be locked based on when in the turn you see this menu.
Since the menu options wont change, we can also hard-code the actions to take on the menu
selection.
If you try to cancel from this state, one of two things will occur – it will either undo your Move (if
possible) or switch to the ExploreState so you can search the board.
When the user chooses Action from the Command Selection State, this state will become active.
It allows you to determine what kind of action to take. You can simply Attack, or you can activate
a special skill from categories of skills like White or Black Magic.
Although Attack will be a xed entry in this sub-menu, the other categories themselves should be
dynamic based on the Unit selected and whatever determines what skills they actually have (like
their Job, etc). Because I haven’t added this portion of the system, we will just hard-code a few
options to serve as place-holder content.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class CategorySelectionState : BaseAbilityMenuState
6. {
7. protected override void LoadMenu ()
8. {
9. if (menuOptions == null)
10. {
11. menuTitle = "Action";
12. menuOptions = new List<string>(3);
13. menuOptions.Add("Attack");
14. menuOptions.Add("White Magic");
15. menuOptions.Add("Black Magic");
16. }
17.
18. abilityMenuPanelController.Show(menuTitle, menuOptions);
19. }
20.
21. protected override void Confirm ()
22. {
23. switch (abilityMenuPanelController.selection)
24. {
25. case 0:
26. Attack();
27. break;
28. case 1:
29. SetCategory(0);
30. break;
31. case 2:
32. SetCategory(1);
33. break;
34. }
35. }
36.
37. protected override void Cancel ()
38. {
39. owner.ChangeState<CommandSelectionState>();
40. }
41.
42. void Attack ()
43. {
44. turn.hasUnitActed = true;
45. if (turn.hasUnitMoved)
46. turn.lockMove = true;
47. owner.ChangeState<CommandSelectionState>();
48. }
49.
50. void SetCategory (int index)
51. {
52. ActionSelectionState.category = index;
53. owner.ChangeState<ActionSelectionState>();
54. }
55. }
Note that this implementation doesn’t check whether or not to lock any menu entries, but in a
fully implemented game, there might be conditions which could. For example, some sort of
status ailment might prevent you from Attacking. Those sorts of scenarios could be addressed in
the LoadMenu method.
When the user Con rms an Attack, I mark the Turn model to show that an action has taken place.
This way the next time the CommandSelectionState is entered, we wont be able to take another
action. If the Unit has also moved, I also tell the turn to lock movement so that it can’t be undone
either.
Whenever the user selects a skill category, we will enter another sub-menu and state, but rst, we
let the state know which category we picked.
Finally, if the user should cancel from this state, we simply drop back to the previous menu.
The nal menu state we will add is the one which appears when picking a skill category from the
CategorySelectionState. The implementation shown below is all hard coded, but only because we
have not designed this portion of the game yet. In the future, the choices should be dynamically
driven based on the unlocked skill-set of the current unit.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ActionSelectionState : BaseAbilityMenuState
6. {
7. public static int category;
8. string[] whiteMagicOptions = new string[] { "Cure", "Raise", "Holy" };
9. string[] blackMagicOptions = new string[] { "Fire", "Ice", "Lightning" };
10.
11. protected override void LoadMenu ()
12. {
13. if (menuOptions == null)
14. menuOptions = new List<string>(3);
15.
16. if (category == 0)
17. {
18. menuTitle = "White Magic";
19. SetOptions(whiteMagicOptions);
20. }
21. else
22. {
23. menuTitle = "Black Magic";
24. SetOptions(blackMagicOptions);
25. }
26.
27. abilityMenuPanelController.Show(menuTitle, menuOptions);
28. }
29.
30. protected override void Confirm ()
31. {
32. turn.hasUnitActed = true;
33. if (turn.hasUnitMoved)
34. turn.lockMove = true;
35. owner.ChangeState<CommandSelectionState>();
36. }
37.
38. protected override void Cancel ()
39. {
40. owner.ChangeState<CategorySelectionState>();
41. }
42.
43. void SetOptions (string[] options)
44. {
45. menuOptions.Clear();
46. for (int i = 0; i < options.Length; ++i)
47. menuOptions.Add(options[i]);
48. }
49. }
Now that we have menu states, it would be nice to be able to cancel out of the move target state.
Modify the OnFire method to the following:
1. IEnumerator Sequence ()
2. {
3. Movement m = turn.actor.GetComponent<Movement>();
4. yield return StartCoroutine(m.Traverse(owner.currentTile));
5. turn.hasUnitMoved = true;
6. owner.ChangeState<CommandSelectionState>();
7. }
Scene Setup
This screen grab shows the menu with a single entry called “Move” but the menu itself is
dynamic and able to hold as many entries as we ask it to. We will be creating two prefabs from
this: the entry itself (so we can clone it and add entries) and the menu panel (without the entry
attached to the hierarchy).
Use the following hierarchy screen grab and inspector screen grabs (there is one for each object –
in order – from top to bottom) to help you recreate the asset. When you have nished creating all
the pieces, attaching scripts and assigning references, then drag from the Ability Menu Entry in
the Hierarchy pane to the Project pane to create a prefab of just that bit of content. Afterward,
delete the Ability Menu Entry from the hierarchy and then create a prefab by dragging the Ability
Menu Controller to the Project pane. It is important that you don’t include the entry in this prefab,
or you will always have one more entry in your menu than you expected.
Note that the reference to Entry Prefab on the Ability Menu Controller should point to the prefab
in the Project, not the instance that had been in the scene. If you accidentally assigned the local
reference, then when the entry is deleted, your assignment will be lost.
Make sure to save the Project as well as the Scene so that the Ability Menu Entry is included.
Also, don’t forget to assign the reference in the Battle Controller for this menu.
Go ahead and play the scene. Make sure to try out various combinations of making progress on
your turn and then cancelling it to verify that our ability to undo is correctly implemented.
In The Future…
Although moving a unit on the board feels complete, there is a lot left to do regarding unit
Actions. When you choose to Move you enter a state which allows you to pick your move target.
When you take an Action, whether attacking or using magic, we will need to provide a similar
state which allows us to pick a target(s). Then, after picking a target(s), we should have another
state which gives us an estimate of the effect of applying the action to the target(s).
We will also add a state at the end of a turn which allows you to control the end facing direction
of your unit, instead of coming back to the Command Selection State and making you choose
Wait.
Those few additions will help the turn ow feel much better, but what we have is good enough to
get a general idea, and to see how to work with the Menu itself, which was the primary goal of
this lesson.
Summary
This was another huge lesson – I hope I didn’t lose anyone! We created a reusable menu which
allowed the user to control the ow of a turn. We modeled the turn data itself, and hooked up the
menu to a new sequence of Game States which made it easy to apply commands and even undo
actions. There is still a lot left to do in order to make it a fully playable game, but we are looking
more and more complete all the time!
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
In this lesson we will begin adding Stats to our game. Experience and Level are particularly
important stats, so we will begin by focusing on those and show how we might distribute
experience between heroes in a party. As enough experience is gained, the level of the hero can
also be incremented. Along the way I will also address how one system might create an
exception-to-the-rule in another system and offer a solution on how to allow them to interract
while keeping things as decoupled as possible.
Preface
As usual, I internally debated on the implementation for this portion of the project – should I use
GameObjects and Monobehaviours or simple C# objects? I know a lot of the more advanced users
will complain if I use a Monobehaviour for stuff like this. Simple C# classes provide some nice
exibility, and reducing it further (to something like a database), would probably be even better.
In an effort to implement the game with native C# objects I created my own custom component
based system. Because it was based on Unity, I found it very easy to adapt to and I had all the
exibility and speed I wanted without losing the features I needed. Unfortunately, very few people
seemed interested in the system (almost no views), and the only feedback I received was
negative. The comments I received were basically that it was a lot of extra work for no extra
features, or that Unity’s component based architecture sucked and I should have gone for an
Entity Component System if I was going to bother to change anything.
So what to do? I decided to embrace my toolset. I like Unity, and I like working with GameObjects
and MonoBehaviours. By using Unity’s implementation we will be able to make use of a lot of
great features including their component architecture (hopefully you don’t hate it), serialization
(within a scene and or the project itself), editor visual aids (the inspector is great during
development for debugging), etc. Sure they might not be the most ef cient, but they are feature
rich and good enough that you can certainly create a game, and even a nicely performant game
at the level I am aiming for without worry. Perhaps if I were making the next multi-million dollar
MMORPG I would need something more advanced, but I am not there yet, so I wouldn’t be a great
teacher on that anyway.
If you are advanced enough to think my decision is too primitive, then you probably are skilled
enough to make good use of my earlier mentioned system or an ECS, etc. Simply adapt the ideas I
present here if you like them. Otherwise, don’t be so quick to judge. The speed at which Unity
helps one prototype a game is hard to compete with.
Noti cations
This lesson will be taking advantage of my noti cation center. I will be using the version I
blogged about most recently, here. Of course you can also get a copy from the repository here.
Because I have already spoken a lot on the noti cation center I wont spend much time on it now.
For a quick introduction, you can think of it as a messaging system which is similar to events.
However, any object can post any noti cation and any object can listen to any noti cation (and
you can specify whether you wish to listen to ALL senders and or from a targeted sender). The
noti cation itself is a string, which means it can be dynamic, and this is something we will be
taking advantage of in this lesson.
Base Exception
Don’t confuse this with C# language Exceptions (an error occuring during application execution).
Here I am talking about the complex interactions between systems of an RPG. For example, you
may normally be able to move, except you stepped in some glue and are waiting for the effect to
wear off. Or you might normally do X amount of damage, but your sword has an elemental charge
that the opponent is weak to so it now does extra damage. Rather than have all systems know
about all other systems to handle each use-case, I have decided to create an exception class
which is posted along with noti cations where an exception might be needed. Listeners which
need to produce the exception can then listen and alter a scenario as necessary.
The most basic level of exception I will model is one where a thing is normally allowed (but might
be able to be blocked) or vice-versa. For this, create a new script named BaseException in the
Scripts/Exceptions folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class BaseException
5. {
6. public bool toggle { get; private set; }
7. private bool defaultToggle;
8.
9. public BaseException (bool defaultToggle)
10. {
11. this.defaultToggle = defaultToggle;
12. toggle = defaultToggle;
13. }
14.
15. public void FlipToggle ()
16. {
17. toggle = !defaultToggle;
18. }
19. }
Modi ers
We are creating a stat system, and the exceptions we will be interested in will probably extend
beyond whether or not to allow a stat to be changed and into the realm of how to change it. For
example, if you are awarding experience to a hero, but your hero has equipped an amulet which
causes experience to grow faster, then it will want a chance to modify the value which will be
assigned.
When you post a noti cation, you wont know the order in which listeners are noti ed. However,
the order which modi cations are applied can be signi cant. For example, the following two
statements would produce different results based on the order of execution indicated by the
parentheses:
I want to allow multiple listeners the chance to apply changes but I also want some changes to be
done earlier than other changes, or want to specify another change to happen after all other
changes. If you have spent any time looking at damage algorithms in Final Fantasy you wont be
surprised to see twenty or so steps along the way. They might start with a base damage formula,
then add bonuses for equipment, then buffs, then the phase of the moon (possibly not joking
here), and perhaps next would be the angle between the attacker and defender. Then they may
clamp some values for good measure before continuing down the path. It is quite complex!
I chose to accomplish this in the following way. Any exception which includes modi ers will
store them all as a list. All modi ers will have a sort order. After all modi ers have been added,
they will be sorted based on their sort order, and then their modi cations will be applied
sequentially.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class Modifier
5. {
6. public readonly int sortOrder;
7.
8. public Modifier (int sortOrder)
9. {
10. this.sortOrder = sortOrder;
11. }
12. }
As I indicated, there may be lot’s of different kinds of modi ers taking place (particularly in
regard to modifying a value), some for adding, some for multiplying, some for clamping values,
etc. Here is the base abstract class for a value modi er followed by several concrete
implementations:
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class ValueModifier : Modifier
5. {
6. public ValueModifier (int sortOrder) : base (sortOrder) {}
7. public abstract float Modify (float value);
8. }
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AddValueModifier : ValueModifier
5. {
6. public readonly float toAdd;
7.
8. public AddValueModifier (int sortOrder, float toAdd) : base (sortOrder)
9. {
10. this.toAdd = toAdd;
11. }
12.
13. public override float Modify (float value)
14. {
15. return value + toAdd;
16. }
17. }
1. using UnityEngine;
2. using System.Collections;
3.
4. public class ClampValueModifier : ValueModifier
5. {
6. public readonly float min;
7. public readonly float max;
8.
9. public ClampValueModifier (int sortOrder, float min, float max) : base (sortOrder)
10. {
11. this.min = min;
12. this.max = max;
13. }
14.
15. public override float Modify (float value)
16. {
17. return Mathf.Clamp(value, min, max);
18. }
19. }
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MaxValueModifier : ValueModifier
5. {
6. public float max;
7.
8. public MaxValueModifier (int sortOrder, float max) : base (sortOrder)
9. {
10. this.max = max;
11. }
12.
13. public override float Modify (float value)
14. {
15. return Mathf.Max(value, max);
16. }
17. }
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MinValueModifier : ValueModifier
5. {
6. public float min;
7.
8. public MinValueModifier (int sortOrder, float min) : base (sortOrder)
9. {
10. this.min = min;
11. }
12.
13. public override float Modify (float value)
14. {
15. return Mathf.Min(min, value);
16. }
17. }
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MultValueModifier : ValueModifier
5. {
6. public readonly float toMultiply;
7.
8. public MultValueModifier (int sortOrder, float toMultiply) : base (sortOrder)
9. {
10. this.toMultiply = toMultiply;
11. }
12.
13. public override float Modify (float value)
14. {
15. return value * toMultiply;
16. }
17. }
Based on the ideas I brought forth in the topics of value modi ers, here is a concrete subclass of
the Base Exception which holds a list of value modi ers to modify the value which will be
assigned in an exception use-case.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ValueChangeException : BaseException
6. {
7. #region Fields / Properteis
8. public readonly float fromValue;
9. public readonly float toValue;
10. public float delta { get { return toValue - fromValue; }}
11. List<ValueModifier> modifiers;
12. #endregion
13.
14. #region Constructor
15. public ValueChangeException (float fromValue, float toValue) : base (true)
16. {
17. this.fromValue = fromValue;
18. this.toValue = toValue;
19. }
20. #endregion
21.
22. #region Public
23. public void AddModifier (ValueModifier m)
24. {
25. if (modifiers == null)
26. modifiers = new List<ValueModifier>();
27. modifiers.Add(m);
28. }
29.
30. public float GetModifiedValue ()
31. {
32. float value = toValue;
33.
34. if (modifiers == null)
35. return value;
36.
37. modifiers.Sort(Compare);
38. for (int i = 0; i < modifiers.Count; ++i)
39. value = modifiers[i].Modify(value);
40.
41. return value;
42. }
43. #endregion
44.
45. #region Private
46. int Compare (ValueModifier x, ValueModifier y)
47. {
48. return x.sortOrder.CompareTo(y.sortOrder);
49. }
50. #endregion
51. }
Stat Types
Individual stat types are just an abstract idea. Most every RPG out there, including those within
the same series (like Final Fantasy), use a different set of stats to represent each game. Even if
you have similarly named stats, the formulas you use for level growth, damage, etc can all be
wildly different. There are often a lot of different stats, and because I am in a prototype stage it
doesn’t necessarily make a lot of sense to hard code each stat. Instead, I will begin with an
enumeration. By associating the enum type with a value, we can also make it really easy for other
game features to target and or respond to a particular stat in a DRY (Dont Repeat Yourself)
manner.
Create a new script named StatTypes in the Scripts/Enums folder. The implementation follows:
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum StatTypes
5. {
6. LVL, // Level
7. EXP, // Experience
8. HP, // Hit Points
9. MHP, // Max Hit Points
10. MP, // Magic Points
11. MMP, // Max Magic Points
12. ATK, // Physical Attack
13. DEF, // Physical Defense
14. MAT, // Magic Attack
15. MDF, // Magic Defense
16. EVD, // Evade
17. RES, // Status Resistance
18. SPD, // Speed
19. MOV, // Move Range
20. JMP, // Jump Height
21. Count
22. }
Stat Component
Anything which needs stats (heroes, enemies, bosses, etc) will have a Stat component added to it.
This component will provide a single reference point from which we can relate a stat type to a
value held by the stat.
Create a subfolder called Actor under Scripts/View Model Component and add a new script there
named Stats.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Stats : MonoBehaviour
6. {
7. // Add Code Here
8. }
If you were confused earlier when I said we wouldn’t hard code our stats (but then I hard coded an
enumeration), hopefully it is about to make more sense. Instead of having individual int elds for
each of the stat types, I can make an array which aligns a stat type to an int. If I want to add,
remove, or rename a stat I only have to worry about the enum and this will still work.
Note that I wont actually make the backing array public. Other classes dont need to know how or
where I store the data. Instead, I will add an indexer which allows getting and setting values
safely:
The setter (via the SetValue method) will handle several bits of logic. Don’t miss the fact that I am
able to reuse this logic regardless of which stat is changing!
One job of the setter will be to post noti cations that we will be changing a stat (in case listeners
want a chance to make some sort of exception) and another noti cation will be posted after we
did change a stat (in case listeners want a chance to respond based on the change). For example,
after incrementing the experience stat, a listener might also decide to modify the level stat to
match. After incrementing the level stat, a variety of other stats might change such as attack or
defense.
The noti cations for each stat are built dynamically and stored statically by the class. This way
both the listeners and the component itself can continually reuse a string instead of constantly
needing to recreate it.
The rst thing our setter checks is whether or not there are any changes to the value. If not we
just exit early. If exceptions are allowed we will create a ValueChangeException and post it along
with our will change noti cation. If the value does change, we assign the new value in the array
and post a noti cation that the stat value actually changed.
Components which handle loading and or initialization can make use of the public method
directly and specify that exceptions should not be allowed. Pretty well any other time that the
stat values need to be set or modi ed should probably go through the indexer which does allow
for exceptions.
Rank
Our hero actors will have a component called Rank which determines how the experience (EXP)
stat relates to the level (LVL) stat. For example, as the experience stat increments so will the level
stat (though not at the same rate). The leveling curve is based on an Ease In Quad curve, which
means that low levels can be attained with less experience than high levels. For instance, it only
takes 104 experience to go from level 1 to level 2, but it takes 20,304 experience to go from level 98
to level 99!
I think of this component as something like a wrapper because it doesn’t hold any new elds of
its own, it simply exposes convenience properties around the existing Stats component’s values.
Add another script called Rank to the Scripts/View Model Component/Actor/ folder. The
implementation follows:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Rank : MonoBehaviour
5. {
6. #region Consts
7. public const int minLevel = 1;
8. public const int maxLevel = 99;
9. public const int maxExperience = 999999;
10. #endregion
11.
12. #region Fields / Properties
13. public int LVL
14. {
15. get { return stats[StatTypes.LVL]; }
16. }
17.
18. public int EXP
19. {
20. get { return stats[StatTypes.EXP]; }
21. set { stats[StatTypes.EXP] = value; }
22. }
23.
24. public float LevelPercent
25. {
26. get { return (float)(LVL - minLevel) / (float)(maxLevel - minLevel); }
27. }
28.
29. Stats stats;
30. #endregion
31.
32. #region MonoBehaviour
33. void Awake ()
34. {
35. stats = GetComponent<Stats>();
36. }
37.
38. void OnEnable ()
39. {
40. this.AddObserver(OnExpWillChange, Stats.WillChangeNotification(StatTypes.EXP),
stats);
41. this.AddObserver(OnExpDidChange, Stats.DidChangeNotification(StatTypes.EXP),
stats);
42. }
43.
44. void OnDisable ()
45. {
46. this.RemoveObserver(OnExpWillChange, Stats.WillChangeNotification(StatTypes.EXP),
stats);
47. this.RemoveObserver(OnExpDidChange, Stats.DidChangeNotification(StatTypes.EXP),
stats);
48. }
49. #endregion
50.
51. #region Event Handlers
52. void OnExpWillChange (object sender, object args)
53. {
54. ValueChangeException vce = args as ValueChangeException;
55. vce.AddModifier(new ClampValueModifier(int.MaxValue, EXP, maxExperience));
56. }
57.
58. void OnExpDidChange (object sender, object args)
59. {
60. stats.SetValue(StatTypes.LVL, LevelForExperience(EXP), false);
61. }
62. #endregion
63.
64. #region Public
65. public static int ExperienceForLevel (int level)
66. {
67. float levelPercent = Mathf.Clamp01((float)(level - minLevel) / (float)(maxLevel -
minLevel));
68. return (int)EasingEquations.EaseInQuad(0, maxExperience, levelPercent);
69. }
70.
71. public static int LevelForExperience (int exp)
72. {
73. int lvl = maxLevel;
74. for (; lvl >= minLevel; --lvl)
75. if (exp >= ExperienceForLevel(lvl))
76. break;
77. return lvl;
78. }
79.
80. public void Init (int level)
81. {
82. stats.SetValue(StatTypes.LVL, level, false);
83. stats.SetValue(StatTypes.EXP, ExperienceForLevel(level), false);
84. }
85. #endregion
86. }
Note that this component subscribes to the EXP Will Change stat noti cation. It creates a clamp
modi er with the highest possible sort order (to make sure it is the last modi er to be applied). It
makes sure that experience is ONLY allowed to increment – not decrement. No un-leveling in
this game. Of course you may not wish to have this constraint in your own game. Perhaps the
ability to lose experience could be a fun feature! Who knows?
We also subscribe to the EXP Did Change stat noti cation. This way we can make sure that the
LVL stat is always correctly set based on how the experience growth curve would specify.
One nal note is that I expose a couple of public static methods which allow you to convert
between experience values and level values. These might be useful if you were creating a UI
which showed some sort of progress bar of how close you are to the next level.
Experience Manager
Next let’s create a system which can distribute experience among a team of heroes. Perhaps in a
normal battle, every enemy unit killed adds a certain amount of experience to a shared pool for
later. If, and only if, the level/battle is conquered will the team actually receive the experience
points and have a chance to “Level-up”. I would like heroes that are lower level to receive more
experience than the heroes with a higher level, but I want to make sure that all heroes still gain
experience points. Of course there can still be exceptions here like perhaps any KO’d heroes will
not be able to receive experience.
1. using UnityEngine;
2. using System;
3. using System.Collections;
4. using System.Collections.Generic;
5. using Party = System.Collections.Generic.List<UnityEngine.GameObject>;
6.
7. public static class ExperienceManager
8. {
9. const float minLevelBonus = 1.5f;
10. const float maxLevelBonus = 0.5f;
11.
12. public static void AwardExperience (int amount, Party party)
13. {
14. // Grab a list of all of the rank components from our hero party
15. List<Rank> ranks = new List<Rank>(party.Count);
16. for (int i = 0; i < party.Count; ++i)
17. {
18. Rank r = party[i].GetComponent<Rank>();
19. if (r != null)
20. ranks.Add(r);
21. }
22.
23. // Step 1: determine the range in actor level stats
24. int min = int.MaxValue;
25. int max = int.MinValue;
26. for (int i = ranks.Count - 1; i >= 0; --i)
27. {
28. min = Mathf.Min(ranks[i].LVL, min);
29. max = Mathf.Max(ranks[i].LVL, max);
30. }
31.
32. // Step 2: weight the amount to award per actor based on their level
33. float[] weights = new float[party.Count];
34. float summedWeights = 0;
35. for (int i = ranks.Count - 1; i >= 0; --i)
36. {
37. float percent = (float)(ranks[i].LVL - min) / (float)(max - min);
38. weights[i] = Mathf.Lerp(minLevelBonus, maxLevelBonus, percent);
39. summedWeights += weights[i];
40. }
41.
42. // Step 3: hand out the weighted award
43. for (int i = ranks.Count - 1; i >= 0; --i)
44. {
45. int subAmount = Mathf.FloorToInt((weights[i] / summedWeights) * amount);
46. ranks[i].EXP += subAmount;
47. }
48. }
49. }
Note that this sample is just a rough prototype and hasn’t really been play-tested. If your game’s
party only consisted of 2 units, one at level 4 and one at level 5, it might seem odd for the rst unit
to get three times as much experience as the other unit. If you had a party of 6 or so units you
may never notice. By keeping the system separate it should be easy to tweak to our hearts
content without fear of messing up anything else.
Test & Demo
Let’s wrap this lesson up with a quick test which also serves as a demo. In this test I want to
verify that my implementation of converting back and forth between LVL and EXP in the Rank
component works for every level as expected. So I will loop from level 1 thru 99 and verify that the
output from the static methods match.
For my second test / demo I will create an array of heroes, init them all to random levels, and
then use the manager to award the party an amount of experience. I will use a variety of
modi ers to tweak the value awarded and print each step along the way to verify that it all works
as expected.
Create a new scene and add a new script to the camera called TestLevelGrowth. The
implementation follows.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4. using Party = System.Collections.Generic.List<UnityEngine.GameObject>;
5.
6. public class TestLevelGrowth : MonoBehaviour
7. {
8. void OnEnable ()
9. {
10. this.AddObserver(OnLevelChange, Stats.DidChangeNotification(StatTypes.LVL));
11. this.AddObserver(OnExperienceException,
Stats.WillChangeNotification(StatTypes.EXP));
12. }
13.
14. void OnDisable ()
15. {
16. this.RemoveObserver(OnLevelChange, Stats.DidChangeNotification(StatTypes.LVL));
17. this.RemoveObserver(OnExperienceException,
Stats.WillChangeNotification(StatTypes.EXP));
18. }
19.
20. void Start ()
21. {
22. VerifyLevelToExperienceCalculations ();
23. VerifySharedExperienceDistribution ();
24. }
25.
26. void VerifyLevelToExperienceCalculations ()
27. {
28. for (int i = 1; i < 100; ++i)
29. {
30. int expLvl = Rank.ExperienceForLevel(i);
31. int lvlExp = Rank.LevelForExperience(expLvl);
32.
33. if (lvlExp != i)
34. Debug.Log( string.Format("Mismatch on level:{0} with exp:{1} returned:{2}", i,
expLvl, lvlExp) );
35. else
36. Debug.Log(string.Format("Level:{0} = Exp:{1}", lvlExp, expLvl));
37. }
38. }
39.
40. void VerifySharedExperienceDistribution ()
41. {
42. string[] names = new string[]{ "Russell", "Brian", "Josh", "Ian", "Adam", "Andy"
};
43.
44. Party heroes = new Party();
45. for (int i = 0; i < names.Length; ++i)
46. {
47. GameObject actor = new GameObject(names[i]);
48. actor.AddComponent<Stats>();
49. Rank rank = actor.AddComponent<Rank>();
50. rank.Init((int)UnityEngine.Random.Range(1, 5));
51. heroes.Add(actor);
52. }
53.
54. Debug.Log("===== Before Adding Experience ======");
55. LogParty(heroes);
56.
57. Debug.Log("=====================================");
58. ExperienceManager.AwardExperience(1000, heroes);
59.
60. Debug.Log("===== After Adding Experience ======");
61. LogParty(heroes);
62. }
63.
64. void LogParty (Party p)
65. {
66. for (int i = 0; i < p.Count; ++i)
67. {
68. GameObject actor = p[i];
69. Rank rank = actor.GetComponent<Rank>();
70. Debug.Log( string.Format("Name:{0} Level:{1} Exp:{2}", actor.name, rank.LVL,
rank.EXP) );
71. }
72. }
73.
74. void OnLevelChange (object sender, object args)
75. {
76. Stats stats = sender as Stats;
77. Debug.Log(stats.name + " leveled up!");
78. }
79.
80. void OnExperienceException (object sender, object args)
81. {
82. GameObject actor = (sender as Stats).gameObject;
83. ValueChangeException vce = args as ValueChangeException;
84. int roll = UnityEngine.Random.Range(0, 5);
85. switch (roll)
86. {
87. case 0:
88. vce.FlipToggle();
89. Debug.Log(string.Format("{0} would have received {1} experience, but we stopped
it", actor.name, vce.delta));
90. break;
91. case 1:
92. vce.AddModifier( new AddValueModifier( 0, 1000 ) );
93. Debug.Log(string.Format("{0} would have received {1} experience, but we added
1000", actor.name, vce.delta));
94. break;
95. case 2:
96. vce.AddModifier( new MultValueModifier( 0, 2f ) );
97. Debug.Log(string.Format("{0} would have received {1} experience, but we
multiplied by 2", actor.name, vce.delta));
98. break;
99. default:
100. Debug.Log(string.Format("{0} will receive {1} experience", actor.name,
vce.delta));
101. break;
102. }
103. }
104. }
Run the scene and look through the console’s output to verify that everything is as you would
expect it to be.
Summary
That ended up being a lot longer than I expected, again. One might think that something as
simple as adding some stats and tying EXP to LVL would be easy. But then we started adding
exceptions to the rule, value modi ers, a manager to distribute the experience among a party, etc.
and things got a lot more complex. Hopefully you were able to follow along with all of my
examples and implementations. If not, feel free to add a comment below!
The Liquid Fire
Game Programming Blog
Now that we have stats, we will need new ways to modify them. Of course one way is through
leveling up your character, but a potentially more fun way is by items. Equiping a sword which is
not only cool looking but provides a great bonus to your ATK (attack) stat can be very rewarding.
Likewise, when you are damaged in battle a health potion might be in order to boost your HP (hit
points) stat. In this lesson we will examine a few ways to create items, both consumable and
equippable, as well as how to manage your equipment.
Feature
The main purpose of an item is to modify something. I will describe this ability to modify a thing
as a feature of the item. Every item will need some sort of feature, and possibly multiple features,
so let’s start by creating an abstract class by this name. Add the script to the Scripts/View Model
Component/Features folder.
1. A feature can be activated for a time and then be deactivated. This would be the case with
equipment – equip a sword for an attack boost but when you un-equip the sword your attack
stat drops back down. I have exposed the methods Activate and Deactivate for this purpose.
2. A feature can have a one-shot (permanent) application. This would be the case when you
consume a health potion – you get a boost to your hit point stat which doesn’t need to be un-
done by the item. I have exposed the method Apply for this purpose.
The base class applications are not virtual, so they will always work in a very speci c way.
Concrete subclasses implement what happens when the feature is activated in the OnApply and
OnRemove methods which were left empty.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class Feature : MonoBehaviour
5. {
6. #region Fields / Properties
7. protected GameObject _target { get; private set; }
8. #endregion
9.
10. #region Public
11. public void Activate (GameObject target)
12. {
13. if (_target == null)
14. {
15. _target = target;
16. OnApply();
17. }
18. }
19.
20. public void Deactivate ()
21. {
22. if (_target != null)
23. {
24. OnRemove();
25. _target = null;
26. }
27. }
28.
29. public void Apply (GameObject target)
30. {
31. _target = target;
32. OnApply();
33. _target = null;
34. }
35. #endregion
36.
37. #region Private
38. protected abstract void OnApply ();
39. protected virtual void OnRemove () {}
40. #endregion
41. }
There are potentially many types of features which could be added in an RPG, such as reviving a
fallen ally, providing some sort of buff, causing a status-ailment, etc. Of course, those are a bunch
of systems we haven’t implemented yet, so all we will focus on at the moment is the simple
ability to modify a stat.
This simple component can be applied to any stat type, either as a good or bad modi er, and is
compatible both with consumable and equippable items. Add a new script named
StatModi erFeature to the Scripts/View Model Component/Features folder. The implementation
follows:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class StatModifierFeature : Feature
5. {
6. #region Fields / Properties
7. public StatTypes type;
8. public int amount;
9.
10. Stats stats
11. {
12. get
13. {
14. return _target.GetComponentInParent<Stats>();
15. }
16. }
17. #endregion
18.
19. #region Protected
20. protected override void OnApply ()
21. {
22. stats[type] += amount;
23. }
24.
25. protected override void OnRemove ()
26. {
27. stats[type] -= amount;
28. }
29. #endregion
30. }
Merchandise
Since we are bringing up the subject of an item, a common concern is how to obtain them? They
could simply be given to the player at the beginning of the game, found in chests or as awards
from battle, but probably most frequently, they can be purchased from a shop. Likewise,
unwanted inventory items can also be sold. In order to facilitate the ability to purchase and or sell
an item let’s add a new component named Merchandise to the Scripts/View Model
Component/Item folder.
This script is exceedingly simple – it merely exposes elds for a buy price and a sell price. We
won’t be doing anything with it at the moment, but additional future functionality might be in
order once we get around to implementing shops.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Merchandise : MonoBehaviour
5. {
6. public int buy;
7. public int sell;
8. }
Consumable
If an item has a consumable component, then our systems which allow the use of items in battle
will be able to show the item as an option for use. The consumption of said item would then apply
whatever features were also on the item, to whichever target is speci ed with the Consume
method. Note that I want to specify a target because it shouldn’t be assumed that the consumable
will be applied to the user – while the user might wish to use a health potion on himself, there
might be times he wishes to use a health potion on an ally, or perhaps the consumable is a bomb,
and then the target would almost certainly be an opponent.
Add another script named Consumable to the Scripts/View Model Component/Item folder. The
implementation follows:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Consumable : MonoBehaviour
5. {
6. public void Consume (GameObject target)
7. {
8. Feature[] features = GetComponentsInChildren<Feature>();
9. for (int i = 0; i < features.Length; ++i)
10. features[i].Apply(target);
11. }
12. }
Equip Slots
Before we can implement the Equippable item component, I need to specify a new enum. This
enum will be a bit-mask which can specify one or more combinations of locations to equip an
item. For example, there might be one-handed as well as two-handed weapons, so the item will
need to indicate how many of these slots to occupy.
Add a new script named EquipSlots to the Scripts/Enums folder. The implementation follows:
1. using UnityEngine;
2. using System.Collections;
3.
4. [System.Flags]
5. public enum EquipSlots
6. {
7. None = 0,
8. Primary = 1 << 0, // usually a weapon (sword etc)
9. Secondary = 1 << 1, // usually a shield, but could be another sword (dual-wield) or
occupied by two-handed weapon
10. Head = 1 << 2, // helmet, hat, etc
11. Body = 1 << 3, // body armor, robe, etc
12. Accessory = 1 << 4 // ring, belt, etc
13. }
Equippable
Items which are equippable have features which are activated for as long as the item is still
equipped. However, with equipment you are required to manage what you choose to wear. For
instance, there may only be a single accessory slot and you will have to choose between the
amulet of speed or the buckler of defense. Perhaps different missions would justify swapping out
your equipment.
Add a new script named Equippable to the Scripts/View Model Component/Item folder. The
implementation is below. Note that I have added three public elds of our EquipSlots enum:
defaultSlots – The EquipSlots ag which is the default equip location(s) for this item. For
example, a normal weapon would only specify primary, but a two-handed weapon would
specify both primary and secondary.
secondarySlots – Some equipment may be allowed to be equipped in more than one slot
location, such as when dual-wielding swords.
slots – The slot(s) where an item is currently equipped.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Equippable : MonoBehaviour
6. {
7. #region Fields
8. public EquipSlots defaultSlots;
9. public EquipSlots secondarySlots;
10. public EquipSlots slots;
11. bool _isEquipped;
12. #endregion
13.
14. #region Public
15. public void OnEquip ()
16. {
17. if (_isEquipped)
18. return;
19.
20. _isEquipped = true;
21.
22. Feature[] features = GetComponentsInChildren<Feature>();
23. for (int i = 0; i < features.Length; ++i)
24. features[i].Activate(gameObject);
25. }
26.
27. public void OnUnEquip ()
28. {
29. if (!_isEquipped)
30. return;
31.
32. _isEquipped = false;
33.
34. Feature[] features = GetComponentsInChildren<Feature>();
35. for (int i = 0; i < features.Length; ++i)
36. features[i].Deactivate();
37. }
38. #endregion
39. }
Equipment
The equippable component gets attached to an item, but we need a different component to attach
to our actors so that they can manage which items they wish to wear, and where they will be
attached. For this we will create a new script called Equipment in the Scripts/View Model
Component/Actor folder.
This component will post noti cations when equipping and un-equipping items so that other
components or game systems can respond (like actually updating a visual character model with
the item). It exposes a readonly list of the items which are currently equipped, just in case
another system needs to review them. Also, the system is somewhat smart in that it will
automatically un-equip any item which had been previously equipped in an overlapping slot(s)
before equipping a new item in the same slot(s).
Note that the class isn’t perfectly safe for just any use-case. For example I am not stopping you
from equipping the same item more than once. I didn’t bother to safe-guard against that use-case
because I am imagining that the user interface will be constructed in such a way as to prevent it
for us. For example, whatever menu allows you to equip an item will only show items from your
inventory, and when you equip an item it will rst be removed from your inventory.
1. using UnityEngine;
2. using System;
3. using System.Collections.Generic;
4.
5. public class Equipment : MonoBehaviour
6. {
7. #region Notifications
8. public const string EquippedNotification = "Equipment.EquippedNotification";
9. public const string UnEquippedNotification = "Equipment.UnEquippedNotification";
10. #endregion
11.
12. #region Fields / Properties
13. public IList<Equippable> items { get { return _items.AsReadOnly(); }}
14. List<Equippable> _items = new List<Equippable>();
15. #endregion
16.
17. #region Public
18. public void Equip (Equippable item, EquipSlots slots)
19. {
20. UnEquip(slots);
21.
22. _items.Add(item);
23. item.transform.SetParent(transform);
24. item.slots = slots;
25. item.OnEquip();
26.
27. this.PostNotification(EquippedNotification, item);
28. }
29.
30. public void UnEquip (Equippable item)
31. {
32. item.OnUnEquip();
33. item.slots = EquipSlots.None;
34. item.transform.SetParent(transform);
35. _items.Remove(item);
36.
37. this.PostNotification(UnEquippedNotification, item);
38. }
39.
40. public void UnEquip (EquipSlots slots)
41. {
42. for (int i = _items.Count - 1; i >= 0; --i)
43. {
44. Equippable item = _items[i];
45. if ( (item.slots & slots) != EquipSlots.None )
46. UnEquip(item);
47. }
48. }
49. #endregion
50. }
Demo
Now that I have created several new components to play with, let’s create a new scene and script
to show off some potential ways they can be used together. I will be creating a mock battle which
plays out on its own. The script will create everything it needs to help make this test easier on
you, however, you should note that in practice I would not create the combatants and items all in
place like this.
It isn’t necessarily bad to have a Factory class to create your items, etc. but as a Unity developer I
would prefer to have created all of the items and actors ahead of time and stored them in some
sort of prefab or scriptable object. Then I only need to obtain a reference (perhaps through
Resources.Load) and instantiate them. For more on this idea see my post on Bestiary
Management and Scriptable Objects.
Create a new script named TestItems anywhere in your project (I dont intend to keep it). Create a
new scene and attach this script to your camera and press Play. Watch how the battle unfolds in
the Console output window. The hero and monster take turns, and the hero may choose to use
items or change equipment. Eventually one of the two will lose all of its hit points and the battle
will end. If you play again you should see a different experience.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class TestItems : MonoBehaviour
6. {
7. #region Fields
8. List<GameObject> inventory = new List<GameObject>();
9. List<GameObject> combatants = new List<GameObject>();
10. #endregion
11.
12. #region MonoBehaviour
13. void Start ()
14. {
15. CreateItems();
16. CreateCombatants();
17. StartCoroutine(SimulateBattle());
18. }
19.
20. void OnEnable ()
21. {
22. this.AddObserver(OnEquippedItem, Equipment.EquippedNotification);
23. this.AddObserver(OnUnEquippedItem, Equipment.UnEquippedNotification);
24. }
25.
26. void OnDisable ()
27. {
28. this.RemoveObserver(OnEquippedItem, Equipment.EquippedNotification);
29. this.RemoveObserver(OnUnEquippedItem, Equipment.UnEquippedNotification);
30. }
31. #endregion
32.
33. #region Notification Handlers
34. void OnEquippedItem (object sender, object args)
35. {
36. Equipment eq = sender as Equipment;
37. Equippable item = args as Equippable;
38. inventory.Remove(item.gameObject);
39. string message = string.Format("{0} equipped {1}", eq.name, item.name);
40. Debug.Log(message);
41. }
42.
43. void OnUnEquippedItem (object sender, object args)
44. {
45. Equipment eq = sender as Equipment;
46. Equippable item = args as Equippable;
47. inventory.Add(item.gameObject);
48. string message = string.Format("{0} un-equipped {1}", eq.name, item.name);
49. Debug.Log(message);
50. }
51. #endregion
52.
53. #region Factory
54. GameObject CreateItem (string title, StatTypes type, int amount)
55. {
56. GameObject item = new GameObject(title);
57. StatModifierFeature smf = item.AddComponent<StatModifierFeature>();
58. smf.type = type;
59. smf.amount = amount;
60. return item;
61. }
62.
63. GameObject CreateConumableItem (string title, StatTypes type, int amount)
64. {
65. GameObject item = CreateItem(title, type, amount);
66. item.AddComponent<Consumable>();
67. return item;
68. }
69.
70. GameObject CreateEquippableItem (string title, StatTypes type, int amount,
EquipSlots slot)
71. {
72. GameObject item = CreateItem(title, type, amount);
73. Equippable equip = item.AddComponent<Equippable>();
74. equip.defaultSlots = slot;
75. return item;
76. }
77.
78. GameObject CreateHero ()
79. {
80. GameObject actor = CreateActor("Hero");
81. actor.AddComponent<Equipment>();
82. return actor;
83. }
84.
85. GameObject CreateActor (string title)
86. {
87. GameObject actor = new GameObject(title);
88. Stats s = actor.AddComponent<Stats>();
89. s[StatTypes.HP] = s[StatTypes.MHP] = UnityEngine.Random.Range(500, 1000);
90. s[StatTypes.ATK] = UnityEngine.Random.Range(30, 50);
91. s[StatTypes.DEF] = UnityEngine.Random.Range(30, 50);
92. return actor;
93. }
94. #endregion
95.
96. #region Private
97. void CreateItems ()
98. {
99. inventory.Add( CreateConumableItem("Health Potion", StatTypes.HP, 300) );
100. inventory.Add( CreateConumableItem("Bomb", StatTypes.HP, -150) );
101. inventory.Add( CreateEquippableItem("Sword", StatTypes.ATK, 10,
EquipSlots.Primary) );
102. inventory.Add( CreateEquippableItem("Broad Sword", StatTypes.ATK, 15,
(EquipSlots.Primary | EquipSlots.Secondary)) );
103. inventory.Add( CreateEquippableItem("Shield", StatTypes.DEF, 10,
EquipSlots.Secondary) );
104. }
105.
106. void CreateCombatants ()
107. {
108. combatants.Add( CreateHero() );
109. combatants.Add( CreateActor("Monster") );
110. }
111.
112. IEnumerator SimulateBattle ()
113. {
114. while (VictoryCheck() == false)
115. {
116. LogCombatants();
117. HeroTurn();
118. EnemyTurn();
119. yield return new WaitForSeconds(1);
120. }
121. LogCombatants();
122. Debug.Log("Battle Completed");
123. }
124.
125. void HeroTurn ()
126. {
127. int rnd = UnityEngine.Random.Range(0, 2);
128. switch (rnd)
129. {
130. case 0:
131. Attack(combatants[0], combatants[1]);
132. break;
133. default:
134. UseInventory();
135. break;
136. }
137. }
138.
139. void EnemyTurn ()
140. {
141. Attack(combatants[1], combatants[0]);
142. }
143.
144. void Attack (GameObject attacker, GameObject defender)
145. {
146. Stats s1 = attacker.GetComponent<Stats>();
147. Stats s2 = defender.GetComponent<Stats>();
148. int damage = Mathf.FloorToInt((s1[StatTypes.ATK] * 4 - s2[StatTypes.DEF] * 2) *
UnityEngine.Random.Range(0.9f, 1.1f));
149. s2[StatTypes.HP] -= damage;
150. string message = string.Format("{0} hits {1} for {2} damage!", attacker.name,
defender.name, damage);
151. Debug.Log(message);
152. }
153.
154. void UseInventory ()
155. {
156. int rnd = UnityEngine.Random.Range(0, inventory.Count);
157.
158. GameObject item = inventory[rnd];
159. if (item.GetComponent<Consumable>() != null)
160. ConsumeItem(item);
161. else
162. EquipItem(item);
163. }
164.
165. void ConsumeItem (GameObject item)
166. {
167. inventory.Remove(item);
168. // This is dummy code - a user would know how to use an item and who to target
with it
169. StatModifierFeature smf = item.GetComponent<StatModifierFeature>();
170. if (smf.amount > 0)
171. {
172. item.GetComponent<Consumable>().Consume( combatants[0] );
173. Debug.Log("Ah... a potion!");
174. }
175. else
176. {
177. item.GetComponent<Consumable>().Consume( combatants[1] );
178. Debug.Log("Take this you stupid monster!");
179. }
180. }
181.
182. void EquipItem (GameObject item)
183. {
184. Debug.Log("Perhaps this will help...");
185. Equippable toEquip = item.GetComponent<Equippable>();
186. Equipment equipment = combatants[0].GetComponent<Equipment>();
187. equipment.Equip (toEquip, toEquip.defaultSlots );
188. }
189.
190. bool VictoryCheck ()
191. {
192. for (int i = 0; i < 2; ++i)
193. {
194. Stats s = combatants[i].GetComponent<Stats>();
195. if (s[StatTypes.HP] <= 0)
196. return true;
197. }
198. return false;
199. }
200.
201. void LogCombatants ()
202. {
203. Debug.Log("============");
204. for (int i = 0; i < 2; ++i)
205. LogToConsole( combatants[i] );
206. Debug.Log("============");
207. }
208.
209. void LogToConsole (GameObject actor)
210. {
211. Stats s = actor.GetComponent<Stats>();
212. string message = string.Format("Name:{0} HP:{1}/{2} ATK:{3} DEF:{4}", actor.name,
s[StatTypes.HP], s[StatTypes.MHP], s[StatTypes.ATK], s[StatTypes.DEF]);
213. Debug.Log( message );
214. }
215. #endregion
216. }
Summary
In this lesson we created several components which would be required to implement items in our
game. This included components used for buying and selling items, components which give the
items a feature which can modify the game in some way, components which make an item
consumable, and components which make an item equippable. Afterwards we created a quick
demo script which showed a random battle between two combatants and which included the use
of both consumable and equippable items to help the battle play out differently.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
In this lesson we will show how to de ne a variety of jobs and store their data as a project asset.
Although we have done this before (with conversation assets), this time we will be creating
prefabs programmatically. By choosing prefabs over scriptable objects, we have the ability to take
advantage of components such as the features which we introduced in the previous lesson.
Reference
Although I have looked at several games, I’ve been spending the most time looking at Final
Fantasy Tactics Advance (FFTA). You can nd plenty of FAQ’s and guides online which provide a
great way to understand the overall scope of the game. For example, there are around 100
different jobs speci ed by FFTA – each providing a variation on gameplay:
1. Stats (some xed like movement range, others as growth on level up)
2. Items (what categories can be equipped)
3. Abilities (what can be actively used while operating as that job, what can be learned and used
even outside the job)
4. Job Tree (learn enough of one job, and there may be a secondary job which opens up to you)
There is a lot of room for complexity here, but a lot of it is really dependent on your own design.
Initially, our job system will be limited to determining the starting stats and growth rates of
characters, but it shouldn’t be hard to add Job features to control the categories of equippable
items and usable skills in much the same way as we added features to items.
Stats
I still like the idea of being able to change jobs and so I see a great reason to de ne a lot of
different job types. We will begin by creating spreadsheets (.csv) which contain data from which
to programmatically create our project assets. Of course it’s up to you to determine how you want
to organize your data. Do whatever feels the best to you in order for the data to be easy to view
and balance.
Here I have created a simple example with three very generic job-types. I used values somewhere
within the ranges you might see from FFTA but made it my own custom list. Ideally you will do
the same and esh out many, many more jobs, rather than cheating by directly copying data from
Final Fantasy, tempting though it may be. I am starting with two different spreadsheets. The rst
I call JobStartingStats.csv which I have placed in the Settings folder.
1. Name,MHP,MMP,ATK,DEF,MAT,MDF,SPD,MOV,JMP
2. Warrior,43,5,61,89,11,58,100,4,1
3. Wizard,30,25,11,58,61,89,98,3,2
4. Rogue,32,13,51,67,51,67,110,5,3
Note that in this case, MOV and JMP are not merely starting stats. They will actually be
implemented as StatModi erFeature components so that changing to or from a job will allow the
stats to uctuate up and down.
Next I created a spreadsheet called JobGrowthStats.csv which I also placed in the Settings folder.
1. Name,MHP,MMP,ATK,DEF,MAT,MDF,SPD
2. Warrior,8.4,0.8,8.8,9.2,1.1,7.6,1.1
3. Wizard,6.6,2.2,1.1,7.6,8.8,9.2,0.8
4. Rogue,7.6,1.1,5.6,8.8,5.6,8.8,1.8
Here I have used a oating point number for each modi ed stat. However, it is a special
convention I saw while referencing the FAQ’s. The whole number portion of the number is a xed
amount of growth in that stat with every level-up. The fractional portion of the number is a
percent chance that an additional bonus point will be awarded.
For example, using the two spreadsheets above you can deduce that a character which begins the
game as a Warrior will start with 43 hit points. Upon gaining a level this character’s maximum hit
points will grow by a minimum of 8 but there is a 40% chance it could grow by 9.
Job
Now let’s implement the component which holds the data from our spreadsheets, and which
listens to level-ups to actually apply the stat growth, etc. Create a new script called Job in the
Scripts/View Model Component/Actor folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Job : MonoBehaviour
5. {
6. #region Fields / Properties
7. public static readonly StatTypes[] statOrder = new StatTypes[]
8. {
9. StatTypes.MHP,
10. StatTypes.MMP,
11. StatTypes.ATK,
12. StatTypes.DEF,
13. StatTypes.MAT,
14. StatTypes.MDF,
15. StatTypes.SPD
16. };
17.
18. public int[] baseStats = new int[ statOrder.Length ];
19. public float[] growStats = new float[ statOrder.Length ];
20. Stats stats;
21. #endregion
22.
23. #region MonoBehaviour
24. void OnDestroy ()
25. {
26. this.RemoveObserver(OnLvlChangeNotification,
Stats.DidChangeNotification(StatTypes.LVL));
27. }
28. #endregion
29.
30. #region Public
31. public void Employ ()
32. {
33. stats = gameObject.GetComponentInParent<Stats>();
34. this.AddObserver(OnLvlChangeNotification,
Stats.DidChangeNotification(StatTypes.LVL), stats);
35.
36. Feature[] features = GetComponentsInChildren<Feature>();
37. for (int i = 0; i < features.Length; ++i)
38. features[i].Activate(gameObject);
39. }
40.
41. public void UnEmploy ()
42. {
43. Feature[] features = GetComponentsInChildren<Feature>();
44. for (int i = 0; i < features.Length; ++i)
45. features[i].Deactivate();
46.
47. this.RemoveObserver(OnLvlChangeNotification,
Stats.DidChangeNotification(StatTypes.LVL), stats);
48. stats = null;
49. }
50.
51. public void LoadDefaultStats ()
52. {
53. for (int i = 0; i < statOrder.Length; ++i)
54. {
55. StatTypes type = statOrder[i];
56. stats.SetValue(type, baseStats[i], false);
57. }
58.
59. stats.SetValue(StatTypes.HP, stats[StatTypes.MHP], false);
60. stats.SetValue(StatTypes.MP, stats[StatTypes.MMP], false);
61. }
62. #endregion
63.
64. #region Event Handlers
65. protected virtual void OnLvlChangeNotification (object sender, object args)
66. {
67. int oldValue = (int)args;
68. int newValue = stats[StatTypes.LVL];
69.
70. for (int i = oldValue; i < newValue; ++i)
71. LevelUp();
72. }
73. #endregion
74.
75. #region Private
76. void LevelUp ()
77. {
78. for (int i = 0; i < statOrder.Length; ++i)
79. {
80. StatTypes type = statOrder[i];
81. int whole = Mathf.FloorToInt(growStats[i]);
82. float fraction = growStats[i] - whole;
83.
84. int value = stats[type];
85. value += whole;
86. if (UnityEngine.Random.value > (1f - fraction))
87. value++;
88.
89. stats.SetValue(type, value, false);
90. }
91.
92. stats.SetValue(StatTypes.HP, stats[StatTypes.MHP], false);
93. stats.SetValue(StatTypes.MP, stats[StatTypes.MMP], false);
94. }
95. #endregion
96. }
First, I declared an array of StatTypes called statOrder – this will serve as a convenience array to
help me parse data from the spreadsheets we created earlier. It is static because it wont change
from job to job and this way they can all share.
Next I de ned two instance arrays, one for holding the starting stat values, and one for holding
the grow stat values. I was able to init them with a length equal to the length of the statOrder
array from earlier. I might have decided to implement these as a Dictionary, but because Unity
doesn’t serialize Dictionaries I decided to keep it as an Array.
There are three public methods. First is Employ which should be called after instantiating a job
and attaching it to an actor’s hierarchy. In this method, we get a reference to the actor’s Stats
component so that we can listen to targeted level up noti cations as well as apply growth to the
other stats in response. In addition, this method will allow any job-based feature to become
active.
If you want to switch jobs, you should rst UnEmploy any currently active Job. This gives the
script a chance to deactivate its features and unregister from level up noti cations etc.
When creating a unit for the rst time, call LoadDefaultstats so that its stats will be initiated to
playable values.
Job Parser
Now it’s time to create a script which can parse our spreadsheets and create project assets from
them. Create a new script named JobParser in the Editor folder.
1. using UnityEngine;
2. using UnityEditor;
3. using System;
4. using System.IO;
5. using System.Collections;
6.
7. public static class JobParser
8. {
9. [MenuItem("Pre Production/Parse Jobs")]
10. public static void Parse()
11. {
12. CreateDirectories ();
13. ParseStartingStats ();
14. ParseGrowthStats ();
15. AssetDatabase.SaveAssets();
16. AssetDatabase.Refresh();
17. }
18.
19. static void CreateDirectories ()
20. {
21. if (!AssetDatabase.IsValidFolder("Assets/Resources/Jobs"))
22. AssetDatabase.CreateFolder("Assets/Resources", "Jobs");
23. }
24.
25. static void ParseStartingStats ()
26. {
27. string readPath = string.Format("{0}/Settings/JobStartingStats.csv",
Application.dataPath);
28. string[] readText = File.ReadAllLines(readPath);
29. for (int i = 1; i < readText.Length; ++i)
30. PartsStartingStats(readText[i]);
31. }
32.
33. static void PartsStartingStats (string line)
34. {
35. string[] elements = line.Split(',');
36. GameObject obj = GetOrCreate(elements[0]);
37. Job job = obj.GetComponent<Job>();
38. for (int i = 1; i < Job.statOrder.Length + 1; ++i)
39. job.baseStats[i-1] = Convert.ToInt32(elements[i]);
40.
41. StatModifierFeature move = GetFeature (obj, StatTypes.MOV);
42. move.amount = Convert.ToInt32(elements[8]);
43.
44. StatModifierFeature jump = GetFeature (obj, StatTypes.JMP);
45. jump.amount = Convert.ToInt32(elements[9]);
46. }
47.
48. static void ParseGrowthStats ()
49. {
50. string readPath = string.Format("{0}/Settings/JobGrowthStats.csv",
Application.dataPath);
51. string[] readText = File.ReadAllLines(readPath);
52. for (int i = 1; i < readText.Length; ++i)
53. ParseGrowthStats(readText[i]);
54. }
55.
56. static void ParseGrowthStats (string line)
57. {
58. string[] elements = line.Split(',');
59. GameObject obj = GetOrCreate(elements[0]);
60. Job job = obj.GetComponent<Job>();
61. for (int i = 1; i < elements.Length; ++i)
62. job.growStats[i-1] = Convert.ToSingle(elements[i]);
63. }
64.
65. static StatModifierFeature GetFeature (GameObject obj, StatTypes type)
66. {
67. StatModifierFeature[] smf = obj.GetComponents<StatModifierFeature>();
68. for (int i = 0; i < smf.Length; ++i)
69. {
70. if (smf[i].type == type)
71. return smf[i];
72. }
73.
74. StatModifierFeature feature = obj.AddComponent<StatModifierFeature>();
75. feature.type = type;
76. return feature;
77. }
78.
79. static GameObject GetOrCreate (string jobName)
80. {
81. string fullPath = string.Format("Assets/Resources/Jobs/{0}.prefab", jobName);
82. GameObject obj = AssetDatabase.LoadAssetAtPath<GameObject>(fullPath);
83. if (obj == null)
84. obj = Create(fullPath);
85. return obj;
86. }
87.
88. static GameObject Create (string fullPath)
89. {
90. GameObject instance = new GameObject ("temp");
91. instance.AddComponent<Job>();
92. GameObject prefab = PrefabUtility.CreatePrefab( fullPath, instance );
93. GameObject.DestroyImmediate(instance);
94. return prefab;
95. }
96. }
Because this is a pre-production script, I didn’t put a lot of effort into it. There are hard coded
strings, repeated bits of code, etc that could all be cleaned up, but this is not at all re-usable, and
doesn’t need to be performant, so I felt no need to waste time on it. As long as it works, I am
happy.
In order to make this script work its magic, we added a MenuItem tag. As the name implies, this
adds a new entry into Unity’s menu bar. You should see a new entry called “Pre Production” and
under that an option called “Parse Jobs”. Select that and our Job assets will be created in the
project.
You can easily delete and recreate these assets at any time. Because of this, you might choose to
ignore these assets in your source control repository, not that it hurts to keep them. All you truly
need to version is the spreadsheet and parser, not the result of using them together.
It is possible to “listen” for changes to your spreadsheets and have the assets re-created
automatically. See my post on Bestiary Management and Scriptable Objects for an example of
this.
Now that movement range and jump height stats are able to be driven by a job, let’s change our
SpawnTestUnits code to create one of each of the three sample job types. The code to create and
con gure our units is getting a bit long, and is an indication that we will probably need some sort
of factory class soon.
1. void SpawnTestUnits ()
2. {
3. string[] jobs = new string[]{"Rogue", "Warrior", "Wizard"};
4. for (int i = 0; i < jobs.Length; ++i)
5. {
6. GameObject instance = Instantiate(owner.heroPrefab) as GameObject;
7.
8. Stats s = instance.AddComponent<Stats>();
9. s[StatTypes.LVL] = 1;
10.
11. GameObject jobPrefab = Resources.Load<GameObject>( "Jobs/" + jobs[i] );
12. GameObject jobInstance = Instantiate(jobPrefab) as GameObject;
13. jobInstance.transform.SetParent(instance.transform);
14.
15. Job job = jobInstance.GetComponent<Job>();
16. job.Employ();
17. job.LoadDefaultStats();
18.
19. Point p = new Point((int)levelData.tiles[i].x, (int)levelData.tiles[i].z);
20.
21. Unit unit = instance.GetComponent<Unit>();
22. unit.Place(board.GetTile(p));
23. unit.Match();
24.
25. instance.AddComponent<WalkMovement>();
26.
27. units.Add(unit);
28.
29. // Rank rank = instance.AddComponent<Rank>();
30. // rank.Init (10);
31. }
32. }
Movement
We will also need to convert our Movement component into a wrapper much like the Rank
component was. For this, add a eld to store a reference to the Stats component, and then turn
range and jumpHeight into properties as follows:
Have the component get its reference to the Stats component in the Start method:
Demo
Open the main Battle scene and play it. There should be three units as there were before, but we
removed the variation on movement types – everyone walks for now. See that the range of the
units is different depending on the job they began with.
Switch the inspector to Debug mode so that you can see the private stat data in the Stats
component. You should see the values have been set according to the starting stats which we
had speci ed for the job.
Stop the scene and go back to the SpawnTestUnits method of the InitBattleState then
uncomment the two lines where we add the Rank component and init the starting level to 10.
Play the scene a few times and look at the stats of each hero. You should see slight differences in
the stats thanks to the random bonus portion of the Job.
Summary
In this lesson we discussed the various purposes of a Jobs system and looked at references from
the Final Fantasy series. Then we began implementing our game via spreadsheets so that it
would be easy to see and balance the data. We created an editor script which could then parse our
spreadsheets and create prefabs as project assets. Finally, we tied these systems back into the
main game so that our demo units have stats (including movement stats) which are driven by
their job.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
We’ve been modifying stats for a while now, but it’s not very desirable to have to switch the
inspector to debug mode just to see their values. In this lesson, we will go ahead and create a Stat
Panel view, which shows an avatar along with his or her name and a few important stats. There
will actually be two of these panels, one for the currently active (or selected) unit and a second for
the target of an action.
Stat Panel
Create a new script named StatPanel in the Scripts/View Model Component folder. The purpose
of this script is to present a few relevant bits of information about any given unit including a
portrait, name, hit points, magic points, and level.
To function, all you must do is call the Display method and pass along a GameObject (of a Unit) so
that the panel can get whatever components it needs to determine how to update its various
pieces.
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System.Collections;
4.
5. public class StatPanel : MonoBehaviour
6. {
7. public Panel panel;
8. public Sprite allyBackground;
9. public Sprite enemyBackground;
10. public Image background;
11. public Image avatar;
12. public Text nameLabel;
13. public Text hpLabel;
14. public Text mpLabel;
15. public Text lvLabel;
16.
17. public void Display (GameObject obj)
18. {
19. // Temp until I add a component to determine unit alliances
20. background.sprite = UnityEngine.Random.value > 0.5f? enemyBackground :
allyBackground;
21. // avatar.sprite = null; Need a component which provides this data
22. nameLabel.text = obj.name;
23. Stats stats = obj.GetComponent<Stats>();
24. if (stats)
25. {
26. hpLabel.text = string.Format( "HP {0} / {1}", stats[StatTypes.HP],
stats[StatTypes.MHP] );
27. mpLabel.text = string.Format( "MP {0} / {1}", stats[StatTypes.MP],
stats[StatTypes.MMP] );
28. lvLabel.text = string.Format( "LV. {0}", stats[StatTypes.LVL]);
29. }
30. }
31. }
Next create a new script called StatPanelController in the Scripts/Controller folder. This script
manages both the primary and secondary stat panels and controls when they show or hide
themselves. The code here is not very different from what we have already seen, and shares a lot
in common with the ConversationController.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class StatPanelController : MonoBehaviour
5. {
6. #region Const
7. const string ShowKey = "Show";
8. const string HideKey = "Hide";
9. #endregion
10.
11. #region Fields
12. [SerializeField] StatPanel primaryPanel;
13. [SerializeField] StatPanel secondaryPanel;
14.
15. Tweener primaryTransition;
16. Tweener secondaryTransition;
17. #endregion
18.
19. #region MonoBehaviour
20. void Start ()
21. {
22. if (primaryPanel.panel.CurrentPosition == null)
23. primaryPanel.panel.SetPosition(HideKey, false);
24. if (secondaryPanel.panel.CurrentPosition == null)
25. secondaryPanel.panel.SetPosition(HideKey, false);
26. }
27. #endregion
28.
29. #region Public
30. public void ShowPrimary (GameObject obj)
31. {
32. primaryPanel.Display(obj);
33. MovePanel(primaryPanel, ShowKey, ref primaryTransition);
34. }
35.
36. public void HidePrimary ()
37. {
38. MovePanel(primaryPanel, HideKey, ref primaryTransition);
39. }
40.
41. public void ShowSecondary (GameObject obj)
42. {
43. secondaryPanel.Display(obj);
44. MovePanel(secondaryPanel, ShowKey, ref secondaryTransition);
45. }
46.
47. public void HideSecondary ()
48. {
49. MovePanel(secondaryPanel, HideKey, ref secondaryTransition);
50. }
51. #endregion
52.
53. #region Private
54. void MovePanel (StatPanel obj, string pos, ref Tweener t)
55. {
56. Panel.Position target = obj.panel[pos];
57. if (obj.panel.CurrentPosition != target)
58. {
59. if (t != null && t.easingControl != null)
60. t.easingControl.Stop();
61. t = obj.panel.SetPosition(pos, true);
62. t.easingControl.duration = 0.5f;
63. t.easingControl.equation = EasingEquations.EaseOutQuad;
64. }
65. }
66. #endregion
67. }
You can begin by opening the Battle scene. We will be creating a new prefab which can manage
the display of a primary and secondary stat panel. You could think of it as an attacker and
defender panel, although that fails to encompass the full range of scenarios, such as when one
ally takes an action to help another ally. The nal result should look like the following picture:
There are many different scripts which will be slightly modi ed in order to implement our new
system. Each entry follows:
BattleController
We will add a reference to our StatPanelController in the BattleController. Make sure to update the
reference in the scene after modifying the script. Add the following line:
Let’s do a convenience property wrapper in the base BattleState class so that all subclasses can
directly reference it:
I will also add another method which makes it easy to get a Unit from a board position:
Then, I will add some additional methods which can tell the StatPanelController to refresh an
appropriate panel (show or hide it) based on whether or not the indicated location has a unit or
not.
There are a lot of possibile use cases for a turn, such as entering from one state to the next
linearly, or entering and then canceling back out before trying something different. Trying to only
show a panel or hide a panel when necessary can get kind of confusing given all the use cases,
and can be even more confusing if you insert additional states or modify them in the future – one
seemingly innocent modifcation could lead to a weird “bug” where the panel stayed visible when
it shouldn’t or hid when it should have stayed visible.
In order to keep things as simple as possible, I handle this issue by making every state
completely responsible for itself. If a state shows a panel, it needs to hide the panel before exiting.
If the next state also needs the panel, it is responsible for making sure it appears. It may seem like
extra work, but it should help your sanity level in the end, and could actually help reduce a bunch
of extra code that checks previous states, etc. in order to manage weird use cases.
These states will merely need to keep the stat panel visible, with the currently active unit
displayed:
These states need to show whatever unit is highlighted by the cursor, regardless of whose turn it
currently is. Add a call to refresh the stat panel in Enter and OnMove (after selecting the new tile),
and add a call to hide the stat panel in Exit.
This state is similar to the last two. In the ChangeCurrentUnit method (which is called from
Enter) add a call to refresh the stat panel just after the turn has changed to the next unit. Hide the
stat panel in Exit as we have done before.
Demo
Run the Battle scene and then cancel out of the CommandSelectionState so you can explore the
map. Move the cursor over the different units on the board and see that the stat panel appears
when a unit is selected (and shows that unit’s stats) and hides when no unit is selected. Complete
a turn and note that the stat panel shows the currently active unit in the other states.
Note that we didn’t get around to implementing the secondary stat panel because we havent
added skills and actions yet. However, the implementation for displaying it will be almost
identical to what we have done here. You simply tell the controller to show the secondary panel
instead of the primary one.
Summary
In this lesson we implemented the UI for showing unit stats. Nothing here was terribly different
from material we have already done in the past – it is just more complete now. Big projects like
this take a lot of time!
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
Abilities such as attacking or using magic are part of a large and complex system. In this lesson
we will take the rst step toward this implementation by providing several classes that can be
used to control the range of any particular one.
Every ability must have a component which de nes its range. The ranges for some abilities
might be merely distance based, but others will take on some sort of pattern, such as a line or a
cone. When an ability is selected, tiles will also be highlighted according to the distance and or
pattern speci ed by this component. In this way an ability can show the valid target(s) which are
within its reach.
Abstract Base Class
Begin by creating the abstract base class named AbilityRange and organize it in the following
path Scripts/View Model Component/Ability/Range. All of the concrete subclasses which we
create in this lesson should also be saved there.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public abstract class AbilityRange : MonoBehaviour
6. {
7. public int horizontal = 1;
8. public int vertical = int.MaxValue;
9. public virtual bool directionOriented { get { return false; }}
10. protected Unit unit { get { return GetComponentInParent<Unit>(); }}
11.
12. public abstract List<Tile> GetTilesInRange (Board board);
13. }
This class is super simple – only two elds, two properties, and an abstract method signature.
The rst eld, horizontal, de nes the number of tiles away from the user which can be reached
(think X & Z axis). The second eld, vertical, de nes the height difference between the user’s tile
and the target tiles which are within reach (think Y axis).
The rst property, directionOriented should be true when the range is a pattern like a cone or line.
When it is true, we will use the movement input buttons to change the user’s facing direction so
that the effected tiles change. When the directionOriented property is false, you may move the
cursor to select tiles within the highlighted range.
To illustrate the difference, consider a black mage ability like Fire from Final Fantasy Tactics
Advance. When you select the spell, you have a range of tiles highlighted around the caster. You
then select a location within the range to actually cast the spell by moving the cursor. In contrast,
a Dragoon has Fire Breath which highlights tiles in a cone in front of the unit in the same
direction the unit faces. When you use the movement input, you simply change the direction the
dragoon faces and in this way are able to target different sets of tiles.
The nal property, unit, crawls up through the hierarchy chain to nd the Unit component. Some
ranges will need to know the current user’s location in order to determine what tiles are
reachable.
Every concrete subclass will be required to implement the GetTilesInRange method, which will
return a List of Tile(s) which can be reached by the selected ability. This is how we will know
what tiles to highlight on the board, and in the future will be used to determine if there are targets
within reach.
The frist concrete subclass we will make will probably be one of the most frequently used types of
ranges. The use case for this type of class is when an ability always has the same pre-speci ed
amount of range, i.e. Spell “X” has “Y” reach in terms of tiles. This class would be similar to a class
where the range is based off of the range of an equipped weapon, except that the horizontal and
vertical elds would need to be adjusted rst to match.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ConstantAbilityRange : AbilityRange
6. {
7. public override List<Tile> GetTilesInRange (Board board)
8. {
9. return board.Search(unit.tile, ExpandSearch);
10. }
11.
12. bool ExpandSearch (Tile from, Tile to)
13. {
14. return (from.distance + 1) <= horizontal && Mathf.Abs(to.height -
unit.tile.height) <= vertical;
15. }
16. }
The code here should look familiar – we used something very similar when we implemented the
Movement components. I am using the board’s search ability to retrieve the list of tiles within
range of the user’s tile, and I use a delegate which only allows the search to continue as long as
we are within the range speci ed by the horizontal and vertical elds.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class SelfAbilityRange : AbilityRange
6. {
7. public override List<Tile> GetTilesInRange (Board board)
8. {
9. List<Tile> retValue = new List<Tile>(1);
10. retValue.Add(unit.tile);
11. return retValue;
12. }
13. }
If you want to reach any or all targets no matter where they are on the board, then you will need a
new type of range. Add a new script called In niteAbilityRange. Like before, we wont need to do a
search here, we can simply return a list which directly copies all of the board’s tiles.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class InfiniteAbilityRange : AbilityRange
6. {
7. public override List<Tile> GetTilesInRange (Board board)
8. {
9. return new List<Tile>(board.tiles.Values);
10. }
11. }
All of the range types until now have been ones which were not dependent upon the facing
direction of the user. Now we need a special case where the direction does matter. We will be
selecting tiles which extend in a cone shape from the location of the user out.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ConeAbilityRange : AbilityRange
6. {
7. public override bool directionOriented { get { return true; }}
8.
9. public override List<Tile> GetTilesInRange (Board board)
10. {
11. Point pos = unit.tile.pos;
12. List<Tile> retValue = new List<Tile>();
13. int dir = (unit.dir == Directions.North || unit.dir == Directions.East) ? 1 : -1;
14. int lateral = 1;
15.
16. if (unit.dir == Directions.North || unit.dir == Directions.South)
17. {
18. for (int y = 1; y <= horizontal; ++y)
19. {
20. int min = -(lateral / 2);
21. int max = (lateral / 2);
22. for (int x = min; x <= max; ++x)
23. {
24. Point next = new Point(pos.x + x, pos.y + (y * dir));
25. Tile tile = board.GetTile(next);
26. if (ValidTile(tile))
27. retValue.Add(tile);
28. }
29. lateral += 2;
30. }
31. }
32. else
33. {
34. for (int x = 1; x <= horizontal; ++x)
35. {
36. int min = -(lateral / 2);
37. int max = (lateral / 2);
38. for (int y = min; y <= max; ++y)
39. {
40. Point next = new Point(pos.x + (x * dir), pos.y + y);
41. Tile tile = board.GetTile(next);
42. if (ValidTile(tile))
43. retValue.Add(tile);
44. }
45. lateral += 2;
46. }
47. }
48.
49. return retValue;
50. }
51.
52. bool ValidTile (Tile t)
53. {
54. return t != null && Mathf.Abs(t.height - unit.tile.height) <= vertical;
55. }
56. }
This code is a bit longer, but hopefully not too hard to understand. Basically, I differentiate
between a North / South axis and an East / West axis. The loops are swapped on iterating over “X”
or “Y” rst based on which axis we are facing.
For example, if the unit is facing north, then the outer loop will increment on “Y” for as many tiles
as is speci ed by the horizontal eld. Then an inner loop on “X” beginning with a lateral offset of 1
and incrementing by 2 (so you get odd numbers like 1, 3, 5 etc) is used to determine the sideways
spread of the cone at each step away from the user.
It might be a little confusing why I iterate over the “Y” with the horizontal reach. The axis is
actually treated as “Z”, but I use “Y” because the Point class used to represent a tile’s position uses
the elds “X” and “Y”.
The nal sample for this lesson is another direction oriented pattern – a line starting from the
user and extending to the edge of the board. Before I can show you the implementation though,
we need to add a bit of code to the Board class so that it can tell us its bounding area. First add the
following Fields / Properties to the Board script:
Also in the Board script, we will initialize the min and max backers at the beginning of the Load
method, and then update them with “better” values as we actually load each tile:
Now when the board is loaded we can quickly know the minimum and maximum positions that a
tile could possibily appear within.
Go ahead and create another script named LineAbilityRange and use the following:
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class LineAbilityRange : AbilityRange
6. {
7. public override bool directionOriented { get { return true; }}
8.
9. public override List<Tile> GetTilesInRange (Board board)
10. {
11. Point startPos = unit.tile.pos;
12. Point endPos;
13. List<Tile> retValue = new List<Tile>();
14.
15. switch (unit.dir)
16. {
17. case Directions.North:
18. endPos = new Point(startPos.x, board.max.y);
19. break;
20. case Directions.East:
21. endPos = new Point(board.max.x, startPos.y);
22. break;
23. case Directions.South:
24. endPos = new Point(startPos.x, board.min.y);
25. break;
26. default: // West
27. endPos = new Point(board.min.x, startPos.y);
28. break;
29. }
30.
31. while (startPos != endPos)
32. {
33. if (startPos.x < endPos.x) startPos.x++;
34. else if (startPos.x > endPos.x) startPos.x--;
35.
36. if (startPos.y < endPos.y) startPos.y++;
37. else if (startPos.y > endPos.y) startPos.y--;
38.
39. Tile t = board.GetTile(startPos);
40. if (t != null && Mathf.Abs(t.height - unit.tile.height) <= vertical)
41. retValue.Add(t);
42. }
43.
44. return retValue;
45. }
46. }
Battle States
We have created all of the Range types I will make for this lesson, but I want to go ahead and plug
them into the game so you can see everything working. To accomplish this we will need to
modify one of our Battle States and add another new one.
In addition we will add one more extension method to DirectionsExtensions shown below:
and we will add a reference in the Turn script which lets us know which ability has been selected
through the Ability Menu.
Add a new script called AbilityTargetState in the Scripts/Controller/Battle States folder. This state
should become active after selecting “Attack” from the Ability Menu. It will nd an AbilityRange
component on the user (we will simply attach one to the hero prefab as a demonstration) and
then highlight the tiles it can reach.
If the range component is directional, then using movement input will cause the unit to change
facing directions and the selected tiles will be changed. Otherwise, it will allow you to move the
cursor around the board so you can select tiles from within the area. In addition, whoever the
cursor highlights will be displayed in the secondary stat panel.
1. using System.Collections;
2. using System.Collections.Generic;
3.
4. public class AbilityTargetState : BattleState
5. {
6. List<Tile> tiles;
7. AbilityRange ar;
8.
9. public override void Enter ()
10. {
11. base.Enter ();
12. ar = turn.ability.GetComponent<AbilityRange>();
13. SelectTiles ();
14. statPanelController.ShowPrimary(turn.actor.gameObject);
15. if (ar.directionOriented)
16. RefreshSecondaryStatPanel(pos);
17. }
18.
19. public override void Exit ()
20. {
21. base.Exit ();
22. board.DeSelectTiles(tiles);
23. statPanelController.HidePrimary();
24. statPanelController.HideSecondary();
25. }
26.
27. protected override void OnMove (object sender, InfoEventArgs<Point> e)
28. {
29. if (ar.directionOriented)
30. {
31. ChangeDirection(e.info);
32. }
33. else
34. {
35. SelectTile(e.info + pos);
36. RefreshSecondaryStatPanel(pos);
37. }
38. }
39.
40. protected override void OnFire (object sender, InfoEventArgs<int> e)
41. {
42. if (e.info == 0)
43. {
44. turn.hasUnitActed = true;
45. if (turn.hasUnitMoved)
46. turn.lockMove = true;
47. owner.ChangeState<CommandSelectionState>();
48. }
49. else
50. {
51. owner.ChangeState<CategorySelectionState>();
52. }
53. }
54.
55. void ChangeDirection (Point p)
56. {
57. Directions dir = p.GetDirection();
58. if (turn.actor.dir != dir)
59. {
60. board.DeSelectTiles(tiles);
61. turn.actor.dir = dir;
62. turn.actor.Match();
63. SelectTiles ();
64. }
65. }
66.
67. void SelectTiles ()
68. {
69. tiles = ar.GetTilesInRange(board);
70. board.SelectTiles(tiles);
71. }
72. }
We moved the code from the Attack method into the con rm branch of OnFire in
AbilityTargetState – another step closer to its nal resting place, but we have a few more states in
between to add. In the meantime, we now need to use the old Attack method to call the new state:
1. void Attack ()
2. {
3. turn.ability = turn.actor.GetComponentInChildren<AbilityRange>().gameObject;
4. owner.ChangeState<AbilityTargetState>();
5. }
Note that this is still placeholder code. The attack ability should be de ned in another script
somewhere, like the job component (in case the type of attack is different per job type) or ideally
in some sort of a skill-set manager component.
Demo
Drag the hero prefab into the scene. Add a child gameobject called “Attack” and add one of the
range components to it. Apply the changes to the prefab, delete the instance, and then play the
scene. When you choose “Attack” from the Ability menu you should see tiles light up on the board.
Repeat this process for each of the different types of ranges. Experiment to make sure you can
change directions on the line and cone patterns, but can move the cursor in the other types.
What’s Next?
In the next lesson we will add the next step of the process – the Area of Effect. This is somewhat
similar to Range, but serves a slightly differnet purpose. For example, imagine two different
actions, one with an archer who has a long range and only a single tile target, vs another with a
black mage who has a medium range with a splash area target for his spell. In both of these cases
you could begin the process by using the ConstantRangeAbility but after moving the cursor and
con rming its placement, another state will appear showing the “real” target area which must
again be con rmed. It may sound repetitive, but during this state we have the ability to show a
variety of other useful information to the player such as the chance of the ability hitting, and the
stats of the attacked target etc.
Summary
In this lesson we began the rst of several posts which are geared to the implementation of using
abilities. We implemented a variety of range types which can be attached to an ability, and then
added a new battle state so we could see that it selected tiles correctly.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
To fully select targets for an ability we need a few more steps. We began with specifying a Range.
In this lesson we will implent a variety of Area Of Effect components which are applied based off
the selection from the range. In addition, each ability may have multiple “effects” – each of which
may specify a unique set of valid targets. I will provide a couple of implementations for these as
well, so that we can have a completed targeting selection loop in place.
Area Of Effect
Some abilities have a range with a single target, like an archer. Some abilities have a range with a
subrange target, like the blast radius of a magic spell. This difference in targeting area is what I
will refer to as the Area of Effect. All abilities will need to have this type of component, just like
they needed a Range component. By mixing and matching the two components on your
GameObjects you can have a nice variety of ways to select the target(s) for your abilities.
Let’s begin by creating the abstract base class. Create a new script called AbilityArea and place it
in Scripts/View Model Component/Ability/Area Of Effect.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public abstract class AbilityArea : MonoBehaviour
6. {
7. public abstract List<Tile> GetTilesInArea (Board board, Point pos);
8. }
Much like the Range component, this script returns a list of tiles in its area. These are tiles which
need to be highlighted and shown to the player as candidate locations for effect application. Note
that in addition to the Board reference, we must also pass a Point parameter to the method. This
is used to indicate a selected location within a range from which to determine what tiles to grab.
Our rst concrete subclass is called UnitAbilityArea and it simply returns whatever Tile exists at
the indicated position. This could be used for implementing the attack ability of an archer.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class UnitAbilityArea : AbilityArea
6. {
7. public override List<Tile> GetTilesInArea (Board board, Point pos)
8. {
9. List<Tile> retValue = new List<Tile>();
10. Tile tile = board.GetTile(pos);
11. if (tile != null)
12. retValue.Add(tile);
13. return retValue;
14. }
15. }
When you want to target an area of tiles around the cursor’s position, you will use our next
subclass, SpecifyAbilityArea. For example, this would be used to implement a black mage’s re
spell which can also hit tiles adjacent to the targeted location.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class SpecifyAbilityArea : AbilityArea
6. {
7. public int horizontal;
8. public int vertical;
9. Tile tile;
10.
11. public override List<Tile> GetTilesInArea (Board board, Point pos)
12. {
13. tile = board.GetTile(pos);
14. return board.Search(tile, ExpandSearch);
15. }
16.
17. bool ExpandSearch (Tile from, Tile to)
18. {
19. return (from.distance + 1) <= horizontal && Mathf.Abs(to.height - tile.height) <=
vertical;
20. }
21. }
Our next concrete subclass is called FullAbilityArea, and as the name implies, every tile that is
highlighted by an ability’s range is also a potential target for this area of effect. This could be used
for a dragoon’s re breath attack (again I am referencing Final Fantasy Tactics Advance here).
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class FullAbilityArea : AbilityArea
6. {
7. public override List<Tile> GetTilesInArea (Board board, Point pos)
8. {
9. AbilityRange ar = GetComponent<AbilityRange>();
10. return ar.GetTilesInRange(board);
11. }
12. }
Effect Target
As I brie y mentioned in the introduction, an ability can have more than one effect. For example,
a Cure spell which normally restores hit points to units might have a secondary effect of
damaging the undead. So in this example, the rst effect of the ability would only target living
units, and the second effect would only target undead units. Even if the ability only has a single
effect, it still may require special targeting. The ability to determine what is and is not a valid
target will be another component.
Create an abstract base class called AbilityEffectTarget and place it in Scripts/View Model
Component/Ability/Effect Target. Use the following implementation:
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class AbilityEffectTarget : MonoBehaviour
5. {
6. public abstract bool IsTarget (Tile tile);
7. }
Couldn’t be much easier right? The only thing this component does is to determine whether or
not the effect applies to whatever may or may not be located at the speci ed board tile.
Let’s create our rst concrete subclass called DefaultAbilityEffectTarget. Most ability effects will
probably use this – it simply requires that there be something on the tile, and that the something
which is there has hit points. Note that it doesn’t necessarily have to be a normal unit – you may
or may not wish to include that requirement.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class DefaultAbilityEffectTarget : AbilityEffectTarget
5. {
6. public override bool IsTarget (Tile tile)
7. {
8. if (tile == null || tile.content == null)
9. return false;
10.
11. Stats s = tile.content.GetComponent<Stats>();
12. return s != null && s[StatTypes.HP] > 0;
13. }
14. }
Just for a quick bit of variety I decided to create one more concrete targeter. Add another script
called KOdAbilityEffectTarget. This time we are looking for an entity with no hit points. For
example, a resurrection skill might require this target type.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class KOdAbilityEffectTarget : AbilityEffectTarget
5. {
6. public override bool IsTarget (Tile tile)
7. {
8. if (tile == null || tile.content == null)
9. return false;
10.
11. Stats s = tile.content.GetComponent<Stats>();
12. return s != null && s[StatTypes.HP] <= 0;
13. }
14. }
Turn
Now that we will truly be selecting the targets of an ability, we will store them in the Turn object,
so that they are available between multiple battle states. Add a eld for a List of Tile called
targets:
Battle States
We have a few more components to play with, so let’s plug them into the game and see how they
work. As is normally the case, we will need to both modify and add new States. I will add a few
extra states to help complete the loop of actually using an ability and ending a turn by choosing a
facing direction.
Once a user has selected a direction for their range, or a location within their range (depending
upon the range type of the ability), we will enter a new state called Con rmAbilityTargetState.
This state will highlight a (potentially) new set of tiles which shows the area that can be effected
by the ability using the current cursor position or facing angle of the active unit.
When the state enters, we will look thru the effect targeting components attached to the ability
and determine what, if any, valid targets fall within the selected area. If we have a target, then we
will show it in the secondary stat panel. If we have more than one target, you can use the
movement input to cycle through which of the targets is displayed in the stat panel.
Note that in the future, when we determine things like the predicted damage of an ability, hit
chance, etc. we will use this state to show this information in the UI to help the player make more
informed decisions.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ConfirmAbilityTargetState : BattleState
6. {
7. List<Tile> tiles;
8. AbilityArea aa;
9. int index = 0;
10.
11. public override void Enter ()
12. {
13. base.Enter ();
14. aa = turn.ability.GetComponent<AbilityArea>();
15. tiles = aa.GetTilesInArea(board, pos);
16. board.SelectTiles(tiles);
17. FindTargets();
18. RefreshPrimaryStatPanel(turn.actor.tile.pos);
19. SetTarget(0);
20. }
21.
22. public override void Exit ()
23. {
24. base.Exit ();
25. board.DeSelectTiles(tiles);
26. statPanelController.HidePrimary();
27. statPanelController.HideSecondary();
28. }
29.
30. protected override void OnMove (object sender, InfoEventArgs<Point> e)
31. {
32. if (e.info.y > 0 || e.info.x > 0)
33. SetTarget(index + 1);
34. else
35. SetTarget(index - 1);
36. }
37.
38. protected override void OnFire (object sender, InfoEventArgs<int> e)
39. {
40. if (e.info == 0)
41. {
42. if (turn.targets.Count > 0)
43. {
44. owner.ChangeState<PerformAbilityState>();
45. }
46. }
47. else
48. owner.ChangeState<AbilityTargetState>();
49. }
50.
51. void FindTargets ()
52. {
53. turn.targets = new List<Tile>();
54. AbilityEffectTarget[] targeters =
turn.ability.GetComponentsInChildren<AbilityEffectTarget>();
55. for (int i = 0; i < tiles.Count; ++i)
56. if (IsTarget(tiles[i], targeters))
57. turn.targets.Add(tiles[i]);
58. }
59.
60. bool IsTarget (Tile tile, AbilityEffectTarget[] list)
61. {
62. for (int i = 0; i < list.Length; ++i)
63. if (list[i].IsTarget(tile))
64. return true;
65.
66. return false;
67. }
68.
69. void SetTarget (int target)
70. {
71. index = target;
72. if (index < 0)
73. index = turn.targets.Count - 1;
74. if (index >= turn.targets.Count)
75. index = 0;
76. if (turn.targets.Count > 0)
77. RefreshSecondaryStatPanel(turn.targets[index].pos);
78. }
79. }
In order to reach our new state, we need to modify what happens when you use the con rm input
during the AbilityTargetState script. Instead of pretending like we just completed an attack, we
will rst verify that our selection is valid, and if so, enter the Con rmAbilityTargetState which we
just created.
After having selected an ability and a target to apply the ability to, it is time to actually take
action. There are a ton of ways this could be implemented, though Mechanim would probably be
used since we are focusing on Unity. Ultimately we need some way to have events tied to
animation so that we can do something like swing a sword, and then at a speci c point in the
animation, play a sound and apply the effect of the ability – which in that case would be to
reduce the target’s hit points.
This state is sort of a placeholder – I left comments showing potential places for logic to appear. I
also added a TemporaryAttackExample method. As the name hopefully implies, this is
placeholder code. In a more complete project, I would not directly do the work of an Ability’s
Effect in this state. Instead, there would be another class per effect, very much like we did with
the Feature component of an item. The real implementation would probably loop through the
effects and targets and attempt to apply the effect on each target.
When the animation and application of the ability are complete, we continue onto the next
relevant state.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class PerformAbilityState : BattleState
5. {
6. public override void Enter ()
7. {
8. base.Enter ();
9. turn.hasUnitActed = true;
10. if (turn.hasUnitMoved)
11. turn.lockMove = true;
12. StartCoroutine(Animate());
13. }
14.
15. IEnumerator Animate ()
16. {
17. // TODO play animations, etc
18. yield return null;
19. // TODO apply ability effect, etc
20. TemporaryAttackExample();
21.
22. if (turn.hasUnitMoved)
23. owner.ChangeState<EndFacingState>();
24. else
25. owner.ChangeState<CommandSelectionState>();
26. }
27.
28. void TemporaryAttackExample ()
29. {
30. for (int i = 0; i < turn.targets.Count; ++i)
31. {
32. GameObject obj = turn.targets[i].content;
33. Stats stats = obj != null ? obj.GetComponentInChildren<Stats>() : null;
34. if (stats != null)
35. {
36. stats[StatTypes.HP] -= 50;
37. if (stats[StatTypes.HP] <= 0)
38. Debug.Log("KO'd Uni!", obj);
39. }
40. }
41. }
42. }
This state is used to wrap up the end of a turn, instead of simply choosing “Wait” from the ability
menu. It allows you a chance to decide which direction you want a unit to face before giving
control to the next unit.
In the future we will add some UI here of arrows over the active units head which indicate what
you are supposed to be doing.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class EndFacingState : BattleState
5. {
6. Directions startDir;
7.
8. public override void Enter ()
9. {
10. base.Enter ();
11. startDir = turn.actor.dir;
12. SelectTile(turn.actor.tile.pos);
13. }
14.
15. protected override void OnMove (object sender, InfoEventArgs<Point> e)
16. {
17. turn.actor.dir = e.info.GetDirection();
18. turn.actor.Match();
19. }
20.
21. protected override void OnFire (object sender, InfoEventArgs<int> e)
22. {
23. switch (e.info)
24. {
25. case 0:
26. owner.ChangeState<SelectUnitState>();
27. break;
28. case 1:
29. turn.actor.dir = startDir;
30. turn.actor.Match();
31. owner.ChangeState<CommandSelectionState>();
32. break;
33. }
34. }
35. }
If a player chooses Wait from the Ability Menu, let’s go to the EndFacingState instead of
immediately ending the turn. Not only does it allow them to face a different direction, but it
provides an opportunity for them to “cancel” back out just in case they had chosen to wait by
accident.
Expand the Hero prefab’s hierarchy in the project pane and select the Attack object from before.
Experiment by adding different combinations and settings for the different ranges, areas, and
effect targets (one of each). Then play the scene, and notice that after attacking another unit(s)
their hit points will be reduced. Note that you can even change components while the game is
playing, so it would be easy to give each of the Units on the board a different con guration.
Summary
In this lesson we wrapped up the selection process for an ability. A user can see how far an ability
will reach (range), what area the ability will affect (area of effect), and who within that area will be
targeted (effect target). I demonstrated how keeping each of these as separate components
makes it easy to have a large number of con gurations and adds great diversity to your game.
We also added a few extra battle states to help the process feel more complete. We will need
several more UI pieces, and will need to actually create the abilities and their effects themselves,
so stay tuned – I plan to get there eventually!
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
As it is now, turns are completely linear as if our units were all standing in line. In a tactics battle,
things are a lot more complex. Units with a higher speed stat should be able to act more quickly
in general, but other exceptions to the rule might exist. Final Fantasy Tactics for example would
include status effects such as Haste, Slow, or even Stop. In this lesson we will create a new class
to determine when each unit on the board gets to take a turn while keeping things exible
enough to account for all of these types of scenarios.
StatTypes
I decided to add another entry to our StatTypes enum called CTR which stands for “counter” – it is
a stat that is used to determine when a unit gets to act. Except in special conditions, it will
increment according to the SPD stat and when it passes a certain threshold value, the Unit will be
eligible for a turn. Simply taking a turn will knock this number back down, but choosing to take
an action and or move will knock it down further still.
Open the StatTypes script and add the following entry inside the enum (just before Count):
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class TurnOrderController : MonoBehaviour
6. {
7. #region Constants
8. const int turnActivation = 1000;
9. const int turnCost = 500;
10. const int moveCost = 300;
11. const int actionCost = 200;
12. #endregion
13.
14. #region Notifications
15. public const string RoundBeganNotification = "TurnOrderController.roundBegan";
16. public const string TurnCheckNotification = "TurnOrderController.turnCheck";
17. public const string TurnCompletedNotification = "TurnOrderController.turnCompleted";
18. public const string RoundEndedNotification = "TurnOrderController.roundEnded";
19. #endregion
20.
21. #region Public
22. public IEnumerator Round ()
23. {
24. BattleController bc = GetComponent<BattleController>();;
25. while (true)
26. {
27. this.PostNotification(RoundBeganNotification);
28.
29. List<Unit> units = new List<Unit>( bc.units );
30. for (int i = 0; i < units.Count; ++i)
31. {
32. Stats s = units[i].GetComponent<Stats>();
33. s[StatTypes.CTR] += s[StatTypes.SPD];
34. }
35.
36. units.Sort( (a,b) => GetCounter(a).CompareTo(GetCounter(b)) );
37.
38. for (int i = units.Count - 1; i >= 0; --i)
39. {
40. if (CanTakeTurn(units[i]))
41. {
42. bc.turn.Change(units[i]);
43. yield return units[i];
44.
45. int cost = turnCost;
46. if (bc.turn.hasUnitMoved)
47. cost += moveCost;
48. if (bc.turn.hasUnitActed)
49. cost += actionCost;
50.
51. Stats s = units[i].GetComponent<Stats>();
52. s.SetValue(StatTypes.CTR, s[StatTypes.CTR] - cost, false);
53.
54. units[i].PostNotification(TurnCompletedNotification);
55. }
56. }
57.
58. this.PostNotification(RoundEndedNotification);
59. }
60. }
61. #endregion
62.
63. #region Private
64. bool CanTakeTurn (Unit target)
65. {
66. BaseException exc = new BaseException( GetCounter(target) >= turnActivation );
67. target.PostNotification( TurnCheckNotification, exc );
68. return exc.toggle;
69. }
70.
71. int GetCounter (Unit target)
72. {
73. return target.GetComponent<Stats>()[StatTypes.CTR];
74. }
75. #endregion
76. }
First, I added a few constants. It’s never a good idea to hard-code values in your code. These are
sometimes referred to as “mystery numbers” because you don’t always understand the intent
behind the number. When you name the value with a constant, the intent is much more obvious.
In addition, should you ever decide to alter the values, you can do it in one place, instead of
scattered throughout your script.
turnActivation – this is the minimum threshold value that the CTR stat must reach before a
unit is eligible for a turn (except under special conditions).
turnCost – this is the minimum debit to the CTR stat when a unit takes its turn.
moveCost – this is an optional debit to the CTR stat, if the unit chooses not to move, it might
get another turn more quickly.
actionCost – this is an optional debit to the CTR stat, if the unit chooses not to take an action
(like attack), it might get another turn more quickly.
Next we have a few noti cations. These are also de ned as constants. Note that I used the class
as a pre x within the string itself. This practice will help to avoid “collisions” in your noti cation
names. It is entirely possible for example that I would have another “roundBegan” noti cation in
some other class, but with the pre x of the class in the noti cation itself, I will be able to avoid
accidental noti cation handling.
The real meat of this class is the Round method. Note that it is an IEnumerator but we will not be
using Unity’s StartCoroutine to use it. Instead, we will use it natively and chose to Step through
the code based on actually completing turns through our Battle States. We have done this before,
such as with the ConversationController, but if you haven’t quite mastered the idea yet, hopefully
seeing it again will help.
I am using an “in nite loop” here by using while (true). Be careful with these because if there is
no “pause” point your game will “freeze”. In our case, there is always a “pause” with each unit’s
turn, and if we code correctly, a game over state will be recognized before we ever run into a
situation where there are no units which can take a turn.
Each cycle of the while loop is considered a complete “Round” – where a round allows all units
which are allowed to take a turn to actually take their turn. At the very beginning of this round I
post a noti cation letting our game know that a new round has begun. You could use this
noti cation for any number of things (or nothing at all) but I will probably use it as an opportunity
to decrement timers on status effects. For example “Poison” could last for “X” number of rounds –
each round it would decrement a “duration” variable and if the value reaches zero could remove
itself.
Next, I create a copy of the BattleController’s units list. I do this because it is generally a bad idea
to iterate over a list which has the chance of being modi ed while you are iterating over it. Plus I
want to be able to sort the list, but I don’t want the original list to be modi ed (the order there isn’t
important now, but in the future who knows?).
Using our copied list of units, we iterate over each unit and increment the CTR stat by the SPD
stat. Note that because we are using the indexer the stat component will allow the value to have
exceptions applied. Any class can listen to the corresponding WillChange noti cation, grab the
ValueChangeException argument and make changes. For example I could append a
MultValueModi er – a multiplier of “2” could implement a Haste status effect or a multiplier of
“0.5” could implement a Slow status effect. You can also simply FlipToggle on the exception and
have the implementation for Stop.
Now that the nal CTR values for all of the units are known, we sort the list of units according to
that stat. Looping through the list again from highest to lowest, we allow any unit which can take
a turn, to actually take a turn. When we check whether or not a unit can take a turn, we post
another noti cation which allows our game to make some exceptions. By default, the exception
is based on whether or not the unit has a high enough CTR stat to overcome the threshold.
However, there are reasons we might not want to allow the unit to take a turn – such as if the unit
has been defeated. Alternatively, you might come up with reasons that a unit which normally
wouldn’t be able to take a turn, now can take a turn. This noti cation is your opportunity to make
those sorts of changes.
When we nd a unit which can act, we assign it to the BattleController’s Turn object. Then we
“pause” execution of this loop using the yield statement which will allow control to be passed
back to our BattleStates (and other code). Whenever we “continue” execution (through another
BattleState) it will pick up exactly where it left off. We can check what has been done on that turn
and reduce the CTR stat accordingly. I also re a noti cation once the nal costs have been
applied and the turn is truly at its most “complete” point, though at the moment I don’t know what
action I will take here – if any.
After giving all of the units a chanace to take a turn, we post a round complete noti cation and
the whole process will then repeat itself.
Battle Controller
Because the BattleController maintains references to pretty much anything I could want, I will
add a reference there for our TurnOrderController‘s round enumerator.
To initialize the round enumerator we will go ahead and use our InitBattleState. Add the following
line to the Init method:
1. owner.round = owner.gameObject.AddComponent<TurnOrderController>().Round();
We will use the SelectUnitState to handle advancement of the round enumerator. Remove the old
logic for stepping through each unit and use the following instead:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class SelectUnitState : BattleState
5. {
6. public override void Enter ()
7. {
8. base.Enter ();
9. StartCoroutine("ChangeCurrentUnit");
10. }
11.
12. public override void Exit ()
13. {
14. base.Exit ();
15. statPanelController.HidePrimary();
16. }
17.
18. IEnumerator ChangeCurrentUnit ()
19. {
20. owner.round.MoveNext();
21. SelectTile(turn.actor.tile.pos);
22. RefreshPrimaryStatPanel(pos);
23. yield return null;
24. owner.ChangeState<CommandSelectionState>();
25. }
26. }
Demo
Open our battle scene – press “Play” and experiement to see who moves rst – who had the
highest speed stat? Make one unit move and act, another only move or act, and one simply wait.
Who moves rst on the next round? If everything worked correctly you should see the turn order
changing based on your actions.
Summary
This week we replaced our placeholder linear turn order system with a notably more complex
and dynamic system. The order a unit moves can be determined by a large number of factors
including their stats and any number of other scripts such as those which give status effects. In
addition, turns can be bypassed completely such as by a unit being KO’d.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
During our last lesson I suggested possible implementations for a few status effects, such as
Haste, Slow and Stop. In this lesson we will actually add them. We will also learn how to manage
the way multiple conditions might be keeping a status active. For some variation we will also add
a Poison status effect and see how it can be tied to an item as an equip feature – a cursed sword.
Refactoring
It turns out I was only mostly right with the way I suggested we implement our status effects in
the last lesson. The current implementation of our ValueModi er classes only modify the overall
value, not the amount of change of a value.
Imagine the following scenario: we have a unit with a stat CTR value of 500, and on an update
“tick” will increment the value by 100. If we “catch” the noti cation and attach a
MultValueModi er with a multiplier of 2 in an attempt to implement haste, then the actual result
is “(500 + 100) * 2 = 1200”. What I wanted for haste is “500 + (100 * 2) = 700”.
In order to allow us a way to modify the amount of change of a value we will have to refactor our
code. Open the VauleModi er script and change the signature of our Modify method to the
following:
Each of the subclasses will also need to use the new signature. As you update the signatures and
implementation bodies, use the “toValue” parameter to replace the original “value” parameter. If
you try to “Build” you will see errors until you x each subclass instance. Refer to the code in my
repository if you get stuck.
Now that we have modi ed the ValueModi ers, we will also need to modify the way that a
ValueChangeException determines the modi ed value. Use the following:
By knowing the value we were changing “from” and the value we are changing “to”, it is a simple
matter to determine the delta. Because of this I can now add modi ers which modify the result
based on that delta. Add a new script named MultDeltaModi er to the
Scripts/Exceptions/Modi ers folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MultDeltaModifier : ValueModifier
5. {
6. public readonly float toMultiply;
7.
8. public MultDeltaModifier (int sortOrder, float toMultiply) : base (sortOrder)
9. {
10. this.toMultiply = toMultiply;
11. }
12.
13. public override float Modify (float fromValue, float toValue)
14. {
15. float delta = toValue - fromValue;
16. return fromValue + delta * toMultiply;
17. }
18. }
Status Conditions
There might be multiple reasons why a status is active on a Unit. Note that this is different than
the cause of the status. For example, you could apply Blind by casting a magic spell or by hitting
something with a special item – these are the cause of the status. When one of these causes
occur, we add the status effect and add a “condition” for how long the effect remains active. In
these cases we might say that the condition is some sort of timer or “duration” and once the time
requirement is met, the condition for the status is removed, which also removes the status effect
itself assuming that no other conditions were still active.
Normally I create an “abstract” base class, but in this case I decided to leave the base class as
“usable” – it will be a “manual” condition which doesn’t take care of removing itself, and instead
something else will decide when to add and remove it. Later, this base class will be used as part of
an Equip Feature, where the condition is that a status will be applied for as long as the item is
equipped.
Add a new script named StatusCondition to the following folder path Scripts/View Model
Component/Status/Conditions.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class StatusCondition : MonoBehaviour
5. {
6. public virtual void Remove ()
7. {
8. Status s = GetComponentInParent<Status>();
9. if (s)
10. s.Remove(this);
11. }
12. }
The only thing this script does is to know how to remove itself. That’s not much, but its really the
sole purpose of this class. It is there as a sort of “lock” to keep a status effect applied, but it doesn’t
care what the status effect is, it only needs to know about itself and how to remove itself. The
Status component manages the relationship between these “locks” and the effects, but we will get
to that later.
Subclasses of the StatusCondition class will be able to remove themselves through some sort of
more speci c event. For example, time – add another script named DurationStatusCondition to
the same folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class DurationStatusCondition : StatusCondition
5. {
6. public int duration = 10;
7.
8. void OnEnable ()
9. {
10. this.AddObserver(OnNewTurn, TurnOrderController.RoundBeganNotification);
11. }
12.
13. void OnDisable ()
14. {
15. this.RemoveObserver(OnNewTurn, TurnOrderController.RoundBeganNotification);
16. }
17.
18. void OnNewTurn (object sender, object args)
19. {
20. duration--;
21. if (duration <= 0)
22. Remove();
23. }
24. }
This subclass listens for noti cations that a new round has begun, and with each new round
reduces a duration counter by one. Note that you can specify how many rounds a particular effect
will last because the duration eld is public.
Status Effects
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class StatusEffect : MonoBehaviour
5. {
6.
7. }
Yep – its a completely empty script. Why would I ever do such a thing? Even though this base
class has no functionality whatsoever, I wanted to make sure that all status effects share a
common base class. This way, if as I am implementing them I do see some common
functionality, it will be easy to move it to the base class and allow it to be reused. In addition, it
makes the intent of my code more clear in other classes. For example, the Status component
which I am about to create will work based on pairs of StatusEffect and StatusCondition
components. If I didn’t specify a base class for the StatusEffect, then there would be nothing
stopping a particularly “clever” user from adding “any” MonoBehaviour he wanted which may
lead to unexpected consequences. Because I did specify a base class, the intentions of the
architecture are much more obvious.
Haste
1. using UnityEngine;
2. using System.Collections;
3.
4. public class HasteStatusEffect : StatusEffect
5. {
6. Stats myStats;
7.
8. void OnEnable ()
9. {
10. myStats = GetComponentInParent<Stats>();
11. if (myStats)
12. this.AddObserver( OnCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), myStats );
13. }
14.
15. void OnDisable ()
16. {
17. this.RemoveObserver( OnCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), myStats );
18. }
19.
20. void OnCounterWillChange (object sender, object args)
21. {
22. ValueChangeException exc = args as ValueChangeException;
23. MultDeltaModifier m = new MultDeltaModifier(0, 2);
24. exc.AddModifier(m);
25. }
26. }
In this case we register for the WillChangeNoti cation of the CTR stat. In the noti cation handler
we add our brand new MultDeltaModi er to the ValueChangeException with a multiplier of “2”.
This will cause the amount by which the stat changes to be doubled.
Slow
The Slow status effect is nearly identical to the Haste status effect. The only difference (besides
the name of the class) is the multiplier value. At this point you should probably be thinking “Oh
no, I’ve just repeated myself!” and if you are then you should give yourself a gold star.
There are a few ways we could have reduced the amount of code here. One way is that the Haste
and Slow status effects could share a common base class. Another is that we could simply use a
single script with a public eld for the multiplier value to use. Perhaps this script would be called
ModifyCounterSpeedStatusEffect. Unfortunately it isn’t quite as intuitive that this single
component would be used for both the implementation of Haste and Slow – I could see myself
forgetting how or where it was implemented. Furthermore, as this game is eshed out more in
the future I still might prefer they be separate classes due to new implementation details such as
the different ways that visual aids (the things on-screen which indicate that a unit is under the
haste or slow status effect) might appear. I’ll leave the nal decision on this sort of architecture
up to you.
Stop
Add another script named StopStatusEffect to the same folder:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class StopStatusEffect : StatusEffect
5. {
6. Stats myStats;
7.
8. void OnEnable ()
9. {
10. myStats = GetComponentInParent<Stats>();
11. if (myStats)
12. this.AddObserver( OnCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), myStats );
13. }
14.
15. void OnDisable ()
16. {
17. this.RemoveObserver( OnCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), myStats );
18. }
19.
20. void OnCounterWillChange (object sender, object args)
21. {
22. ValueChangeException exc = args as ValueChangeException;
23. exc.FlipToggle();
24. }
25. }
This script also looks very similar to both Haste and Slow. In fact, it would have been possible to
use the same script for all three if I had used a public eld for the multiplier value. In this case, I
would just use a multiplier of 0. However, because I mentioned in the last post that you could
simply ip the toggle, I wanted to show that implementation. Also, ipping a toggle is more
“strict” than multiplying by zero. For example, if I had multiple value modi ers in play, one might
multiply by zero, and another could add some other amount so that the nal result was still non-
zero. When the toggle is ipped, it doesn’t matter what the nal value would have been, a change
simply isn’t allowed.
Poison
Add our nal status effect named PoisonStatusEffect to the same folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class PoisonStatusEffect : StatusEffect
5. {
6. Unit owner;
7.
8. void OnEnable ()
9. {
10. owner = GetComponentInParent<Unit>();
11. if (owner)
12. this.AddObserver(OnNewTurn, TurnOrderController.TurnBeganNotification, owner);
13. }
14.
15. void OnDisable ()
16. {
17. this.RemoveObserver(OnNewTurn, TurnOrderController.TurnBeganNotification, owner);
18. }
19.
20. void OnNewTurn (object sender, object args)
21. {
22. Stats s = GetComponentInParent<Stats>();
23. int currentHP = s[StatTypes.HP];
24. int maxHP = s[StatTypes.MHP];
25. int reduce = Mathf.Min(currentHP, Mathf.FloorToInt(maxHP * 0.1f));
26. s.SetValue(StatTypes.HP, (currentHP - reduce), false);
27. }
28. }
This status effect operates on a different noti cation, one that we haven’t actually added yet (but
will in a moment). I had originally allowed it to work on the beginning of each new round, but
then I realized there were several rounds before our units build up enough CTR to actually take a
turn. I decided that it felt better to see the effect of the poison just before the unit takes a turn.
When the noti cation handler executes it gets a reference to the Stats component and reduces
HP by one-tenth of the MHP or the current HP of the unit, whichever is less. Note that I could
have relied on a Clamp Value Modi er which existed elsewhere (like a Health component) to
ensure that HP never goes below zero (or above MHP). However, in this case I used the SetValue
method with the AllowExceptions parameter set to false. I decided that the effect of Poison would
be unalterable, but this also may not be a design you agree with. Feel free to modify things as you
desire.
Don’t forget that we will need to add the new noti cation to the TurnOrderController script. It
would look like the following:
1. ...
2. if (CanTakeTurn(units[i]))
3. {
4. bc.turn.Change(units[i]);
5. units[i].PostNotification(TurnBeganNotification); // ADDED
6. yield return units[i];
7. ...
Extensions
In Unity, when you Destroy a GameObject or Component, the thing which you destroyed is still
there until the next frame. Imagine for example, that I have added a component, destroy it, and
then do a GetComponent from somewhere else. The GetComponent call can nd the component
which is being destroyed and this can lead to some unfortunate problems. In addition, Unity
doesn’t provide any sort of eld which can be referenced to know that the object is scheduled for
destruction.
In order to x the problem mentioned above, and also for the sake of clear debugging in the
hierarchy, I will use a system where I add components to children objects. When I want to destroy
an object, I can rst unparent the transform so that calls to GetComponentInChildren will not
succeed in nding the object which is going to be destroyed.
A good polish step in the future might be to use the GameObjectPoolController and reuse
GameObjects rather than constantly creating and destroying them. I’m not that worried at the
moment because the frequency of the creation and destruction of these objects is so sporadic.
Add a new script named GameObjectExtensions to the Scripts/Extensions folder. This script will
make it easy to create a new child object which is parented to the indicated game object, and
attach the component of type you specify with generics.
1. using UnityEngine;
2. using System.Collections;
3.
4. public static class GameObjectExtensions
5. {
6. public static T AddChildComponent<T> (this GameObject obj) where T : MonoBehaviour
7. {
8. GameObject child = new GameObject( typeof(T).Name );
9. child.transform.SetParent(obj.transform);
10. return child.AddComponent<T>();
11. }
12. }
Status
Add a new script named Status to the Scripts/View Model Component/Status folder. This
component will be responsible for determining how long a status effect should remain applied to
a unit. It handles this by checking for the existance of status conditions. As long as there is a
condition tied to an effect, the effect remains applied. Note that the script itself doesn’t know or
need to know anything speci c about the status effects or status conditions themselves.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Status : MonoBehaviour
6. {
7. public const string AddedNotification = "Status.AddedNotification";
8. public const string RemovedNotification = "Status.RemovedNotification";
9.
10. public U Add<T, U> () where T : StatusEffect where U : StatusCondition
11. {
12. T effect = GetComponentInChildren<T>();
13.
14. if (effect == null)
15. {
16. effect = gameObject.AddChildComponent<T>();
17. this.PostNotification(AddedNotification, effect);
18. }
19.
20. return effect.gameObject.AddChildComponent<U>();
21. }
22.
23. public void Remove (StatusCondition target)
24. {
25. StatusEffect effect = target.GetComponentInParent<StatusEffect>();
26.
27. target.transform.SetParent(null);
28. Destroy(target.gameObject);
29.
30. StatusCondition condition = effect.GetComponentInChildren<StatusCondition>();
31. if (condition == null)
32. {
33. effect.transform.SetParent(null);
34. Destroy(effect.gameObject);
35. this.PostNotification(RemovedNotification, effect);
36. }
37. }
38. }
So far I have shown examples of “what” a status effect can do, and “when” it should be active. I
haven’t shown examples of “how” to actually apply something. I did suggest a common way
would be to use a magic spell or attack to deliver the status effect along with a duration condition,
but now I want to do something a little different. We will use our Feature component so that we
can make the addition of a status effect one of the features of equipping an item.
Most of the time you will choose something nice, like special shoes which provide haste or
something like that. Sometimes it can be interesting to mix things in an unexpected way, such as
a sword which is exceedingly powerful, but which is also cursed so equipping it causes you to be
poisoned.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class AddStatusFeature<T> : Feature where T : StatusEffect
5. {
6. #region Fields
7. StatusCondition statusCondition;
8. #endregion
9.
10. #region Protected
11. protected override void OnApply ()
12. {
13. Status status = GetComponentInParent<Status>();
14. statusCondition = status.Add<T, StatusCondition>();
15. }
16.
17. protected override void OnRemove ()
18. {
19. if (statusCondition != null)
20. statusCondition.Remove();
21. }
22. #endregion
23. }
Next you can add a subclass called AddPoisonStatusFeature:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AddPoisonStatusFeature : AddStatusFeature<PoisonStatusEffect>
5. {
6.
7. }
One thing to consider with this architecture, is the potential for an “explosion of classes” by
which I mean that the more kinds of status effects we add, the more kinds of speci c “Add Status
Feature” subclasses we might also add. If the method of deliveries also included something like
an “OnHitAddStatus” class then we may likewise have subclasses of that for each type of
subclassed status effect. The classes themselves are empty but it is unfortunate to need so many.
An alternative architecture pattern could be to simply provide a public System.Type eld which
determines what type of feature to add. A single class would be able to be used no matter how
many types of status effects we wanted to add. Unfortunately, you lose some of the readability
and constraints that generics provided. Also, there isn’t a good method for attaching a “Type”
through the inspector for your prefabs. You could use a string and get a type from the string, but
that is also vulnerable to abuse and lacks compile time checking. Furthermore, you can’t directly
use the “Type” with generics, but would need to use Re ection and that also feels a little wrong to
me. Just my opinions – feel free to pick whatever feels best to you.
Demo
Let’s continue to use our Battle Scene, but this time as each unit moves, we will do “something”
regarding status effects. One of our units will equip our cursed sword (and therefore get
poisoned), and each of the units will get one of the CTR based status effects as well. Experiment
with moving the pieces on the board, but for now don’t actually attack. Simply observe that one of
the units will be faster than the others (Haste), one will get turns but very slowly in comparison
(Slow), and another wont be getting turns at all (Stop). Eventually the status effects will all wear
off, and each of the unit’s speeds will return to normal. Also note that even though no attacking
took place, the unit that had equipped the poison sword will have lost hit points.
Before we begin the demo, add a Status and Equipment component to the Hero prefab in the
project pane.
Next, add a tempory script named Demo and attach it to the Battle Controller GameObject in the
scene.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Demo : MonoBehaviour
5. {
6. Unit cursedUnit;
7. Equippable cursedItem;
8. int step;
9.
10. void OnEnable ()
11. {
12. this.AddObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification);
13. }
14.
15. void OnDisable ()
16. {
17. this.RemoveObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification);
18. }
19.
20. void OnTurnCheck (object sender, object args)
21. {
22. BaseException exc = args as BaseException;
23. if (exc.toggle == false)
24. return;
25.
26. Unit target = sender as Unit;
27. switch (step)
28. {
29. case 0:
30. EquipCursedItem(target);
31. break;
32. case 1:
33. Add<SlowStatusEffect>(target, 15);
34. break;
35. case 2:
36. Add<StopStatusEffect>(target, 15);
37. break;
38. case 3:
39. Add<HasteStatusEffect>(target, 15);
40. break;
41. default:
42. UnEquipCursedItem(target);
43. break;
44. }
45. step++;
46. }
47.
48. void Add<T> (Unit target, int duration) where T : StatusEffect
49. {
50. DurationStatusCondition condition = target.GetComponent<Status>().Add<T,
DurationStatusCondition>();
51. condition.duration = duration;
52. }
53.
54. void EquipCursedItem (Unit target)
55. {
56. cursedUnit = target;
57.
58. GameObject obj = new GameObject("Cursed Sword");
59. obj.AddComponent<AddPoisonStatusFeature>();
60. cursedItem = obj.AddComponent<Equippable>();
61. cursedItem.defaultSlots = EquipSlots.Primary;
62.
63. Equipment equipment = target.GetComponent<Equipment>();
64. equipment.Equip(cursedItem, EquipSlots.Primary);
65. }
66.
67. void UnEquipCursedItem (Unit target)
68. {
69. if (target != cursedUnit || step < 10)
70. return;
71.
72. Equipment equipment = target.GetComponent<Equipment>();
73. equipment.UnEquip(cursedItem);
74. Destroy(cursedItem.gameObject);
75.
76. Destroy(this);
77. }
78. }
Note that I wont have saved the scene changes or demo script to my repository. They are merely
fun little snippets to verify that our code works and to help you understand how things work
together.
Summary
In this lesson we refactored some of our code to support new ways to work with value change
exceptions. Using the new method of value modi cations, we were easily able to implement
several status effects including Haste, Slow, and Stop. For variety we also added Poison. Next, we
provided a system which could manage the “lifespan” of a status effect by keeping it active for as
long as any status condition was also applied. Finally, we showed a means of actually applying a
status effect. We went for a specialty route and applied a status effect by making it a Feature of
an equipped weapon.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
There is still plenty to do before we have actually implemented a true “attack” ability. In this
lesson we will determine what sort of chance there is that the unit will actually hit the target.
Sometimes special status effects or abilities might alter the chances of hitting, but at a minimum
we will take the angle of attack (front, side or back) into account. It is easier to hit an opponent if
they don’t see the attack coming. In addition we will add another UI panel to indicate this new hit
rate.
Facings
We will use a new enum to hold the three basic facing angles that I care about for this game.
Create a new script named Facings located in the Scripts/Enums folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Facings
5. {
6. Front,
7. Side,
8. Back
9. }
In order to determine an angle of attack (our Facing angle) we will need to determine the angle
from the attacker to the target, and the angle from the target to its current direction. In order to
help illustrate the idea, consider the following image:
In this image, the center square has an arrow pointing to the right. Imagine that this arrow is a
target unit which we wish to attack and that it is facing toward the right. Our attacker could be
located at any other square, and if it was, we need a good way to know what angle would the
attack be from – the back, side, or front?
According to the image, if we place the unit on any yellow tile which is marked with an “F” then
we would be attacking from the Front. I have similarly marked the Side (“S”) and Back (“B”) tile
locations.
If you aren’t familiar with “advanced” math, your algorithm for implementing this might be a bit
long and hard to read, because each tile could be any of the three Facings – it depends on the
direction the target is facing as well as the direction of the attacker to the target.
If you are familiar with “advanced” math, then this problem is surprisingly easy. The solution is
called a “dot product”. I thought about including the mathematical de nition, but somehow I don’t
nd it very helpful at all, so as a non-mathematician have mercy on me making up my own. You
can use a dot product (a single oat value) to determine the relationship between two vectors –
such as whether they face in the same direction (values greater than zero), a perpindicular
direction (zero), or opposite directions (values less than zero), etc.
It might sound pretty dif cult and advanced but it actually boils down to some very simple
concepts that anyone with an elementary math education (maybe slightly more) should be able
to understand. Take two Vectors (I assume you are familiar with Vector2 in Unity by now) and
then multiply the x’s together, multiply the y’s together and add the result. That nal number is
the dot product. Of course Unity handles this “scary” math stuff automatically:
1. float d1 = Vector2.Dot( new Vector2(1, 0), new Vector2(0, 1) ); // 0 is Perpindicular
2. float d2 = Vector2.Dot( new Vector2(1, 0), new Vector2(1, 0) ); // 1 is Same Direction
3. float d3 = Vector2.Dot( new Vector2(1, 0), new Vector2(-1, 0) ); // -1 is Opposite
Direction
Before I lose everyone (hopefully I haven’t already), I’ll just get to the point. If you were to take the
dot product of the normalized vector from our attacker to our defender, and the normalized vector
representing the angle the defender was facing, then you would get numbers in the range of -1
(attacking from the front) to +1 (attacking from the back). These relationships are shown in the
image below as the dog (attacker) approaches the cat (defender).
Enough talk, let’s see what this looks like as code. Create a new script called FacingsExtensions
located in the Scripts/Extensions folder:
1. using UnityEngine;
2. using System.Collections;
3.
4. public static class FacingsExtensions
5. {
6. public static Facings GetFacing (this Unit attacker, Unit target)
7. {
8. Vector2 targetDirection = target.dir.GetNormal();
9. Vector2 approachDirection = ((Vector2)(target.tile.pos -
attacker.tile.pos)).normalized;
10. float dot = Vector2.Dot( approachDirection, targetDirection );
11. if (dot >= 0.45f)
12. return Facings.Back;
13. if (dot <= -0.45f)
14. return Facings.Front;
15. return Facings.Side;
16. }
17. }
Note that in order for this code to compile I also added the following snippet to
DirectionsExtensions
I also added an implicit conversion from Point to Vector2 in the Point struct:
Hit Rate
Create a new script called HitRate in the Scripts/View Model Component/Ability/Hit Rate folder.
This will be our abstract base class for another type of component which we will add to each type
of ability. There will be three concrete implementations based on the design of Final Fantasy
Tactics, where we have one kind that is used for a standard attack, one that is used for applying
status ailments, and one that is used for abilities which should always hit.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class HitRate : MonoBehaviour
5. {
6. #region Notifications
7. /// <summary>
8. /// Includes a toggleable MatchException argument which defaults to false.
9. /// </summary>
10. public const string AutomaticHitCheckNotification =
"HitRate.AutomaticHitCheckNotification";
11.
12. /// <summary>
13. /// Includes a toggleable MatchException argument which defaults to false.
14. /// </summary>
15. public const string AutomaticMissCheckNotification =
"HitRate.AutomaticMissCheckNotification";
16.
17. /// <summary>
18. /// Includes an Info argument with three parameters: Attacker (Unit), Defender
(Unit),
19. /// and Defender's calculated Evade / Resistance (int). Status effects which modify
Hit Rate
20. /// should modify the arg2 parameter.
21. /// </summary>
22. public const string StatusCheckNotification = "HitRate.StatusCheckNotification";
23. #endregion
24.
25. #region Public
26. /// <summary>
27. /// Returns a value in the range of 0 t0 100 as a percent chance of
28. /// an ability succeeding to hit
29. /// </summary>
30. public abstract int Calculate (Unit attacker, Unit target);
31. #endregion
32.
33. #region Protected
34. protected virtual bool AutomaticHit (Unit attacker, Unit target)
35. {
36. MatchException exc = new MatchException(attacker, target);
37. this.PostNotification(AutomaticHitCheckNotification, exc);
38. return exc.toggle;
39. }
40.
41. protected virtual bool AutomaticMiss (Unit attacker, Unit target)
42. {
43. MatchException exc = new MatchException(attacker, target);
44. this.PostNotification(AutomaticMissCheckNotification, exc);
45. return exc.toggle;
46. }
47.
48. protected virtual int AdjustForStatusEffects (Unit attacker, Unit target, int rate)
49. {
50. Info<Unit, Unit, int> args = new Info<Unit, Unit, int>(attacker, target, rate);
51. this.PostNotification(StatusCheckNotification, args);
52. return args.arg2;
53. }
54.
55. protected virtual int Final (int evade)
56. {
57. return 100 - evade;
58. }
59. #endregion
60. }
The concrete subclasses of this component include several of the same “checks” but not in the
same order. The base class provides the implementations of the shared checks, which are called
in whatever order is important in the subclass in the Calculate method.
One such “check” to be performed is whether “something” will cause an ability to certainly
succeed. For example, our ability might be a regular “Attack” and on a normal occassion the
chance to hit needs to consider the target’s chance to evade. However, if the target were under the
effects of a “Stop” or “Sleep” status effect, then the chance to evade would be “zero” and we would
consider it an automatic hit type of event.
A similar but oppositie check is whether “something” can cause an ability to certainly fail. For
example, if the ability is supposed to apply a status effect, but the target has “Immune” attributes
for that status type, then the ability would need to fail no matter what. This would be particularly
important on some boss ghts to keep them from being too easy.
A nal check modi es the chances of a hit, without forcing it to be “certain”. For example, if the
Attacker is “Blind” then he can still hit an opponent, but his chances of hitting are worse.
The rst concrete subclass of our HitRate is the default type which will be used with most
abilities such as a standard attack. Its chances of success are partially determined by the
defender’s EVD (evade) stat. Create another script named ATypeHitRate in the same folder as the
base class:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class ATypeHitRate : HitRate
5. {
6. public override int Calculate (Unit attacker, Unit target)
7. {
8. if (AutomaticHit(attacker, target))
9. return Final(0);
10.
11. if (AutomaticMiss(attacker, target))
12. return Final(100);
13.
14. int evade = GetEvade(target);
15. evade = AdjustForRelativeFacing(attacker, target, evade);
16. evade = AdjustForStatusEffects(attacker, target, evade);
17. evade = Mathf.Clamp(evade, 5, 95);
18. return Final(evade);
19. }
20.
21. int GetEvade (Unit target)
22. {
23. Stats s = target.GetComponentInParent<Stats>();
24. return Mathf.Clamp(s[StatTypes.EVD], 0, 100);
25. }
26.
27. int AdjustForRelativeFacing (Unit attacker, Unit target, int rate)
28. {
29. switch (attacker.GetFacing(target))
30. {
31. case Facings.Front:
32. return rate;
33. case Facings.Side:
34. return rate / 2;
35. default:
36. return rate / 4;
37. }
38. }
39. }
The second type of hit rate component is used for special abilities which focus on applying status
effects. Its chances of success are partially determined by the defender’s RES (resistance) stat.
Create another script named STypeHitRate in the same folder:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class STypeHitRate : HitRate
5. {
6. public override int Calculate (Unit attacker, Unit target)
7. {
8. if (AutomaticMiss(attacker, target))
9. return Final(100);
10.
11. if (AutomaticHit(attacker, target))
12. return Final(0);
13.
14. int res = GetResistance(target);
15. res = AdjustForStatusEffects(attacker, target, res);
16. res = AdjustForRelativeFacing(attacker, target, res);
17. res = Mathf.Clamp(res, 0, 100);
18. return Final(res);
19. }
20.
21. int GetResistance (Unit target)
22. {
23. Stats s = target.GetComponentInParent<Stats>();
24. return s[StatTypes.RES];
25. }
26.
27. int AdjustForRelativeFacing (Unit attacker, Unit target, int rate)
28. {
29. switch (attacker.GetFacing(target))
30. {
31. case Facings.Front:
32. return rate;
33. case Facings.Side:
34. return rate - 10;
35. default:
36. return rate - 20;
37. }
38. }
39. }
Our nal hit rate type is for special abilities which should normally hit without fail. There still
may be exceptions to this rule, so I left a noti cation to allow a chance for misses. Add another
script named FullTypeHitRate to the same folder:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class FullTypeHitRate : HitRate
5. {
6. public override int Calculate (Unit attacker, Unit target)
7. {
8. if (AutomaticMiss(attacker, target))
9. return Final(100);
10.
11. return Final (0);
12. }
13. }
Match Exception
When determining when an ability would have either an automatic hit or automatic miss
exception, I posted a noti cation along with an instance of our next class, the MatchException.
Create and add this class to the Scripts/Exceptions folder.
In order to determine these cases, it can be helpful to know who is attacking and who is being
attacked This would provide access to any number of components you may want to check. Note
that the sender would be a HitRate, which should be tied to the same game object as the ability
being performed, and is also information you may need to know when determining whether or
not to allow this particular exception.
I wanted to use a subclass of a BaseException because I like the way that a condition is normally
a certain way and can only be toggled to the opposite way. This “safety net” will help to avoid
situations where one condition ips a toggle one way and a different condition ips it an
opposite way. The nal result would be dependent upon the order which the scripts listened to
and handled the noti cation and therefore could lead to some dif cult to track down “bugs” in
your code.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MatchException : BaseException
5. {
6. public readonly Unit attacker;
7. public readonly Unit target;
8.
9. public MatchException (Unit attacker, Unit target) : base (false)
10. {
11. this.attacker = attacker;
12. this.target = target;
13. }
14. }
Info
The hit rate also posted a status check noti cation to allow various status effects a chance to
modify the evasion rates of an ability. This noti cation passes along an instance of an Info object
which is just a generic class made up of one or more generic elds. I decided to use this class
instead of needing to make a speci c implementation every time I need to pass a few bits of
information along with a noti cation.
Although it is nice not to have to make a ton of little info classes, note that this method does not
provide any protections that I would have been able to specify in a manually created class. For
example, in the implementation I pass along both the attacker and dender as the rst two elds.
Had I created this class manually, I would make those two elds readonly so that no “listener”
would be able to modify them. The use of good code comments can alleviate this problem but it is
still something to keep in mind.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Info<T0>
5. {
6. public T0 arg0;
7.
8. public Info (T0 arg0)
9. {
10. this.arg0 = arg0;
11. }
12. }
13.
14. public class Info<T0, T1> : Info<T0>
15. {
16. public T1 arg1;
17.
18. public Info (T0 arg0, T1 arg1) : base (arg0)
19. {
20. this.arg1 = arg1;
21. }
22. }
23.
24. public class Info<T0, T1, T2> : Info<T0, T1>
25. {
26. public T2 arg2;
27.
28. public Info (T0 arg0, T1 arg1, T2 arg2) : base (arg0, arg1)
29. {
30. this.arg2 = arg2;
31. }
32. }
Stop Status Effect
Since we exposed a bit of functionality which could cause an ability to have an automatic hit
case, let’s add it to our “Stop” status effect. Final Fantasy Tactics would do the same for a variety
of other status effects like “Petrify”, “Hibernate”, and “Sleep”.
All we need to do is register for the noti cation in the OnEnable method, cleanup by un-
registering in the OnDisable method, and then provide our noti cation handler:
You could provide functionality for automatic misses in very much the same way, you would
simply be observing a different noti cation under different circumstances.
Let’s add a new status effect to help demonstrate how the “StatusCheck” portion of our Hit Rate
will work. Create a new script named BlindStatusEffect in the Scripts/View Model
Component/Status/Effects folder.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class BlindStatusEffect : MonoBehaviour
5. {
6. void OnEnable ()
7. {
8. this.AddObserver( OnHitRateStatusCheck, HitRate.StatusCheckNotification );
9. }
10.
11. void OnDisable ()
12. {
13. this.RemoveObserver( OnHitRateStatusCheck, HitRate.StatusCheckNotification );
14. }
15.
16. void OnHitRateStatusCheck (object sender, object args)
17. {
18. Info<Unit, Unit, int> info = args as Info<Unit, Unit, int>;
19. Unit owner = GetComponentInParent<Unit>();
20. if (owner == info.arg0)
21. {
22. // The attacker is blind
23. info.arg2 += 50;
24. }
25. else if (owner == info.arg1)
26. {
27. // The defender is blind
28. info.arg2 -= 20;
29. }
30. }
31. }
Jobs
When I originally created jobs, I didn’t include any default stats for EVD (evade) or RES (status
resistance). In order to see the results of our hard work in this lesson we will need to give our
units some ability to dodge an attack. I decided to go ahead and add a base amount of evasion
and resistance as a stat modi er, just like the movement and jump range stats:
1. Name,MHP,MMP,ATK,DEF,MAT,MDF,SPD,EVD,RES,MOV,JMP
2. Warrior,43,5,61,89,11,58,100,50,50,4,1
3. Wizard,30,25,11,58,61,89,98,50,50,3,2
4. Rogue,32,13,51,67,51,67,110,50,50,5,3
Don’t forget to recreate the Job project assets using our Pre-Production tool. From the menu bar
choose Pre Production->Parse Jobs.
In order to help the user make better informed decisions, let’s add a new UI element that will
show the HitRate for the selected ability. Add a new script named HitSuccessIndicator to the
Scripts/View Model Component folder.
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System.Collections;
4.
5. public class HitSuccessIndicator : MonoBehaviour
6. {
7. const string ShowKey = "Show";
8. const string HideKey = "Hide";
9.
10. [SerializeField] Canvas canvas;
11. [SerializeField] Panel panel;
12. [SerializeField] Image arrow;
13. [SerializeField] Text label;
14. Tweener transition;
15.
16. void Start ()
17. {
18. panel.SetPosition(HideKey, false);
19. canvas.gameObject.SetActive(false);
20. }
21.
22. public void SetStats (int chance, int amount)
23. {
24. arrow.fillAmount = (chance / 100f);
25. label.text = string.Format("{0}% {1}pt(s)", chance, amount);
26. }
27.
28. public void Show ()
29. {
30. canvas.gameObject.SetActive(true);
31. SetPanelPos(ShowKey);
32. }
33.
34. public void Hide ()
35. {
36. SetPanelPos(HideKey);
37. transition.easingControl.completedEvent += delegate(object sender,
System.EventArgs e) {
38. canvas.gameObject.SetActive(false);
39. };
40. }
41.
42. void SetPanelPos (string pos)
43. {
44. if (transition != null && transition.easingControl.IsPlaying)
45. transition.easingControl.Stop();
46.
47. transition = panel.SetPosition(pos, true);
48. transition.easingControl.duration = 0.5f;
49. transition.easingControl.equation = EasingEquations.EaseInOutQuad;
50. }
51. }
Our UI element will be pretty simple – we will use the AttackArrowBacker and AttackArrowFill
sprites to visually indicate the hit rate chance. In addition we will have a Text label beneath the
arrow to more speci cally show the chance of a hit as well as how much damage might be done
(although we are not implementing the damage algorithm yet).
We will be displaying the HitSuccessIndicator from the Con rmAbilityTargetState. We will need
to show the panel at the end of the Enter method as long as we have at least one target:
1. if (turn.targets.Count > 0)
2. {
3. hitSuccessIndicator.Show();
4. SetTarget(0);
5. }
Since this state can show the panel, it is also responsible for hiding it before leaving. Make sure to
hide the panel in the Exit method:
1. hitSuccessIndicator.Hide();
If the user switches the selected target (SetTarget method) we will also need to update the hit rate
for the new target:
Hopefully it is obvious to you that EstimateDamage has a placeholder implementation for now.
We wont be using xed values in a more complete implementation.
Demo
There is one last step to take before we can test everything out – add the ATypeHitRate
component to the “Attack” game object in the project assets “Hero” prefab. Assuming you have
created the Hit Success Indicator and linked it up to the Battle Controller you should now see a
hit rate appear in the con rm portion of your ability action.
Try targeting a unit from behind, from the side, and from the front to verify that the chance of
hitting from each angle is different. If you use the demo from last week you can also test that
targeting a unit with the “Stop” status effect is guaranteed to hit.
Summary
In this lesson we spent some time illustrating how a little bit of math can help simplify our code.
By using a Dot Product we were able to determine the angle of an attack. We created a few
different types of hit rate components which include the angle of attack to determine their hit
rate chances. Since our hit rate components were exible enough to use exceptions, we
implemented some in the “Stop” status effect and even added a new “Blind” status effect to show
how values could be modi ed. Finally, we added another simple UI element which displays the
hit rate to the user.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
Over the past several lessons we have implemented tempory implementations for the effect of an
ability, such as manually implementing an attack damage estimation and application of the
damage itself. In this lesson we will be providing a much more complete implementation which
allows for very different effects ranging from applying damage to in icting status ailments and
which supports just about anything you would want to do.
There are a few bug xes in this check-in, several of which were pointed out by my readers –
thanks!
Refactoring
I did a little refactoring which I don’t plan to cover in detail since you can refer to the code in the
repository, but I want to point it out. In HitRate I modi ed the signature of the Calculate method.
Let’s begin by creating a new abstract base class called BaseAbilityEffect and place it in a
Scripts/View Model Component/Ability/Effects/ directory. This script will provide two functions
– it will nish lling out the second part of our Hit Success Indicator with a damage “prediction”
and it will provide an interface through which we can apply whatever it is that the ability actually
does.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class BaseAbilityEffect : MonoBehaviour
5. {
6. public abstract int Predict (Tile target);
7. public abstract void Apply (Tile target);
8. }
Our rst concrete subclass will be used in many abilities, and is one which applies damage to a
targeted unit. Create a script named DamageAbilityEffect and place it in the same folder as the
base class.
I created this script while referencing the “Final Fantasy Tactics Advance: Mechanics Guide” by
TFergusson so that I felt fairly con dent I was going to be able to support a very complex damage
algorithm (not that one is always needed). The algorithm in that guide is twenty steps long, with
mutliple sub-steps in most of them. I don’t speci cally outline each step of the process, but
because of the way I architected this component, you could include each of those steps and more.
The damage algorithm starts with the Predict method. The Apply method actually uses the value
calculated by the prediction and then merely adds a little random variance. The nal calculated
value is subtracted from the target’s hit points.
The “trick” to supporting large amounts of control in this algorithm is in the use of noti cations
which pass along a list of Value Modi ers in an argument. Any object can listen to those
noti cations and add a modi er at any step thanks to the sorting order ability of the modi er
objects. The great thing is that my script doesn’t need to know about any of the objects which
create special cases of value modi cation. For example, if I got around to implementing a feature
where you carry along mission items, and by possessing a mission item you got an offensive
bonus, then I can allow that mission item to listen for the appropriate noti cation, insert a value
modifer into the list, and I am done. I can add (or remove) these kinds of little rules to my hearts
content all without ever needing to modify this script in any way.
As an example, when I want to get the “base attack” stat of the attacker, I call my “GetStat”
method. There I create an Info object which holds relevant information such as who the attacker
and defender are, as well as that List of Value Modi ers I mentioned. The “GetAttackNoti cation”
will be posted along with the info object. Any object which listens to the noti cation can then
insert new modi ers into the list. Next, the list of value modi ers are sorted and applied to a
value.
The very rst modi er in the list would be the true “base attack stat” which would come from
either the physical or magical attack stat of the unit, depending on what kind of ability is being
used. It would be “added” to the formula with an “Add Value Modi er” to take the stat from zero to
whatever it should be. Most other changes would multiply the nal result with a “Mult Value
Modi er”. Some of these modi ers could include:
The same basic idea is utilized in each step of the process. When we get the defense stat for
example, we can also check Mission Items, Support checks and Status checks, but keep in mind
that each implementor would be different based on whether the attacker or defender has a
particular status. For example, if the attacker is under Frog status then the attack would become
weaker as a result of reducing the attack stat, but if the defender is under Frog status then the
attack would become stronger as a result of reducing the defense stat.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class DamageAbilityEffect : BaseAbilityEffect
6. {
7. #region Consts & Notifications
8. public const string GetAttackNotification =
"DamageAbilityEffect.GetAttackNotification";
9. public const string GetDefenseNotification =
"DamageAbilityEffect.GetDefenseNotification";
10. public const string GetPowerNotification =
"DamageAbilityEffect.GetPowerNotification";
11. public const string TweakDamageNotification =
"DamageAbilityEffect.TweakDamageNotification";
12.
13. const int minDamage = -999;
14. const int maxDamage = 999;
15. #endregion
16.
17. #region Public
18. public override int Predict (Tile target)
19. {
20. Unit attacker = GetComponentInParent<Unit>();
21. Unit defender = target.content.GetComponent<Unit>();
22.
23. // Get the attackers base attack stat considering
24. // mission items, support check, status check, and equipment, etc
25. int attack = GetStat(attacker, defender, GetAttackNotification, 0);
26.
27. // Get the targets base defense stat considering
28. // mission items, support check, status check, and equipment, etc
29. int defense = GetStat(attacker, defender, GetDefenseNotification, 0);
30.
31. // Calculate base damage
32. int damage = attack - (defense / 2);
33. damage = Mathf.Max(damage, 1);
34.
35. // Get the abilities power stat considering possible variations
36. int power = GetStat(attacker, defender, GetPowerNotification, 0);
37.
38. // Apply power bonus
39. damage = power * damage / 100;
40. damage = Mathf.Max(damage, 1);
41.
42. // Tweak the damage based on a variety of other checks like
43. // Elemental damage, Critical Hits, Damage multipliers, etc.
44. damage = GetStat(attacker, defender, TweakDamageNotification, damage);
45.
46. // Clamp the damage to a range
47. damage = Mathf.Clamp(damage, minDamage, maxDamage);
48. return damage;
49. }
50.
51. public override void Apply (Tile target)
52. {
53. Unit defender = target.content.GetComponent<Unit>();
54.
55. // Start with the predicted damage value
56. int value = Predict(target);
57.
58. // Add some random variance
59. value *= Mathf.FloorToInt(UnityEngine.Random.Range(0.9f, 1.1f));
60.
61. // Clamp the damage to a range
62. value = Mathf.Clamp(value, minDamage, maxDamage);
63.
64. // Apply the damage to the target
65. Stats s = defender.GetComponent<Stats>();
66. s[StatTypes.HP] -= value;
67. }
68. #endregion
69.
70. #region Private
71. int GetStat (Unit attacker, Unit target, string notification, int startValue)
72. {
73. var mods = new List<ValueModifier>();
74. var info = new Info<Unit, Unit, List<ValueModifier>>(attacker, target, mods);
75. this.PostNotification(notification, info);
76. mods.Sort();
77.
78. float value = startValue;
79. for (int i = 0; i < mods.Count; ++i)
80. value = mods[i].Modify(startValue, value);
81.
82. int retValue = Mathf.FloorToInt(value);
83. retValue = Mathf.Clamp(retValue, minDamage, maxDamage);
84. return retValue;
85. }
86. #endregion
87. }
I wanted to show a variation in ability effects so the second effect I created doesn’t do any
damage at all, it merely applies a status effect. Create a new script named In ictAbilityEffect in
the same folder as the base class.
In the Status Effects post I discussed some architectural ideas for implementing an “Add Status
Feature” which is quite similar to an “In ict Ability Effect”. In that post I used a generic
implementation which would require a new subclass for every type of status we would want to
add. Now that I am doing almost the exact same thing with Ability Effects, I feel that the
“explosion of classes” problem I was worried about is a de nite issue. This time I decided to go
ahead and try the re ection route. I begin with the use of a string (since it is serializeable and you
can use it in the editor) which maps to a Type of StatusEffect. I used some error handling to verify
that we both have a legitimate class type and that the type is also a subclass of StatusEffect. If
both of those are true, I am able to generate and invoke the appropriate Generic Method
dynamically. I’m still not crazy about re ection, but at least I can reuse this single component for
any StatusEffect that I might want to add.
1. using UnityEngine;
2. using System.Collections;
3. using System;
4. using System.Reflection;
5.
6. public class InflictAbilityEffect : BaseAbilityEffect
7. {
8. public string statusName;
9. public int duration;
10.
11. public override int Predict (Tile target)
12. {
13. return 0;
14. }
15.
16. public override void Apply (Tile target)
17. {
18. Type statusType = Type.GetType(statusName);
19. if (statusType == null || !statusType.IsSubclassOf(typeof(StatusEffect)))
20. {
21. Debug.LogError("Invalid Status Type");
22. return;
23. }
24.
25. MethodInfo mi = typeof(Status).GetMethod("Add");
26. Type[] types = new Type[]{ statusType, typeof(DurationStatusCondition) };
27. MethodInfo constructed = mi.MakeGenericMethod(types);
28.
29. Status status = target.content.GetComponent<Status>();
30. object retValue = constructed.Invoke(status, null);
31.
32. DurationStatusCondition condition = retValue as DurationStatusCondition;
33. condition.duration = duration;
34. }
35. }
Base Ability Power
When we created the Damage Ability Effect, I mentioned ability “power” and also created several
noti cations that nothing listens to yet. Create a new script named BaseAbilityPower and place it
in the Scripts/View Model Component/Ability/Power/ directory.
This base class implementation will handle subscribing to the various noti cations, and in the
handler methods will add value modi ers with values based on abstract methods which will need
to be implemented by the concrete subclasses.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public abstract class BaseAbilityPower : MonoBehaviour
6. {
7. protected abstract int GetBaseAttack ();
8. protected abstract int GetBaseDefense (Unit target);
9. protected abstract int GetPower ();
10.
11. void OnEnable ()
12. {
13. this.AddObserver(OnGetBaseAttack, DamageAbilityEffect.GetAttackNotification);
14. this.AddObserver(OnGetBaseDefense, DamageAbilityEffect.GetDefenseNotification);
15. this.AddObserver(OnGetPower, DamageAbilityEffect.GetPowerNotification);
16. }
17.
18. void OnDisable ()
19. {
20. this.RemoveObserver(OnGetBaseAttack, DamageAbilityEffect.GetAttackNotification);
21. this.RemoveObserver(OnGetBaseDefense, DamageAbilityEffect.GetDefenseNotification);
22. this.RemoveObserver(OnGetPower, DamageAbilityEffect.GetPowerNotification);
23. }
24.
25. void OnGetBaseAttack (object sender, object args)
26. {
27. var info = args as Info<Unit, Unit, List<ValueModifier>>;
28. if (info.arg0 != GetComponentInParent<Unit>())
29. return;
30.
31. AddValueModifier mod = new AddValueModifier(0, GetBaseAttack());
32. info.arg2.Add( mod );
33. }
34.
35. void OnGetBaseDefense (object sender, object args)
36. {
37. var info = args as Info<Unit, Unit, List<ValueModifier>>;
38. if (info.arg0 != GetComponentInParent<Unit>())
39. return;
40.
41. AddValueModifier mod = new AddValueModifier(0, GetBaseDefense(info.arg1));
42. info.arg2.Add( mod );
43. }
44.
45. void OnGetPower (object sender, object args)
46. {
47. var info = args as Info<Unit, Unit, List<ValueModifier>>;
48. if (info.arg0 != GetComponentInParent<Unit>())
49. return;
50.
51. AddValueModifier mod = new AddValueModifier(0, GetPower());
52. info.arg2.Add( mod );
53. }
54. }
Create a new script named PhysicalAbilityPower in the same directory as the base class. This
script will (obviously) provide the implementation for abilities which are physical in nature. It
references the Attack stat of the attacker and the Defense stat of the defender. It also holds an int
eld called level which can be different for each ability. Weak units performing a strong ability
might still do a lot of damage in this way. Along the same vein, a unit can have multiple abilities
that might do different amounts of damage thanks to the power “level”, even though the attack
stat hasn’t changed.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class PhysicalAbilityPower : BaseAbilityPower
5. {
6. public int level;
7.
8. protected override int GetBaseAttack ()
9. {
10. return GetComponentInParent<Stats>()[StatTypes.ATK];
11. }
12.
13. protected override int GetBaseDefense (Unit target)
14. {
15. return target.GetComponent<Stats>()[StatTypes.DEF];
16. }
17.
18. protected override int GetPower ()
19. {
20. return level;
21. }
22. }
The magical ability power is very much like the physical one, but it uses the magic attack and
magic defense stats instead:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MagicalAbilityPower : BaseAbilityPower
5. {
6. public int level;
7.
8. protected override int GetBaseAttack ()
9. {
10. return GetComponentInParent<Stats>()[StatTypes.MAT];
11. }
12.
13. protected override int GetBaseDefense (Unit target)
14. {
15. return target.GetComponent<Stats>()[StatTypes.MDF];
16. }
17.
18. protected override int GetPower ()
19. {
20. return level;
21. }
22. }
For a third and nal variation of the ability power types, I will cause the power level to be
determined by the equipped weapon. If there is no weapon equipped, it will fallback to using the
default attack strength of whatever job the unit has taken.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class WeaponAbilityPower : BaseAbilityPower
5. {
6. protected override int GetBaseAttack ()
7. {
8. return GetComponentInParent<Stats>()[StatTypes.ATK];
9. }
10.
11. protected override int GetBaseDefense (Unit target)
12. {
13. return target.GetComponent<Stats>()[StatTypes.DEF];
14. }
15.
16. protected override int GetPower ()
17. {
18. int power = PowerFromEquippedWeapon();
19. return power > 0 ? power : UnarmedPower();
20. }
21.
22. int PowerFromEquippedWeapon ()
23. {
24. int power = 0;
25. Equipment eq = GetComponentInParent<Equipment>();
26. Equippable item = eq.GetItem(EquipSlots.Primary);
27. StatModifierFeature[] features = item.GetComponentsInChildren<StatModifierFeature>
();
28.
29. for (int i = 0; i < features.Length; ++i)
30. {
31. if (features[i].type == StatTypes.ATK)
32. power += features[i].amount;
33. }
34.
35. return power;
36. }
37.
38. int UnarmedPower ()
39. {
40. Job job = GetComponentInParent<Job>();
41. for (int i = 0; i < Job.statOrder.Length; ++i)
42. {
43. if (Job.statOrder[i] == StatTypes.ATK)
44. return job.baseStats[i];
45. }
46. return 0;
47. }
48. }
I added a new method to the Equipment script in order to easily determine what the primary item
would be:
1. AbilityEffectTarget[] targeters;
and use that eld in the FindTargets method instead of the local declaration. I removed the
CalculateHitRate and EstimateDamage methods and modi ed the UpdateHitSuccessIndicator so
that I am actually using the correct HitRate and Damage prediction based on what is actually able
to be targeted by an ability effect.
1. void UpdateHitSuccessIndicator ()
2. {
3. int chance = 0;
4. int amount = 0;
5. Tile target = turn.targets[index];
6.
7. for (int i = 0; i < targeters.Length; ++i)
8. {
9. if (targeters[i].IsTarget(target))
10. {
11. HitRate hitRate = targeters[i].GetComponent<HitRate>();
12. chance = hitRate.Calculate(target);
13.
14. BaseAbilityEffect effect = targeters[i].GetComponent<BaseAbilityEffect>();
15. amount = effect.Predict(target);
16. break;
17. }
18. }
19.
20. hitSuccessIndicator.SetStats(chance, amount);
21. }
Finally, let’s open the PerformAbilityState script and replace the TemporaryAttackExample
method with a more complete implementation. This time we aren’t hard-coding an ability effect
but are letting the ability perform the effect for itself. It also takes into account the hit rate – it
rolls a random number to see whether or not it actually hits.
1. void ApplyAbility ()
2. {
3. BaseAbilityEffect[] effects =
turn.ability.GetComponentsInChildren<BaseAbilityEffect>();
4. for (int i = 0; i < turn.targets.Count; ++i)
5. {
6. Tile target = turn.targets[i];
7. for (int j = 0; j < effects.Length; ++j)
8. {
9. BaseAbilityEffect effect = effects[j];
10. AbilityEffectTarget targeter = effect.GetComponent<AbilityEffectTarget>();
11. if (targeter.IsTarget(target))
12. {
13. HitRate rate = effect.GetComponent<HitRate>();
14. int chance = rate.Calculate(target);
15. if (UnityEngine.Random.Range(0, 101) > chance)
16. {
17. // A Miss!
18. continue;
19. }
20. effect.Apply(target);
21. }
22. }
23. }
24. }
Demo
At this point you should be able to build a nice variety of abilities. We will still need to add
Elemental attributes and magic point costs in the future, as well as some constraints on the
ability effects, but we have enough for a good demo already.
If I were to create an Ability, I would want to begin by adding an Empty GameObject as a child of
the Unit which would be able to use it. I would name the GameObject the same name as the
Ability (like you would see in the menu to select it). On this object I would add a component for
the types of “Ability Power”, “Ability Range”, and “Ability Area”. When we add Magic Point costs
and Elemental attributes I would also add those here.
Each effect of the ability (you can have multiple effects per ability) would be added to another
child GameObject of the Ability GameObject. For example, to implement “Attack” I could add
another GameObject named “Damage”. On this child object I would add a type of “Hit Rate”, “Effect
Target”, and “Ability Effect” component. When we add constraints to the Ability Effect (like only
adding a status effect if a previous effect hit, or implementing drain where you absorb the
amount of hit points you attacked for) I would also add them here.
For now, let’s test out a pair of effects. Modify the Hero prefab’s “Attack” ability to cause Damage
and In ict Blind as follows:
Have one unit hit another. If the attack connects, there is a good chance the target will also have
been blinded. You can tell whether or not it succeeded by using the Editor’s hierarchy pane to
look at the targeted unit for a child GameObject named “BlindStatusEffect”. Once a unit has been
blinded, it should have a very low chance of attacking another unit – particularly from the front.
Check for a “miss” to see that our HitRate code is functioning. You can tell we missed if the hit
points of the target haven’t changed.
Summary
In this lesson we implemented a couple of Ability Effects including the ability to apply damage to
a target and the ability to in ict a status effect. We provided a means of powering abilities both by
physical, magical, and weapon power. All of these pieces are components which can be mixed
and matched to create a large variety of abilities.
When implementing the in ict status ability effect, we took a look at using Re ection as a way to
work around an inability to use generics dynamically. In this way we were able to avoid an
“explosion of classes” where we would need a new subclass for every kind of status effect we
would wish to in ict.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
We have enough components to make several abilities, and in this lesson will make a few more.
Magic based abilities are often the most powerful in an RPG, so they need a way to be regulated. If
players could use them freely the game would end up both boring and monotonous. There should
be a reason for players to use different abilities at different times. In order to foster this kind of
strategy, this lesson covers the use of magic and magic points.
I noticed a couple of bugs in my Tweener / EasingControl code. First, if I tried to “attack” a tile that
didn’t have any enemies and then exit out of the state, I would generate a
NullReferenceException. This was occuring because the RectTransformAnimationExtensions
was trying to con gure the EasingControl for a Tweener on the HitSuccessIndicator, and because
there were no targets, the script didn’t try to make the panel appear, and the GameObject was
disabled. Because the GameObject was disabled, the Awake method of the Tweener never ran,
and the EasingControl was never created, therefore you can’t con gure it.
I xed the rst issue by making Tweener a subclass of EasingControl. I had considered a few
different options like using a RequireComponent tag, but that would have caused more trouble
rather than help because it doesn’t actually add a new component if one already existed, and it
makes cleanup that much harder.
Second, even when the EasingControl did exist, there were problems with trying to start a Tick
coroutine while the GameObject it was on was disabled. I xed this issue inside SetPlayState – if
the MonoBehaviour isn’t both Active and Enabled, then the state is actually set to Paused and the
previousPlayState eld is marked with the target state. That way when the OnEnable method is
run, the script will be able to Resume correctly.
Make sure to check out the code from the repository in order to get these xes. There is too much
other stuff to write about to add it all here.
Refactoring
Much of the code I am writing now didn’t exist in my rst prototype. As a side effect, I am slowing
down a bit and there may be more gaps between posts on this project – weekly posts may be too
hard for me to keep up, but I will try to at least post every other week. I just wanted to give you all
a heads-up.
Furthermore, as I am covering fresh ground I am likely to want to repeatedly change things until
it feels “right” – you should never be afraid to throw away your work. I often enough throw away
entire projects (and no, I dont plan to throw this one away) and completely start over. You would
be surprised how quick you can recreate it, and also how much better subsequent versions can
be.
For example, when I created the DamageAbilityEffect component, I had several noti cations for
getting stats etc. In this lesson I want to add a Heal equivalent which also needs to be able to get
stats. Therefore I refactored the shareable portions into the base class.
Likewise, I had code for performing an ability in the PerformAbilityState but decided to make an
actual Ability component which handled all the performing logic. This way, if there are different
states that might be able to apply an ability I wouldn’t need duplicate code. For example, in some
RPG’s you can apply certain abilities through a menu even outside of battle, such as Curing the
party or removing Status Ailments. At the moment I don’t plan on having a different state which
can perform abilities, and I also dont plan to let you use abilities outside of a battle (I would rather
start each battle “fresh”), but it still “felt” better to me, thus the change.
As with the bug xes, I do not plan to show all of the Refactored code. I’ll try to point out when it is
happening, but the changes themselves can be seen in the repository. I will only be showing the
new classes and code that are relevant to the ideas of the lesson.
Stat Wrappers
I’ve been waiting for awhile to add some more stat wrappers. If you don’t know what I am talking
about you might want to check out the lesson on Stats. In that lesson I showed a Rank script
which managed the relationship between Experience and a unit’s Level.
Health
Now that we are able to Attack (and in a moment Cure) we need a way to make sure a unit’s Hit
Points don’t go below zero, or above the Max Hit Points stats. Go ahead and add a new script
named Health to the Scripts/View Model Component/Actor/ directory.
This script keeps the Hit Points stat in a legal range by using a ClampValueModi er whenever the
Hit Points will change noti cation is red. Note that you could expose another eld for the
minimum hit points which could be something other than zero. This way you could have
“Immortal” enemies, or special story matches where the enemy doesn’t actually die, but escapes
once his HP drops to a certain point.
This script also listens for changes to Max Hit Points which could come as a result of leveling-up,
or equipping special gear, etc. In the cases where the max is raised, I decide to also raise the Hit
Points by the same amount. In cases where it is reduced (perhaps you un-equipped that gear)
then I simply make sure HP stays within bounds.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Health : MonoBehaviour
5. {
6. #region Fields
7. public int HP
8. {
9. get { return stats[StatTypes.HP]; }
10. set { stats[StatTypes.HP] = value; }
11. }
12.
13. public int MHP
14. {
15. get { return stats[StatTypes.MHP]; }
16. set { stats[StatTypes.MHP] = value; }
17. }
18.
19. Stats stats;
20. #endregion
21.
22. #region MonoBehaviour
23. void Awake ()
24. {
25. stats = GetComponent<Stats>();
26. }
27.
28. void OnEnable ()
29. {
30. this.AddObserver(OnHPWillChange, Stats.WillChangeNotification(StatTypes.HP),
stats);
31. this.AddObserver(OnMHPDidChange, Stats.DidChangeNotification(StatTypes.MHP),
stats);
32. }
33.
34. void OnDisable ()
35. {
36. this.RemoveObserver(OnHPWillChange, Stats.WillChangeNotification(StatTypes.HP),
stats);
37. this.RemoveObserver(OnMHPDidChange, Stats.DidChangeNotification(StatTypes.MHP),
stats);
38. }
39. #endregion
40.
41. #region Event Handlers
42. void OnHPWillChange (object sender, object args)
43. {
44. ValueChangeException vce = args as ValueChangeException;
45. vce.AddModifier(new ClampValueModifier(int.MaxValue, 0, stats[StatTypes.MHP]));
46. }
47.
48. void OnMHPDidChange (object sender, object args)
49. {
50. int oldMHP = (int)args;
51. if (MHP > oldMHP)
52. HP += MHP - oldMHP;
53. else
54. HP = Mathf.Clamp(HP, 0, MHP);
55. }
56. #endregion
57. }
Mana
Because we will be adding a Magic Point “cost” to the use of some magical abilities, then our
magic points stat will now start being modi ed as well. We will need a component to manage the
relationship between Magic Points and Max Magic Points which is nearly identical to the Health
component we just added. The only difference is that I also want Magic Points to regenerate over
time. For this I added a listener for the TurnBeganNoti cation and give a unit back a percentage
of its max stat on each new turn.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Mana : MonoBehaviour
5. {
6. #region Fields
7. public int MP
8. {
9. get { return stats[StatTypes.MP]; }
10. set { stats[StatTypes.MP] = value; }
11. }
12.
13. public int MMP
14. {
15. get { return stats[StatTypes.MMP]; }
16. set { stats[StatTypes.MMP] = value; }
17. }
18.
19. Unit unit;
20. Stats stats;
21. #endregion
22.
23. #region MonoBehaviour
24. void Awake ()
25. {
26. stats = GetComponent<Stats>();
27. unit = GetComponent<Unit>();
28. }
29.
30. void OnEnable ()
31. {
32. this.AddObserver(OnMPWillChange, Stats.WillChangeNotification(StatTypes.MP),
stats);
33. this.AddObserver(OnMMPDidChange, Stats.DidChangeNotification(StatTypes.MMP),
stats);
34. this.AddObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);
35. }
36.
37. void OnDisable ()
38. {
39. this.RemoveObserver(OnMPWillChange, Stats.WillChangeNotification(StatTypes.MP),
stats);
40. this.RemoveObserver(OnMMPDidChange, Stats.DidChangeNotification(StatTypes.MMP),
stats);
41. this.RemoveObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);
42. }
43. #endregion
44.
45. #region Event Handlers
46. void OnMPWillChange (object sender, object args)
47. {
48. ValueChangeException vce = args as ValueChangeException;
49. vce.AddModifier(new ClampValueModifier(int.MaxValue, 0, stats[StatTypes.MHP]));
50. }
51.
52. void OnMMPDidChange (object sender, object args)
53. {
54. int oldMMP = (int)args;
55. if (MMP > oldMMP)
56. MP += MMP - oldMMP;
57. else
58. MP = Mathf.Clamp(MP, 0, MMP);
59. }
60.
61. void OnTurnBegan (object sender, object args)
62. {
63. if (MP < MMP)
64. MP += Mathf.Max(Mathf.FloorToInt(MMP * 0.1f), 1);
65. }
66. #endregion
67. }
We will need to add the Health and Mana components to our heroes, and since those components
require the Stats component, they must be added afterward. Add these two lines just after adding
the rank component at the end of the SpawnTestUnits for loop:
1. instance.AddComponent<Health>();
2. instance.AddComponent<Mana>();
One of the rst Magical abilities I wanted to add was a way to restore Hit Points. Therefore, I
needed to add a “Heal” equivalent of the “Damage” ability effect. Make sure you check out the
refactoring to that component and the BaseAbilityEffect parent class in the repository, or the
code here wont compile. Then, add a new script named HealAbilityEffect to the same directory as
DamageAbilityEffect.
The algorithm for applying the Healing effect should be different than for Damaging. This is
because a unit doesn’t “want” the effect of damage, so it makes sense that a good “defense” stat
would reduce the amount of damage done. However, under normal circumstances, a unit would
“want” the effect of healing, and therefore you wouldn’t want his defense to reduce the amount of
healing he could receive.
Unfortunately the guides I was referencing for the Damage algorithm didn’t list a Heal algorithm.
I wasn’t sure what to use, so my prediction algorithm is pretty bare – I simply use the Power stat.
Feel free to tweak this to something better.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class HealAbilityEffect : BaseAbilityEffect
5. {
6. public override int Predict (Tile target)
7. {
8. Unit attacker = GetComponentInParent<Unit>();
9. Unit defender = target.content.GetComponent<Unit>();
10. return GetStat(attacker, defender, GetPowerNotification, 0);
11. }
12.
13. protected override int OnApply (Tile target)
14. {
15. Unit defender = target.content.GetComponent<Unit>();
16.
17. // Start with the predicted value
18. int value = Predict(target);
19.
20. // Add some random variance
21. value = Mathf.FloorToInt(value * UnityEngine.Random.Range(0.9f, 1.1f));
22.
23. // Clamp the amount to a range
24. value = Mathf.Clamp(value, minDamage, maxDamage);
25.
26. // Apply the amount to the target
27. Stats s = defender.GetComponent<Stats>();
28. s[StatTypes.HP] += value;
29. return value;
30. }
31. }
I have shown abilities with multiple effects, like an attack that could both cause damage and
in ict blind, but I haven’t shown any abilities with unique effects. By this I mean an ability would
do different things to different kinds of units rather than multiple things to the same type of unit.
Cure will be our rst ability to need unique effects, but I will create a few in this lesson. Cure has
the unique effect of healing normal units but damaging the undead. To implement this we will
add a few new components:
Undead
Add a new component named Undead to the Scripts/View Model Component/Actor/ directory. At
the moment, this script will be an empty MonoBehaviour. It is simply used as a marker to identify
if a Unit is undead or not based on whether or not the component is attached. Of course it may
include additional logic later.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Undead : MonoBehaviour
5. {
6.
7. }
Now that we have the Undead component we can add another AbilityEffectTarget type. This
component makes sure a Unit either Does or Does NOT have the Undead component based on the
state of a toggle. Create and add this script in the same directory as the other AbilityEffectTarget
scripts.
Another Ability I will add is “Raise” – which actually has three unique effects. Non-undead units
are healed, Undead units are damaged, and KO’d units are revived. Note that you can’t “Cure” a
KO’d unit because the effect target which I will combine with it has a requirement that the Hit
Points are greater than zero, and that makes this effect extra valuable.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class ReviveAbilityEffect : BaseAbilityEffect
5. {
6. public float percent;
7.
8. public override int Predict (Tile target)
9. {
10. Stats s = target.content.GetComponent<Stats>();
11. return Mathf.FloorToInt(s[StatTypes.MHP] * percent);
12. }
13.
14. protected override int OnApply (Tile target)
15. {
16. Stats s = target.content.GetComponent<Stats>();
17. int value = s[StatTypes.HP] = Predict(target);
18. return value;
19. }
20. }
For more variety I wanted to show an example of making a dependent ability effect. I had
mentioned the suggestion that some abilities might only work based on an earlier effect
succeeding. For example, if you perform an attack and the attack doesn’t miss, then an “In ict
Blind” effect might also trigger.
The example I decided to implement is “Drain” – an effect which will “heal” the attacker by
however much damage was in icted. Some games may call it a “vampiric” effect.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AbsorbDamageAbilityEffectTarget : BaseAbilityEffect
5. {
6. #region Fields
7. public int trackedSiblingIndex;
8. BaseAbilityEffect effect;
9. int amount;
10. #endregion
11.
12. #region MonoBehaviour
13. void Awake ()
14. {
15. effect = GetTrackedEffect();
16. }
17.
18. void OnEnable ()
19. {
20. this.AddObserver(OnEffectHit, BaseAbilityEffect.HitNotification, effect);
21. this.AddObserver(OnEffectMiss, BaseAbilityEffect.MissedNotification, effect);
22. }
23.
24. void OnDisable ()
25. {
26. this.RemoveObserver(OnEffectHit, BaseAbilityEffect.HitNotification, effect);
27. this.RemoveObserver(OnEffectMiss, BaseAbilityEffect.MissedNotification, effect);
28. }
29. #endregion
30.
31. #region Base Ability Effect
32. public override int Predict (Tile target)
33. {
34. return 0;
35. }
36.
37. protected override int OnApply (Tile target)
38. {
39. Stats s = GetComponentInParent<Stats>();
40. s[StatTypes.HP] += amount;
41. return amount;
42. }
43. #endregion
44.
45. #region Event Handlers
46. void OnEffectHit (object sender, object args)
47. {
48. amount = (int)args;
49. }
50.
51. void OnEffectMiss (object sender, object args)
52. {
53. amount = 0;
54. }
55. #endregion
56.
57. #region Private
58. BaseAbilityEffect GetTrackedEffect ()
59. {
60. Transform owner = GetComponentInParent<Ability>().transform;
61. if (trackedSiblingIndex >= 0 && trackedSiblingIndex < owner.childCount)
62. {
63. Transform sibling = owner.GetChild(trackedSiblingIndex);
64. return sibling.GetComponent<BaseAbilityEffect>();
65. }
66. return null;
67. }
68. #endregion
69. }
Ability
As I mentioned earlier, I refactored some code out of the Battle State and added an Ability
component in its place. Among the jobs of this component will be to verify that an Ability can
actually be performed. When presenting a turn to a user, we will disable abilities in the menu that
can not be performed – which could happen as a result of lacking enough Magic points, or by a
status ailment like Silence. However, when it comes to an AI unit, we will allow them to try to use
abilities and fail due to the same types of exceptions. This way the user can enjoy a break or a
special strategy from time to time.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class Ability : MonoBehaviour
6. {
7. public const string CanPerformCheck = "Ability.CanPerformCheck";
8. public const string FailedNotification = "Ability.FailedNotification";
9. public const string DidPerformNotification = "Ability.DidPerformNotification";
10.
11. public bool CanPerform ()
12. {
13. BaseException exc = new BaseException(true);
14. this.PostNotification(CanPerformCheck, exc);
15. return exc.toggle;
16. }
17.
18. public void Perform (List<Tile> targets)
19. {
20. if (!CanPerform())
21. {
22. this.PostNotification(FailedNotification);
23. return;
24. }
25.
26. for (int i = 0; i < targets.Count; ++i)
27. Perform(targets[i]);
28.
29. this.PostNotification(DidPerformNotification);
30. }
31.
32. void Perform (Tile target)
33. {
34. for (int i = 0; i < transform.childCount; ++i)
35. {
36. Transform child = transform.GetChild(i);
37. BaseAbilityEffect effect = child.GetComponent<BaseAbilityEffect>();
38. effect.Apply(target);
39. }
40. }
41. }
Now let’s add the component which regulates the use of our Magical abilities – a component
which requires magic points in order to use an ability. We will listen to two of the noti cations
sent by the Ability component. We will listen to the CanPerform noti cation and “Flip” an
exception toggle when the unit doesn’t have enough magic points to cover the cost of using the
ability. We will also listen to the DidPerform noti cation to actually remove the speci ed number
of Magic Points.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AbilityMagicCost : MonoBehaviour
5. {
6. #region Fields
7. public int amount;
8. Ability owner;
9. #endregion
10.
11. #region MonoBehaviour
12. void Awake ()
13. {
14. owner = GetComponent<Ability>();
15. }
16.
17. void OnEnable ()
18. {
19. this.AddObserver(OnCanPerformCheck, Ability.CanPerformCheck, owner);
20. this.AddObserver(OnDidPerformNotification, Ability.DidPerformNotification, owner);
21. }
22.
23. void OnDisable ()
24. {
25. this.RemoveObserver(OnCanPerformCheck, Ability.CanPerformCheck, owner);
26. this.RemoveObserver(OnDidPerformNotification, Ability.DidPerformNotification,
owner);
27. }
28. #endregion
29.
30. #region Notification Handlers
31. void OnCanPerformCheck (object sender, object args)
32. {
33. Stats s = GetComponentInParent<Stats>();
34. if (s[StatTypes.MP] < amount)
35. {
36. BaseException exc = (BaseException)args;
37. exc.FlipToggle();
38. }
39. }
40.
41. void OnDidPerformNotification (object sender, object args)
42. {
43. Stats s = GetComponentInParent<Stats>();
44. s[StatTypes.MP] -= amount;
45. }
46. #endregion
47. }
Ability Menu
Next, will need a way to be able to select abilities in-game. At the moment, we have sort of tied
the “Attack” menu selection to the Attack ability, but we dont want to manually tie each ability to
an action, and we will only want to show the abilities a unit can actually use. We will accomplish
this dynamic menu setup by GameObject hierarchy and another component…
Ability Catalog
Add another script named AbilityCatalog to the same Ability directory. This script makes the
assumption that we will work with a particular hierarchy of GameObjects. There will be a
GameObject with the Catalog script. Then, any children of this GameObject will be considered
Categories of abilities. The names of the children will be treated as the names of the Categories
and will be displayed in the menu. Next, any children of the Cateogory GameObjects (or
grandchildren of the catalog object) will be assumed to be Abilities. The name of the Ability
GameObject will likewise be displayed in the menu.
1. using UnityEngine;
2. using System.Collections;
3.
4. /// <summary>
5. /// Assumes that all direct children are categories
6. /// and that the direct children of categories
7. /// are abilities
8. /// </summary>
9. public class AbilityCatalog : MonoBehaviour
10. {
11. public int CategoryCount ()
12. {
13. return transform.childCount;
14. }
15.
16. public GameObject GetCategory (int index)
17. {
18. if (index < 0 || index >= transform.childCount)
19. return null;
20. return transform.GetChild(index).gameObject;
21. }
22.
23. public int AbilityCount (GameObject category)
24. {
25. return category != null ? category.transform.childCount : 0;
26. }
27.
28. public Ability GetAbility (int categoryIndex, int abilityIndex)
29. {
30. GameObject category = GetCategory(categoryIndex);
31. if (category == null || abilityIndex < 0 || abilityIndex >=
category.transform.childCount)
32. return null;
33. return category.transform.GetChild(abilityIndex).GetComponent<Ability>();
34. }
35. }
First we will need to modify the LoadMenu method. The only speci c menu entry will be “Attack”
because I want that to always be an entry. Beyond that, we will show whatever category entries
exist in our ability catalog.
Next, we will need to modify the Con rm method. If attack is chosen, then we will call the Attack
method as we did before (though note that I also changed the “Turn” script’s “Ability” reference
from a GameObject to the new “Ability” script. If we select anything else, we will need to go into
the next state using whatever category we selected.
When a category has been selected, we need to dynamically load the Abilities contained within it.
As we did before, we will need to update the LoadMenu and Con rm methods, but this time we
will also be locking abilities which are currently un-usable. You can make that happen in this
weeks demo by simply using up your magic points. After a few turns the unit will regenerate
enough magic points to use the ability again.
Demo
Let’s con gure our Hero prefab to have a bunch of new abilities. We will leave the “Attack” ability
in place, but then we will add a sibling GameObject called “Ability Catalog” (make sure to add the
AbilityCatalog script) and add the following Categories, Abilities and Effects in a hierarchy like
this:
The following list shows the abilities I implemented in the repository. Note that I copied the
names and values from Final Fantasy Tactics Advance so you will want to replace or modify
them with something original. You can try to create them yourself using the various components
to assemble them, or refer to the prefab in the repository.
White Magic
Cure
Heal
Damage
Raise
Heal
Damage
Revive
Holy
Damage
Sagacity Skil
Water
Damage
Blind
In ict Blind
Drain
Damage
Absorb
After creating (or copying) the abilities I made this week, go ahead and try them out. There is a lot
to play test and make sure everything works as expected. I added a non-exhaustive list of things
to try out in this demo and it’s easy to tell that this was a huge lesson!
1. When you use a magical ability are the Magic Points of the caster going down?
2. Are abilities locked in the menu when you dont have enough magic points to use them?
3. At the start of a new turn do some Hit Points get regenerated?
4. Are health and magic stat points clamped between zero and the max values?
5. Do the correct effects apply to the correct targets? (Try adding an Undead component to one
of the Units while playing and verify that Cure becomes deadly)
6. Can Cure apply to a KO’d unit or do you have to use Raise to revive a KO’d unit rst?
Summary
In this lesson we created two new stat wrappers, one for Health and one for Mana. We also
created additional components so that we could create a greater variety of Abilities. In addition to
the effects we had before, we can now have unique effects which target the Undead, we can
Revive units and we can Absorb damage. We have now seen abilities which are physically based
and magically based, abilities which have multiple effects on a single target, abilities which have
unique effects for unique targets, and abilities which have effects dependent upon the success of
other effects. We updated the Ability Menu so that the Categories and Ability entries are dynamic.
We also added the ability to make exceptions on when an Ability can be used – our rst
exception being due to a magic point cost.
There were several les I modi ed for refactoring and bug xes here and there which I didn’t
necessarily call out. If you haven’t been checking the repository, I would de nitely suggest
looking over the commits this time around.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
It’s well past time to add some enemies to the board, but by now we’ve made all sorts of
modi cations to the “Hero” prefab which haven’t been propogated to the “Monster” prefab. We
could spend time manually setting up each new character to have the same kind of structure, but
I really don’t want to. Instead, we will use this lesson to lay the groundwork on a more exible
system. We will be making a factory to create and con gure new units for us, so that introducing
new and unique characters in the future will be a much simpler process.
Hero Prefab
Before you do anything, drag the Hero prefab into the scene so you can see its full hierarchy.
Open up the “Ability Catalog” and drag each ability into the project pane to create a prefab out of it
– we will load and use them again later. I put them in a Resources/Abilities/ folder with
subfolders for the categories. I put the “Attack” ability in a “Common” category subfolder. Refer to
the project repo so that your setup will match mine. Note that the hierarchy and naming matters
when using Resources.Load with le paths.
With that completed, let’s actually strip the “Hero” prefab back to the way the “Monster” prefab is
currently setup – no scripts and no children GameObjects except the “Jumper” which holds the
spheres we use to represent a character. By keeping our character models reduced to a very basic
form it will be much easier to add new characters.
Finally, we need to move both prefabs from the folder they currently sit in (“Prefabs”) into
Resources/Units. This will allow us to load character models as we need them, rather than require
us to maintain a reference to them. This also means you can remove the reference to the “Hero”
prefab from the BattleController script.
Recipes
The next step is nding a way to make both the “Hero” and “Monster” be the way that the “Hero”
had been before I made you revert all of the changes. The system needs to be exible so that the
same system can create the same setup on any model, with any compatible kinds of movement
components, jobs, and any combinations of sets of abilities, etc.
To manage this idea, I decided that I would pass a “recipe” for a unit to the “factory” which
contained all the speci cs of the setup I wanted. Go ahead and add a new script named
UnitRecipe to the Scripts/Factory directory.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class UnitRecipe : ScriptableObject
5. {
6. public string model;
7. public string job;
8. public string attack;
9. public string abilityCatalog;
10. public Locomotions locomotion;
11. public Alliances alliance;
12. }
The rst several elds are simple string data types. These strings are the names of a resource
which the factory will be able to load at run time. Note that this means I also need to move the
prefabs into a “Resources” folder in order for the whole process to work.
You might wonder why I didn’t simply add GameObject references directly to the prefab instead
of having to copy their name. The reason I chose not to do this is because several of these types
of objects, such as jobs, are dynamically created based on other les like spreadsheets.
Sometimes I can update them in place, but if I ever wanted to make major changes I might decide
to delete the prefabs and let the system rebuild them from scratch. At that point, any other project
resource which was referencing the original job will be disconnected, even though I would end up
creating another object in the same place with the same name.
Another possibility is that either the model, or job, etc would also become a more complex
subsystem. I could for example create another “recipe” style system for one of the features of the
unit and then I could modify the Factory to load the recipe instead of the GameObject I currently
have in place. The abilityCatalog eld is an example of such a subsystem. It is a string name
which will load another recipe called AbilityCatalogRecipe.
Go ahead and create another script in the same folder for the AbilityCatalogRecipe.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AbilityCatalogRecipe : ScriptableObject
5. {
6. [System.Serializable]
7. public class Category
8. {
9. public string name;
10. public string[] entries;
11. }
12. public Category[] categories;
13. }
This recipe is similarly setup with simple strings. I have a category name which relates to the
categories of abilities like “White Magic” we used previously. Then you will see an array of string
entries for the actual abilities like “Cure”. The idea behind this recipe is that you can easily make
a few reusable catalogs for enemies. Perhaps certain mage enemies will share certain spells, etc.
Instead of recreating this structure on all of the enemies that use it, you can simply point to the
shared recipe.
The “Unit Recipe” also had elds for “Locomotions” and “Alliances”, both of which I haven’t de ned
yet. These are both enums which help the factory know what kind of component to add, and/or
how to con gure a component.
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Locomotions
5. {
6. Walk,
7. Fly,
8. Teleport
9. }
The Locomotions enum will be used to determine what kind of Movement component to add to a
unit. I could have skipped the enum and simply passed the name of the component’s class
directly, but this way I can refactor the names of the classes without breaking anything.
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Alliances
5. {
6. None = 0,
7. Neutral = 1 << 0,
8. Hero = 1 << 1,
9. Enemy = 1 << 2
10. }
The Alliances enum decides what “side” of a battle you are on – primarily either the player’s side
(the Hero) or the computer’s side (the Enemy). Each unit created will only have a single type of
Alliance, but I marked them as a bit mask for exibility in the future. For example, an enemy AI
might normally target the units of the Hero alliance, but others might also want to attack Neutral
units. Or if they were under some sort of status effect they might attack every kind of unit
including other enemies. Bit-masks allow me to handle these combinations with a single eld.
The Alliances enum type will be applied to another component of a similar name. Add a new
script named Alliance to the Scripts/View Model Component/Actor directory.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Alliance : MonoBehaviour
5. {
6. public Alliances type;
7. }
The component is super simple for now, but I plan to extend its functionality in the future.
Perhaps it will hold additional elds for who it consideres a friend and foe (which will be great
targets for status effects to modify). I wont want to change the intial “type” eld because the other
units on the eld who are not under a status effect still need a way to determine the “base”
alliance type of a unit.
Asset Creator
Scriptable objects need to be created by some other script. We have done this before to create
ConversationData and now we will want to add a few more:
1. [MenuItem("Assets/Create/Unit Recipe")]
2. public static void CreateUnitRecipe ()
3. {
4. ScriptableObjectUtility.CreateAsset<UnitRecipe> ();
5. }
6.
7. [MenuItem("Assets/Create/Ability Catalog Recipe")]
8. public static void CreateAbilityCatalogRecipe ()
9. {
10. ScriptableObjectUtility.CreateAsset<AbilityCatalogRecipe> ();
11. }
Once you have created your rst recipe you can always duplicate it and modify the copy for the
changes needed, or start from scratch as you like. I have created several unit recipes and ability
catalog recipes which you can feel free to copy from the project repository.
Unit Factory
Now that we have our recipes in place, its time to add the script which accepts them and returns
a unit. Create a new script named UnitFactory and add it to a new Scripts/Factory folder. This
script is a bit on the long side, so I will break it down to just a little bit at a time.
1. using UnityEngine;
2. using System.IO;
3. using System.Collections;
4.
5. public static class UnitFactory
6. {
7. //... Add next code samples here
8. }
The class itself is a static class. I don’t need any instance of a factory, because the factory doesn’t
need to hold any instance data. Everything it will need to do its job (the recipe) will be passed
along as a parameter anyway.
The interface for the class is pretty simple, I have overloaded a method called Create which takes
either a name of a recipe, or a recipe itself, along with the level we want the unit to be created at.
The version of Create which takes the name of a recipe attempts to load the recipe for you and
then passes it along to the other method. If it cant nd the recipe it prints an error message to the
console and returns a null object.
The version of Create which takes the recipe instance does the real work of creating a unit. It is
worth pointing out that creating an object like this can have certain bene ts over having all the
components pre-existing on a prefab:
1. Should you decide to change the setup of a unit you only have to modify the factory class
rather than ALL of the instances of your prefabs.
2. Some components are dependent on other components and will need to initialize themselves
based on the data in the other script. By manually adding them in the correct order, you are
also controlling the initialization order. In contrast, components which are already on a
GameObject (like in a prefab) will initialize in a random order and are subject to race
conditions. Sometimes a setup may appear to work ne, and then it will stop working leaving
you to wonder what you could have done to break it. Note that another solution is via Edit-
>Project Settings->Script Execution Order.
3. Sometimes prefabs break in a variety of ways such as if they get corrupted, lose references to
scripts due to renaming, etc. If you don’t remember how the object was con gured it might be
a pain to recreate. Scripts are much more reliable and are also better for version control.
Several of the methods will make use of this InstantiatePrefab method. It attempts to load a
GameObject from Resources according to the name you provide. If it can’t nd a prefab by that
name it will print an error in the console and tell you the name of the object you had asked it to
load. These little messages are really helpful to indicate when you have a typo, or have missed an
asset in one form or another.
In the event that a prefab is found, the method goes ahead and instantiates a clone and then
returns the new instance to you.
A lot of the statements in the Create method are little one-off methods to do a simple task. For
example, the Unit component didn’t require any con guration at the moment so I simply used the
AddComponent method on the object we had created. However, the Job component needs to be
added and con gured, so I created a little method to handle both in one step. This way I can easily
view the overall construction process of the unit and not worry about the little details. It is also
easy to reorder the “steps” of construction if necessary.
The AddAbilityCatalog is another construction step method like the several preceeding ones, but
it is a bit more complex. It dynamically creates an object hierarchy to match the one we had
setup on the “Hero” prefab for it to use the “Ability Catalog” where we begin with a base
GameObject, then children GameObjects (one per category), then grandchildren GameObjects
(one per ability).
We use the name of an ability catalog to load the instance of the recipe from a Resources.Load
call, and then loop through the content of that recipe using a nested loop. The outer loop iterates
over the categories of the recipe, and the inner loop iterates over the abilities within each
category.
Before we had the “Unit Factory” we simply spawned and con gured unit prefabs in the
SpawnTestUnits method of the InitBattleState script. I’m not quite ready to remove the temp code
completely, but we can clean it up a bit and start using our new factory!
Note that the list of unit recipes are all resources which I created and added to the project. You
can feel free to create and use your own, or of course, grab a copy of the ones I made in the
repository.
As a random side note, I googled lists of names to come up with some good names for the three
basic hero jobs I had created. I found the following:
I didn’t bother to name any of the monsters, but I am sure I would come up with something really
clever like, “Goblin” if I actually had some kind of matching art to accompany it!
There is still a good bit of temporary code here that wouldn’t exist in a fully implemented version.
For example, de ning what enemies will spawn, how many there will be, and where they appear,
could all be saved as part of the level data. The list of heroes would also already exist in a party
data object somewhere else, because they have information which needs to persist between
multiple battles.
Next I grabbed a list with a copy of all the tiles on the board. Then I randomly grab one of the tiles
to use as a spawn location for one of the units. I remove the tile I chose from the list so that I wont
accidentally select it a second time and try to spawn two units at the same location.
I give each of the units a random rotation, just because. Then I add the spawned unit to the
BattleController’s list of units as we did previously, and nally select whatever tile we had
spawned the rst unit onto.
Demo
At this point you should be able to press play and see both our “Hero” and “Monster” characters
populate the board. The game hasn’t really changed at all, but I still think its more fun to have
clear enemies and allies (even if you are still controlling them both)!
Summary
In this lesson we paved the way for us to have more than one type of unit in our game by
stripping the initial unit to its base form and then rebuilding it with a factory. It should be easy to
add many new characters (including upgrading to actual character models with animations, etc)
as well as new abilities, jobs, and well, pretty much everything else.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
Now that we have enemies, we can also provide an actual “goal” for the battle. First we must be
able to actually defeat the enemies, as well as risk defeat for our own units. There needs to be a
consequence for a unit’s hit points dropping to zero, so we will add a “Knock Out” status effect
which disables a unit from acting or taking additional turns. Likewise there should be an effect
for defeating all enemy units, or allowing all hero units to perish. These are sample “victory
conditions” which we will track, and which will allow the battle to end.
I’ve decided to implement “Knock-Out” as a Status Effect – for obvious reasons. It modi es a
particular unit in a particular way, it has a condition of application (no hit points), and it can be
removed, such as by a “Revive” ability. Add a new script named KnockOutStatusEffect to the
Scripts/View Model Component/Status/Effects folder.
At the moment this status effect is responsible for three basic things:
1. Visual indication of a KO – when the status is enabled the unit it applies to will appear
squashed. In a complete game, you might use this opportunity to ip a toggle on an
animation controller so that it plays a death state animation.
2. Disabling the incrementation of the turn counter stat. This way a KO’d unit wont be able to
build up a huge supply of counter points and be able to take multiple turns in a row if revived.
3. Disabling the ability to take turns. This way if a unit already had a high enough counter to act,
it still wont be given a turn.
The visual aid of squashing a unit is applied in the OnEnable method and removed in the
OnDisable method by modifying the scale of the transform. Both of the other tasks require
listening to noti cations – note that we must specify a sender so we don’t block ALL units from
taking turns. In the noti cation handlers, we simply ip the exception’s toggle in order to disable
an action.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class KnockOutStatusEffect : StatusEffect
5. {
6. Unit owner;
7. Stats stats;
8.
9. void Awake ()
10. {
11. owner = GetComponentInParent<Unit>();
12. stats = owner.GetComponent<Stats>();
13. }
14.
15. void OnEnable ()
16. {
17. owner.transform.localScale = new Vector3(0.75f, 0.1f, 0.75f);
18. this.AddObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification, owner);
19. this.AddObserver(OnStatCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), stats);
20. }
21.
22. void OnDisable ()
23. {
24. owner.transform.localScale = Vector3.one;
25. this.RemoveObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification,
owner);
26. this.RemoveObserver(OnStatCounterWillChange,
Stats.WillChangeNotification(StatTypes.CTR), stats);
27. }
28.
29. void OnTurnCheck (object sender, object args)
30. {
31. // Dont allow a KO'd unit to take turns
32. BaseException exc = args as BaseException;
33. if (exc.defaultToggle == true)
34. exc.FlipToggle();
35. }
36.
37. void OnStatCounterWillChange (object sender, object args)
38. {
39. // Dont allow a KO'd unit to increment the turn order counter
40. ValueChangeException exc = args as ValueChangeException;
41. if (exc.toValue > exc.fromValue)
42. exc.FlipToggle();
43. }
44. }
Note that I exposed the BaseException‘s “defaultToggle” eld, although I still block changes to it.
1. public readonly bool defaultToggle;
This change was necessary because I couldn’t just blindly change the exception toggle, or our KO
status could end up allowing units which normally couldn’t take turns (because they didn’t have
a high enough move counter), to now take turns. It is only when a KO’d unit has a high enough
move counter, and so normally could take a turn, that we need to make a ip. An alternate x
could have been to have the TurnOrderController post different noti cations, one for when a unit
can take a turn and one for when it cant. Then we could have listened to a more speci c event
and ipped the exception toggle without any extra checks.
The KO status effect can be removed, but the question is who should be responsible for removing
it? There are two main options. The rst option is that we could have the same class which
in icts the KO status effect be responsible for removing it. This isn’t always a great solution
because the “in ictor” may need to go out of scope before the status would need to be removed.
For example, if an enemy unit uses an ability to in ict a status ailment, and then you defeat the
enemy, we could choose to remove that enemy from battle. Of course if we did, then it wouldn’t be
there to remove the status it had in icted.
This dependency chain could cause problems in the other direction as well. We could change the
previous example so that the enemy unit didn’t die, but perhaps the status which was in icted
was “cured” by a different ability at a different time. When the “duration” condition of the enemy
ability would be completed and the enemy ability would then try to remove the status – it
wouldn’t nd it (or if it had maintained a reference, then there might be some other kind of
problem if the Status Effect GameObject had been destroyed or unparented from the expected
hierarchy, etc.).
Both of the previous problem cases are not an issue when using self-maintaining status
condition components. For example when an enemy ability in icts a status ailment it normally
attaches it with a DurationStatusCondition and then it does not need to be concerned with
keeping any references to remove the status later, nor does it care if a different ability is able to
cure the status effect.
This sort of custom component is our second option, and is the option I have chosen for this use-
case. Create a new script named StatComparisonCondition and place it in the same folder as the
other StatusCondition scripts.
1. using UnityEngine;
2. using System;
3. using System.Collections;
4.
5. public class StatComparisonCondition : StatusCondition
6. {
7. #region Fields
8. public StatTypes type { get; private set; }
9. public int value { get; private set; }
10. public Func<bool> condition { get; private set; }
11. Stats stats;
12. #endregion
13.
14. #region MonoBehaviour
15. void Awake ()
16. {
17. stats = GetComponentInParent<Stats>();
18. }
19.
20. void OnDisable ()
21. {
22. this.RemoveObserver(OnStatChanged, Stats.DidChangeNotification(type), stats);
23. }
24. #endregion
25.
26. #region Public
27. public void Init (StatTypes type, int value, Func<bool> condition)
28. {
29. this.type = type;
30. this.value = value;
31. this.condition = condition;
32. this.AddObserver(OnStatChanged, Stats.DidChangeNotification(type), stats);
33. }
34.
35. public bool EqualTo ()
36. {
37. return stats[type] == value;
38. }
39.
40. public bool LessThan ()
41. {
42. return stats[type] < value;
43. }
44.
45. public bool LessThanOrEqualTo ()
46. {
47. return stats[type] <= value;
48. }
49.
50. public bool GreaterThan ()
51. {
52. return stats[type] > value;
53. }
54.
55. public bool GreaterThanOrEqualTo ()
56. {
57. return stats[type] >= value;
58. }
59. #endregion
60.
61. #region Notification Handlers
62. void OnStatChanged (object sender, object args)
63. {
64. if (condition != null && !condition())
65. Remove();
66. }
67. #endregion
68. }
It’s a semi-long script but functionality-wise is very simple. It has a few elds, one for a type of
stat to observe, one for a value to compare against, and one for a special delegate which returns a
bool data type. The script provides several convenience methods which you can use for that
delegate (EqualTo, LessThan, etc) but you are also able to provide your own if necessary. You
con gure the component by calling the Init method and passing along parameters for each of
those elds.
We will be able to use this script for our KO status condition by passing along a stat type of “HP”, a
value of zero, and we can use “EqualTo” for our condition delegate. What this means is that every
time the “HP” stat changes, the script will check whether or not “HP” equals zero, and if not, then
the condtion will be removed. Assuming that this is the only condition causing the “KO” status,
then the status will also be removed.
Next we need something to actually apply the KO status whenever a unit’s HP are reduced to zero.
That could be done in the Stats or Health components, but I really like all of my components to be
as simple as possible and to really just have a single “job”. For example, I could say that the “job” of
the Health component is to “maintain a relationship between two stats- to make sure that HP
never holds a value less than the min or greater than the max HP”. I would need to use “and” in
my job description to say that the component also applies KO status whenever one of the stats
reaches zero, which really means that it is another job, and is suitable to another component.
So I decided to create a new script whose job is to “apply status effects which occur as a result of
events rather than direct actions”. If the script were to get very large I might break it down into
one script per status effect. For now, just create a new script called AutoStatusController and
place it in the Scripts/Controller directory.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AutoStatusController : MonoBehaviour
5. {
6. void OnEnable ()
7. {
8. this.AddObserver(OnHPDidChangeNotification,
Stats.DidChangeNotification(StatTypes.HP));
9. }
10.
11. void OnDisable ()
12. {
13. this.RemoveObserver(OnHPDidChangeNotification,
Stats.DidChangeNotification(StatTypes.HP));
14. }
15.
16. void OnHPDidChangeNotification (object sender, object args)
17. {
18. Stats stats = sender as Stats;
19. if (stats[StatTypes.HP] == 0)
20. {
21. Status status = stats.GetComponentInChildren<Status>();
22. StatComparisonCondition c = status.Add<KnockOutStatusEffect,
StatComparisonCondition>();
23. c.Init(StatTypes.HP, 0, c.EqualTo);
24. }
25. }
26. }
This script will need to be attached to an object in the battle scene. I have attached it to the “Battle
Controller” gameobject. Once this is done, you could play the scene and see how “KO” works. Try
attacking a single unit until its HP are reduced to zero. It should squash down. Wait a few turns to
see that the defeated unit is not allowed to take a turn. Now use another unit to revive the KO’d
unit – it should scale back to its normal size and be allowed to take turns again.
Health
I mentioned in the previous lesson that you could modify the Health component to have a
“Minimum Hit Point” eld so you could specify values other than zero and have some enemies
not actually be KO’d. I went ahead and added that feature. Check out the repository if you need
help, but basically I reference the minimum eld in all the places I had been referencing zero.
I added this so that I could have special victory conditions where you are targeting a single unit,
and that single unit needs to be able to live another day for a typical RPG sort of story – it
escapes!
Victory Conditions
Now that we have enemies, we can have a real game. By this I mean that we can have some sort
of conditions upon which to base whether we win or lose. Create a new script named
BaseVictoryCondition and add it to the Scripts/Controller/Victory Conditions directory. I will
break this script down into several bits:
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class BaseVictoryCondition : MonoBehaviour
5. {
6. public Alliances Victor
7. {
8. get { return victor; }
9. protected set { victor = value; }
10. }
11. Alliances victor = Alliances.None;
12.
13. // ... Add next code samples here
14. }
Note that this class is abstract – it provides some common functionality that will be reused, but it
only forms the base for other subclasses. In a fully implemented game like Final Fantasy Tactics
there are a lot of different missions with a lot of different objectives, for example:
You might need to nd an item(s) on the game board and enemies might keep spawning until
you either nd them or are defeated.
You might need to escort a unit and either keep it from being defeated for a certain number of
turns, or just defeat all of the enemy units before they can get to it.
You might need to simply defeat all enemy units.
You might need to defeat a particular enemy unit.
To external classes, the only thing that appears in this class is the Victor property. When a battle
has just started, or is in progress, the property will return “None”, meaning that there is no winner.
The script will be self managed and listen to events posted by other classes to determine when a
winning or losing condtion has occured and then it will update the property accordingly.
Sometimes I would post an event for something as “important” as deciding that a battle has been
decided, but in this case, I will simply check for a Victory whenever changing states.
We will need a reference to the BattleController (so we can reference other game content like the
list of units) so I added a eld and assigned it in the Awake method. Note that the methods are
marked as virtual in case subclasses need to extend or change any of the logic.
Next, I register for noti cations that a Unit’s hit points have changed. Since I don’t specify a
“sender” of the noti cation in the parameters, it will listen to ANY sender. Note that this will
check for a GameOver every time a unit’s HP actually changes – even if it is getting healed.
Actions in a turn based game like this don’t occur very rapidly so we wont notice a perfomance
issue, but in another game, I might want to listen to more speci c events, such as that the
HitPoints were fully depleted.
1. protected virtual bool IsDefeated (Unit unit)
2. {
3. Health health = unit.GetComponent<Health>();
4. if (health)
5. return health.MinHP == health.HP;
6.
7. Stats stats = unit.GetComponent<Stats>();
8. return stats[StatTypes.HP] == 0;
9. }
With this method I can determine if any particular unit is considered “defeated” or not. Since we
added the “Minimum Hit Point” feature to the Health component, I need to check whether or not
that value has been reached. I suppose that it is possible that there may be scenarios where some
unit’s may not have a Health component (although I dont know what that reason would be) so as
a fallback, I simply check the stats component.
Next I have a method which can determine if any “Party” of units has been defeated. This method
loops through all the units which the Battle Controller has a reference to. If a single unit of the
party alliance is “not” defeated, then I can immediately return that the party is not defeated. If I
have looped through all the units and not found an exception, then the party “is” defeated.
Finally, here is the noti cation handler which checks whether or not the “Hero” party has been
defeated any time any unit’s hit points have been changed. I only check for this because all
mission types can be “lost” by letting the entire hero party be defeated, on the other hand,
defeating all of an enemy party may not necessarily constitue a “win”. I will let the subclasses
extend from here and determine that.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class DefeatAllEnemiesVictoryCondition : BaseVictoryCondition
5. {
6. protected override void CheckForGameOver ()
7. {
8. base.CheckForGameOver();
9. if (Victor == Alliances.None && PartyDefeated(Alliances.Enemy))
10. Victor = Alliances.Hero;
11. }
12. }
Add another subclass named DefeatTargetVictoryCondition. In this variation, we only care about
a single target. If you can KO the single target you can win even with other enemy Units still
active on the board. Ideally when this victory condition is in play the user should be noti ed both
in the mission objective (before beginning the battle) as well as in the scene with some sort of
marker indicating who the target actually is.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class DefeatTargetVictoryCondition : BaseVictoryCondition
5. {
6. public Unit target;
7.
8. protected override void CheckForGameOver ()
9. {
10. base.CheckForGameOver ();
11. if (Victor == Alliances.None && IsDefeated(target))
12. Victor = Alliances.Hero;
13. }
14. }
Because maps might be reusable with different enemy sets or story elements, some portions of
the battle con guration should be dynamic. In other words, we don’t want to attach a Victory
Condition to anything in the scene, but instead would load one during the init step of the battle,
based on some sort of external setting.
Despite my good advice, I don’t have any such external setting yet. I simply speci ed a particular
entry and manually con gured it. I chose the Victory condition where you don’t actually have to
defeat the entire enemy party, you merely need to defeat the enemy leader. In addition I modi ed
the “Minimum Hit Point” eld of the target enemy so that he isn’t actually defeated. He will live to
ght another battle!
1. void AddVictoryCondition ()
2. {
3. DefeatTargetVictoryCondition vc =
owner.gameObject.AddComponent<DefeatTargetVictoryCondition>();
4. Unit enemy = units[ units.Count - 1 ];
5. vc.target = enemy;
6. Health health = enemy.GetComponent<Health>();
7. health.MinHP = 10;
8. }
Note that you will need to invoke this AddVictoryCondition method from somewhere. I added a
call just after the SpawnTestUnits call in the Init method.
Just like there is an “intro” cut-scene for our battle, there will often need to be an “outro” cut-
scene. Whenever this state enters, it should load the story based on whether battle has ended or
not. In some cases you may also wish to have different scenes play based on whether you won or
lost. Of course if you lose, you could just go to a Game Over screen. The example below does not
re ect nal code. A better example would not hard code references to resources because they will
need to change based on the level you are playing. Note that I moved the data management from
the Awake and OnDestroy methods to the Enter and Exit methods so that it could change.
The content of the conversation data doesn’t really matter. Feel free to create your own or grab
the ones from my repository. Just make sure that you either update the resource strings or use
the same names and paths.
Likewise, when the state is exiting, it needs to go to an appropriate state – if the battle has ended
it will go to a new EndBattleState
I declared the IsBattleOver and DidPlayerWin methods in the base class BattleState as follows:
The end battle state is really just a placeholder for now which causes the scene to reload. It could
be replaced by a sequence of several other states, such as showing what items you had earned
and how much experience you had gained. Eventually it would be responsible for returning to
some other part of the game where you can manage your team, choose other missions, etc.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class EndBattleState : BattleState
5. {
6. public override void Enter ()
7. {
8. base.Enter ();
9. Application.LoadLevel(0);
10. }
11. }
It is after performing an ability that you are most likely to have triggered a victory condition. We
will need to pay close attention to what has happened – you might even have KO’d yourself, and
we need to act in a way which makes sense under various conditions.
1. IEnumerator Animate ()
2. {
3. // TODO play animations, etc
4. yield return null;
5. ApplyAbility();
6.
7. if (IsBattleOver())
8. owner.ChangeState<CutSceneState>();
9. else if (!UnitHasControl())
10. owner.ChangeState<SelectUnitState>();
11. else if (turn.hasUnitMoved)
12. owner.ChangeState<EndFacingState>();
13. else
14. owner.ChangeState<CommandSelectionState>();
15. }
I modi ed the Animate method so that it has a few more conditions which determine which state
will follow. If a victor has been declared, and therefore the battle is over, then we change to the
Cut Scene State where we can play our outro scene. If the unit has killed itself (whether by
accident or not who can tell) then we don’t bother letting it continue its turn (such as moving and
or deciding on an end facing direction.
1. bool UnitHasControl ()
2. {
3. return turn.actor.GetComponentInChildren<KnockOutStatusEffect>() == null;
4. }
For now, KO is the only status which would disable a turn, so I simply checked for it directly. If
there were more ways to stop you in the future I might post an event instead and allow status
effects the ability to respond through exception toggles.
Stat Panel
There is one bit of polish we ought to make now that we can determine the difference between
hero and enemy units. Open up the StatPanel script and make a small change to the Display
method by swapping this:
with this:
Facing Indicator
Since I am doing a little extra polish, I also brough in the Facing Indicator from the prototype
project. This simple script displays a highlighted sphere in the facing direction of a unit. It is
pretty obvious what direction a unit is facing anyway, but without the indicator it isn’t always
obvious what the purpose of the active state is.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class FacingIndicator : MonoBehaviour
5. {
6. [SerializeField] Renderer[] directions;
7. [SerializeField] Material normal;
8. [SerializeField] Material selected;
9.
10. public void SetDirection (Directions dir)
11. {
12. int index = (int)dir;
13. for (int i = 0; i < 4; ++i)
14. directions[i].material = (i == index) ? selected : normal;
15. }
16. }
It is constructed with four simple sphere objects parented to a central empty game object. Each
sphere is offset in the direction that the unit would be facing and they are scaled smaller so they
look nice. Parent the indicator to the Tile Selection Indicator. Feel free to grab it from the
repository if you struggle making your own.
Add a eld for it to the BattleController and hook it up in the scene. Have the Game Object begin
as disabled. Then open up the EndFacingState. Enable the Game Object as well as call
SetDirection on Enter, and keep it updated inside OnMove.
1. owner.facingIndicator.gameObject.SetActive(true);
2. owner.facingIndicator.SetDirection(turn.actor.dir);
Demo
Go ahead and play the scene. You are still controlling both the hero and enemy units, so play once
where the heroes win, and once where they lose. Don’t forget to try reviving a unit – enjoy!
Summary
In this lesson we actually started turning this project into a real game. It could potentially be fun
if played by two local players, one controlling the hero party and one controlling the enemy party.
Now, when units are KO’d, they are not able to take new turns, and when a whole party is KO’d the
battle is over. Just for fun, we added some outro conversation data. We also added some polish
here and there.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
I’m currently hard at work preparing A.I. for this project. I was hoping to have it ready to share
today – it works, but I am still polishing the code and working on writing the accompanying
tutorial. While you wait, I decided I could share the decision making process I followed while
architecting this portion of the project to help whet your appetite.
Intro
There are two main roads I could have taken for implementing AI in this game. I would call one
road the Smart A.I. and the other Dumb A.I.. I provided these labels based on the amount of “hand-
holding” I would have to provide in order for the computer to make seemingly intelligent
decisions.
In the realm of Smart A.I., there are algorithms which could allow me to do little more than tell
the computer what the rules of the game are and how to determine a victor. From there, it can
simulate its own playthroughs and develop its own strategies based on the resulting data. It may
even come up with strategies I had never thought of!
The other path is what I would call Dumb A.I. – note that this doesn’t mean the resulting
computer controlled moves will be poorly made, it simply means that it will only make moves as
strategically as I can teach it to make. I would be trying my best to help it “think” like I would and
make decisions based on the data immediately available.
Smart A.I.
When I rst thought about creating A.I. for this project, I had thought it might be fun to do a really
“smart” implementation – by this I mean an opponent which “thinks” several turns ahead to make
the best use of its abilities and resources. I have experimented with a few different algorithms
which are commonly used for strategy games such as “MiniMax” and “Monte Carlo Tree Search”.
These are both great systems and I would recommend you spend some time studying each. I am
not going to “teach” either system in this lesson, but I did feel it could be bene cial to discuss why
I felt they weren’t the best t for this project.
MiniMax
MiniMax (with Alpha-Beta pruning) is a great system for making sure that a computer opponent
picks the best possible move, all without actually having to check every possible branch in a
move tree. Since the number of possible moves in a turn for our game is quite large, that would
seem like a great feature! However, in order to use this pruning ability, we must rst come up with
a way to “Score” all the possible moves.
At rst you might not think that would be so hard – just sum the hit points of each team and
compare them. If you approach the score of a move in this way, then both damaging an opponent
and healing a teammate could result in better scores. However, a good tactics game can perform
a lot more abilities than merely attacking and curing. How would in icting a status ailment be
scored? Blind is really helpful against a physically strong opponent, and Silence is really helpful
against a magically strong opponent, and neither of those tactics would receive a good score with
our Hit Point based initial approach. Likewise laying a trap on a tile doesn’t immediately have an
effect on either side – how could it be scored?
Attempting to write a system to score moves which take into account all of the possible
strategies in a game this large would be dif cult, if not impossible, so for now I decided to skip
this option.
MCTS has a lot of great features too. One of the rst bene ts of this system also happens to be a
solution to our previous problem – I don’t have to be able to write a system which can score
moves. If I can play a simulation to its completion (i.e. one side defeats the other) then I can
simply use the result of the game to determine when one path is better than another. The
strategy used to get there didn’t need to be “understood” beforehand, but given enough time it will
nd and “think” of patterns you may have never considered.
Unfortunately there are several crippling factors for this approach as well. For example, a
simulation could theoretically play forever- units could move back and forth blindly on opposite
sides of the board, never nding each other, and therefore never complete a battle. Another
possible scenario is that by random chance they may keep picking only to heal themselves and
or team mates, or pick offensive moves which never reduce hit points.
You can get around the in nite-game problem by limiting the depth of the simulation to a certain
number of moves and then rating the overal game. For example, say you played for 50 turns and
the battle had not been ended, you could simply mark the game as complete and pick a winner by
number of active units and or hit points etc.
There is still another problem – the game tree complexity. Even though MCTS doesn’t have to
consider every possible option in order to seemingly pick something “smart”, it needs to be able to
consider a “large enough” spectrum of the moves to appear as smart as a human would. The
number of required moves to check which would ful ll that requirement is probably far larger
than one would imagine.
To put things into perspective let’s consider a very simple game. I read that Tic-Tac-Toe has
255,168 leaf nodes in it’s game tree. This is a super simple game with at most 9 options on the rst
turn, and with each turn having less move options, and there is a de nite end game scenario
relatively quickly. Still, I would not have guessed the tree would grow to a couple hundred
thousand options.
Chess takes the example a little closer to the level of complexity that our game enjoys. There is a
larger board, more pieces, different move rules, etc. I read that there are 10^120 different board
combinations – thats huge! Still, Chess doesn’t necessarily force an end, and the number of move
options per turn can change, but if you played for say 50 turns deep with an average of 20
possibilities per turn then you would still have, well… a really big number… (20 * 20 * 20 etc – yep
really big).
Our game can have a larger board than Chess. It may have less pieces, but the facing direction
coupled with a large variety of individual stats, including changeable stats like hit points and
magic points, buffs and debuffs, etc, make for a number of board combinations which far
surpasses the complexity of Chess.
That simple (and conservative) sequence is (25 * 25 * 4 = 2,500) options to consider. Still, it is not
even the full number of options a single unit could pick from. For example, the unit could choose
to attack before or after moving. Units also have more than one ability to pick from and this is all
within A SINGLE TURN – imagine how quickly the tree would grow when trying to look multiple
moves ahead!
With all of that said, I lack the con dence that it is possible for a modern computer to be able to
consider a “large enough” spectrum of the possibilities to make decisions that look smart, and
even if it could, I would certainly need to let the AI calculate for awhile – which means it should
think on another thread, otherwise the game will appear to freeze. Unfortunately Unity doesn’t
support multi-threading, and my architecture is currently bound to it, so either way this is
another dead-end.
Dumb A.I.
When I say “dumb A.I.” I am imagining one which does not consider the effects of its choices. It
simply makes choices based on whatever it was speci cally told to do. For example, it could
perform attacks according to a pre-set pattern, which can still look kind-of smart since the
pattern was input by a designer, but which may or may not look terribly smart in practice. For
example, if the A.I. already had full hit points but decided to heal itself then it would look kind of
dumb.
When I thought about it a little more, I realized that there is actually something rewarding about a
dumb A.I. in a tactics game. For example, if I siphon all of an opponent’s magic points, and then
the opponent attempts to cast a magic spell and therefore wastes a turn, I nd the feeling to be
very rewarding! Likewise, if I have abilities that let me absorb elemental damage, then it is
exciting for an enemy to attack me using that element. Smart A.I. would potentially notice these
outcomes and avoid them, thus removing some of my favorite moments of combat, but a dumb
A.I. helps a player feel smart!
A dumb A.I. can be implemented with a few fairly simple rules and conditions, and can still easily
scale over the course of the game to feel “harder” simply by raising the number of opponents
and/or by raising their stats, etc. It is the path I decided to follow, but even within this decision
there are plenty of options. I looked at a few systems for inspiration, both of which Final Fantasy
has used in the past: the Gambit System from Final Fantasy 12 and then looked all the way back
to the simple attack sequence for AI in Final Fantasy 1.
Gambits
Put simply, the Gambit system is a list of abilities to perform, but rather than performing them
sequentially, they are performed based on priority. Entries which appear rst have the rst
chance of execution. Each entry also has a condition, and it is only when that condition fails that
a lower priority ability would have the opportunity to execute. For example, you could say
something like:
This sequence of the entries in a gambit list would be considered in order – so that the
resurrection of KO’d units had top priority in my sample. Of course, most of the time your allies
are not KO’d so that step is skipped. If you just started a battle, your HP is probably higher than
50%, so that step is skipped. In special cases you might cast cure to attack an undead, but you
might also be out of Magic Points in which case that step could also be skipped until nally you
simply attack. In that way, the rst action which could be used, would be used, and there would be
a certain logic and priority to the actions which are taken.
I like this approach because it could solve some of the extra “dumb” moments of an A.I. – in
particular the ones which didn’t give the player a sense of accomplishment. For example, if a unit
tries healing itself or another unit which already had full hit-points. When a condition
accompanies the action then the action wont look quite as dumb. I also think this could help add
a little extra “something” to boss ghts where the bosses feel just a little bit harder than a normal
enemy.
On the other hand, I wouldn’t want the A.I. to be quite so rigid. I don’t want it to cast blind on you
immediately after you had remedied it, or to heal itself immediately when its HP is less than 50%.
If it did, then the players might feel that the boss was too hard, and not in a fun way. Ultimately I
took some inspiration from the Gambit system, such as pairing a target with an ability, but
decided not to implement it as is.
Sequences / Patterns
I must again say thanks for all the great people who put together strategy guides! I was able to
reference the “Final Fantasy Game Mechanics Guide” by AstralEsper on gamefaqs to see how
Final Fantasy 1’s A.I. played out. The system was basically just a sequential list (with a few other
steps and conditions). So for example, on an enemy turn it would decide what to do based on the
evaluation of four potential states: Run, Use a Spell, Use a Skill, or Attack. The order of the states
is also the priority of the action – if whatever condition is met in order to run, then the enemy
will run, etc.
Likewise, the Spells and Skills that an enemy could use were also provided as a repeating list. For
example, the enemy named “BigEYE” would always use skills in a repeating: “Gaze-Flash-Gaze-
Flash” pattern. The “WzOGRE” (an Ogre Mage) enemy had a more interesting pattern for magic
spells: “RUSE-DARK-SLEP-HOLD-ICE2”. Any enemy which didn’t have an entry for spells or skills
could still use “Attack”.
The great thing about a sequential pattern is that I can still get a good variety of abilities to be
used, and they will be spaced out so that the user has a chance to recover from the particularly
nasty ones. Also, there can be something rewarding about “learning” an attack pattern and using
that knowledge to defeat an enemy. As a huge plus, it is the easiest of all the A.I. systems to
implement.
Of course, I can spice it up a little bit. For example, in my post, Random Encounters and
Polymorphism, I mentioned creating recipes for what monsters would appear during a random
encounter. It was a list of enemies, where each unit listed would be spawned. However, some of
the elements in the list were actually a pair of enemies and within that pair only one of them
would be chosen at random. This helped to add some nice variety to the encounters so you didn’t
feel like you were always ghting the same battle.
We could provide a similar treatment to our sequential list of abilities for an enemy to use. For
example something like the following:
Implementation Details
Knowing that I want to use a particular sequence of abilities is great and helps satisfy a lot of the
logic about “what” to use, and “when” or “why” to use it. However, knowing the “how” is going to be
much more complex. In a non-tactics RPG the enemies don’t need to be as concerned about
distances and angles etc. but those elements can add a great deal of strategy in our game.
For example, let’s say that our sequential list tells us that it is time to cast “Fire”. I now need to
determine what targets are within reach as well as which, if any, of those targets do I actually
want. I could add some Gambit-like target markers to each entry in my sequential list which
could help. This would be useful because the targeters we implemented on the ability effects
don’t care about the alliance of a unit – it would be legal to cast re on any of them, though
potentially not helpful. AI targeting on the other hand does care about the alliance of a unit. So
even though a particular location could report nding a certain number of targets, there may be
better locations that attack a foe without hitting a nearbly ally.
Those extra targeting scripts will be implemented with some additional classes. It will be their
job to help nd the best locations to move to and the best directions to face and or locations to
target for use with the ability we had previously selected.
Summary
In this post, I discussed some pros and cons of various approaches to A.I. and how it could relate
to and be implemented in our Tactics RPG project. I explained the thought processes in my head
as I decided on a route to pursue. Even though there was no code in this post, I thought it might
introduce a few topics to inspire your own learning, and still could provide valuable insight to the
development process. Feel free to leave comments and let me know whether or not you enjoyed
it!
A.I. takes a long time to implement. Before I share my work I felt it needed more polish time and a
bit more in-depth discussion explaining what all the code is doing. Assuming nothing goes
wrong, I will begin sharing the actual implementation next. There is quite a lot of content so far,
so it may need to be broken down into a few parts. Stay tuned, I think you’re going to love it!
The Liquid Fire
Game Programming Blog
I’ve decided to split the A.I. implementation into two parts. This rst part will cover the “what” as
in “what ability do I want to use?”. This is the easier of the two problems. The next portion on
“how” as in “how do I know where to move and aim to best use the ability?” is rather complex.
Since this bit is easier, I will also include the code that ties the A.I. into the battle states, as well as
adding another user interface view which shows the name of the ability that a computer
controlled unit has chosen to use.
The A.I. for this tactics project will select abilities to use based on a sequential list concept, much
like the Final Fantasy 1 example from the previous post. In this lesson, we will be creating a
system which can cycle through this sort of attack sequence. We will be deciding what to use and
who or what to use it on (in a general sense, not a speci c target).
Targets
In order to be able to apply certain abilities to certain unit types, we will need some way to “ ag”
what a valid target for an ability will be. This is sort of like the way the Gambits in Final Fantasy
12 had both a target and an action. For this I created a new enum called Targets in the
Scripts/Enums folder:
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Targets
5. {
6. None,
7. Self,
8. Ally,
9. Foe,
10. Tile
11. }
Alliance
Of course, the target types such as ally and foe are subject to the unit’s perspective. In other
words, the foe of a monster would be a hero and vice versa. So to use the Targets enum we will
update the Alliance component:
The confused eld isn’t used by anything currently, and I haven’t actually tested it. I included it to
hint at what could be the target of a nasty ability effect. It is used to ip whether or not the unit
believes something is a match.
Plan Of Attack
As soon as it is determined that it is the computer’s turn to make a move, we will need to
formulate a plan of attack. This means I decide what ability to use, who or what to use the ability
on, where I move to on the board, and where I cast the ability or which direction I face while
casting the ability. All of this data is stored in a simple object which will be populated by various
steps of the AI process.
In part one we will ll out the “ability” and “target” elds. I will add a placeholder code for the
implementation of the other elds just to give you an idea of how everything will work, but the
“real” work will be presented in part two. Create a new script named PlanOfAttack and place it in
a new directory Scripts/View Model Component/AI:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class PlanOfAttack
5. {
6. public Ability ability;
7. public Targets target;
8. public Point moveLocation;
9. public Point fireLocation;
10. public Directions attackDirection;
11. }
The rst step in the AI process is determining what Ability to use and who or what to use it on.
For this I created a new component called the BaseAbilityPicker which I put in a Scripts/View
Model Component/AI/Ability Picker directory.
1. using UnityEngine;
2. using System.Collections;
3.
4. public abstract class BaseAbilityPicker : MonoBehaviour
5. {
6. #region Fields
7. protected Unit owner;
8. protected AbilityCatalog ac;
9. #endregion
10.
11. #region MonoBehaviour
12. void Start ()
13. {
14. owner = GetComponentInParent<Unit>();
15. ac = owner.GetComponentInChildren<AbilityCatalog>();
16. }
17. #endregion
18.
19. #region Public
20. public abstract void Pick (PlanOfAttack plan);
21. #endregion
22.
23. #region Protected
24. protected Ability Find (string abilityName)
25. {
26. for (int i = 0; i < ac.transform.childCount; ++i)
27. {
28. Transform category = ac.transform.GetChild(i);
29. Transform child = category.FindChild(abilityName);
30. if (child != null)
31. return child.GetComponent<Ability>();
32. }
33. return null;
34. }
35.
36. protected Ability Default ()
37. {
38. return owner.GetComponentInChildren<Ability>();
39. }
40. #endregion
41. }
This abstract base class indicates that we simply want to be able to pick an ability to use in a
turn, but it doesn’t care how or why the ability is chosen. The abstract Pick method must be
implemented by concrete subclasses and in that step will update the “Plan of Attack” object
which had been passed along as a parameter.
For convenience sake, we added a method which can nd an ability which matches a given
name (as a string). This was important since our units are created dynamically and we can’t
directly link from the picker to the ability since it wont have been created yet.
The primary “picker” used by this rst implementation will be one which says exactly what we
want to use. This goes back to the sequential list idea where I specify something like “Attack, Fire,
Heal” and it will use those speci c abilities.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class FixedAbilityPicker : BaseAbilityPicker
5. {
6. public Targets target;
7. public string ability;
8.
9. public override void Pick (PlanOfAttack plan)
10. {
11. plan.target = target;
12. plan.ability = Find(ability);
13.
14. if (plan.ability == null)
15. {
16. plan.ability = Default();
17. plan.target = Targets.Foe;
18. }
19. }
20. }
We accomplish the job by exposing two elds, the name of an ability and who or what we want to
target with that ability. Then in the Pick method, we use the base class’s Find method to get the
actual ability of the same name and then update the plan of attack with our decisions. If the
named ability can’t be found, we grab the rst ability it can nd instead (which would be Attack).
Because I had mentioned that it would be nice to add a little variety to the attack sequence, we
can help break up the pattern a little by adding some randomly chosen abilities. Note that instead
of maintaining pairs of ability names and targets, I simply refer to a list of other ability pickers. In
most cases those will be FixedAbilityPickers, but we could also nest other complex types of
pickers if we wanted to. Then we randomly grab one of the pickers and return the value that the
selected picker holds.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class RandomAbilityPicker : BaseAbilityPicker
6. {
7. public List<BaseAbilityPicker> pickers;
8.
9. public override void Pick (PlanOfAttack plan)
10. {
11. int index = Random.Range(0, pickers.Count);
12. BaseAbilityPicker p = pickers[index];
13. p.Pick(plan);
14. }
15. }
Attack Pattern
Next we need a component which can organize all of our pickers into the actual sequential list
and keep track of its own position within that list. For this I added a new script named
AttackPattern to the Scripts/View Model Component/AI folder.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class AttackPattern : MonoBehaviour
6. {
7. public List<BaseAbilityPicker> pickers;
8. int index;
9.
10. public void Pick (PlanOfAttack plan)
11. {
12. pickers[index].Pick(plan);
13. index++;
14. if (index >= pickers.Count)
15. index = 0;
16. }
17. }
At this point you can begin creating resources of “Attack Pattern” prefabs. The basic idea is that
you create an empty game object with the attack pattern script attached. Then add children
gameobjects, one for each ability picker. Whenever I create one of the Random Ability Picker
types, I rst would create all of the Fixed types which it would refer to. By keeping each picker on
its own gameobject it is very easy to drag and drop them in the inspector to the compound picker
types and/or to the attack pattern list in the root.
This is still prototype level work, so I think it’s good to just manually create what we want to work
with. When I have veri ed that the system works well and is suf ciently exible, it would be nice
to come up with a way to create them by simpler “recipes” and allow a factory to automatically
generate them like we do with ability catalogs.
An example image of an attack pattern setup appears below. I have added a few of these prefabs
to the project in my repository, and they will be loaded in the Unit factory as part of the unit
creation process.
To the UnitRecipe class I added a string name for this prefab called “strategy”…
… and then I used that name to load the prefab in the UnitFactory with the following method
Drivers
In a lot of Final Fantasy games, you might lose control of your own units (such as through Charm
or Berserk), but you might also gain control of enemy units with special Abilities of your own.
Because the control of a unit can change between human control and computer control, I added a
new set of ags to indicate who should be “driving” at the beginning of a turn.
1. using UnityEngine;
2. using System.Collections;
3.
4. public enum Drivers
5. {
6. None,
7. Human,
8. Computer
9. }
In addition, I created a component to hold that ag called Driver in the Scripts/View Model
Component/Actor directory:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class Driver : MonoBehaviour
5. {
6. public Drivers normal;
7. public Drivers special;
8.
9. public Drivers Current
10. {
11. get
12. {
13. return special != Drivers.None ? special : normal;
14. }
15. }
16. }
The “normal” ag indicates how a unit was loaded initially, and the “special” ag could indicate
that the default behavior is overridden, perhaps by a status ailment, etc. I haven’t implemented
any of these kinds of abilities yet, but I put the code there as a hint to how it might be handled.
Computer Player
Now we need a sort of overall manager for the A.I. components. This class will be responsible for
tying all of these new classes together so that they don’t all need to be coupled together
themselves. Basically the Computer Player script will be responsible for creating the plan of
action object as well as either directly implementing logic to ll-out the plan, or it can nd and
make use of other components which can handle the relevant task.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class ComputerPlayer : MonoBehaviour
6. {
7. BattleController bc;
8. Unit actor { get { return bc.turn.actor; }}
9.
10. void Awake ()
11. {
12. bc = GetComponent<BattleController>();
13. }
14.
15. public PlanOfAttack Evaluate ()
16. {
17. // Create and fill out a plan of attack
18. PlanOfAttack poa = new PlanOfAttack();
19.
20. // Step 1: Decide what ability to use
21. AttackPattern pattern = actor.GetComponentInChildren<AttackPattern>();
22. if (pattern)
23. pattern.Pick(poa);
24. else
25. DefaultAttackPattern(poa);
26.
27. // Step 2: Determine where to move and aim to best use the ability
28. PlaceholderCode(poa);
29.
30. // Return the completed plan
31. return poa;
32. }
33.
34. void DefaultAttackPattern (PlanOfAttack poa)
35. {
36. // Just get the first "Attack" ability
37. poa.ability = actor.GetComponentInChildren<Ability>();
38. poa.target = Targets.Foe;
39. }
40.
41. void PlaceholderCode (PlanOfAttack poa)
42. {
43. // Move to a random location within the unit's move range
44. List<Tile> tiles = actor.GetComponent<Movement>().GetTilesInRange(bc.board);
45. Tile randomTile = (tiles.Count > 0) ? tiles[ UnityEngine.Random.Range(0,
tiles.Count) ] : null;
46. poa.moveLocation = (randomTile != null) ? randomTile.pos : actor.tile.pos;
47.
48. // Pick a random attack direction (for direction based abilities)
49. poa.attackDirection = (Directions)UnityEngine.Random.Range(0, 4);
50.
51. // Pick a random fire location based on having moved to the random tile
52. Tile start = actor.tile;
53. actor.Place(randomTile);
54. tiles = poa.ability.GetComponent<AbilityRange>().GetTilesInRange(bc.board);
55. if (tiles.Count == 0)
56. {
57. poa.ability = null;
58. poa.fireLocation = poa.moveLocation;
59. }
60. else
61. {
62. randomTile = tiles[ UnityEngine.Random.Range(0, tiles.Count) ];
63. poa.fireLocation = randomTile.pos;
64. }
65. actor.Place(start);
66. }
67.
68. public Directions DetermineEndFacingDirection ()
69. {
70. return (Directions)UnityEngine.Random.Range(0, 4);
71. }
72. }
This script is only partially implemented, and a good chunk of it is only placeholder code which
we will replace in Part 2. The important parts are that one of the Battle States will tell this script
to Evaluate on a computer controlled unit’s turn, and at that time we will create and return a lled
out PlanOfAttack so that the other Battle States will know what to show.
Inside the Evaluate method we grab a reference to the AttackPattern component (assuming there
is one) and let it ll out a portion of the plan of attack. If the component is missing we have a
fallback which uses a simple attack instead.
The placeholder code shouldn’t occupy much of your attention, but if you are curious, it simply
grabs relevant components to know what the move range of a unit is, picks a random location
from that list, then determines the range of the selected ability based on that random location
and picks another random tile as an aiming location. We also provide a random attack direction,
because some abilities require a direction rather than an aim location.
In addition I added a DetermineEndFacingDirection method which will be needed in the nal
version, but which is also a placeholder implementation. In the real version we will use that to
cause the unit to turn and face the nearest opponent rather than leaving its back exposed. For
now, I just want to see everything move.
We haven’t connected any sort of animations or special fx to our usage of abilities, so at the
moment there is no visual way to see what ability our new A.I. is actually picking. You could
simply add a Debug.Log and print its name, but I thought we should go ahead and add something
a little nicer to the interface. We will add a black bar across the top of the screen with a label in it
that shows the name of the ability. Add a new script named BattleMessageController to the
Scripts/Controller directory:
1. using UnityEngine;
2. using UnityEngine.UI;
3. using System;
4. using System.Collections;
5.
6. public class BattleMessageController : MonoBehaviour
7. {
8. [SerializeField] Text label;
9. [SerializeField] GameObject canvas;
10. [SerializeField] CanvasGroup group;
11. EasingControl ec;
12.
13. void Awake ()
14. {
15. ec = gameObject.AddComponent<EasingControl>();
16. ec.duration = 0.5f;
17. ec.equation = EasingEquations.EaseInOutQuad;
18. ec.endBehaviour = EasingControl.EndBehaviour.Constant;
19. ec.updateEvent += OnUpdateEvent;
20. }
21.
22. public void Display (string message)
23. {
24. group.alpha = 0;
25. canvas.SetActive(true);
26. label.text = message;
27. StartCoroutine(Sequence());
28. }
29.
30. void OnUpdateEvent (object sender, EventArgs e)
31. {
32. group.alpha = ec.currentValue;
33. }
34.
35. IEnumerator Sequence ()
36. {
37. ec.Play();
38.
39. while (ec.IsPlaying)
40. yield return null;
41.
42. yield return new WaitForSeconds(1);
43.
44. ec.Reverse();
45.
46. while (ec.IsPlaying)
47. yield return null;
48.
49. canvas.SetActive(false);
50. }
51. }
In the past I have used extension methods to tie an animation feature to a component. That
would have been perfectly acceptable here as well – perhaps even better since it would be
reusable, but the code was already so simple I didn’t bother. All I am doing is fading the UI
element in, letting it stay on screen for a second, and fading it back out.
The prefab for this object is also pretty easy. It has a root GameObject with this script attached. Its
rst child is the Canvas (keeping it separated from the GameObject with the script lets me disable
the Canvas without also disabling the controller script). The canvas has a sliced image using the
ActionNameBacker sprite, and then a child label is attached to the image. See if you can build it
yourself, if not, refer to the one on my repository.
Battle Controller
Don’t forget to attach the Computer Player component to the same gameobject. The Battle
Message Controller will be on its own prefab (parent it to the Battle Controller though) and then
connect the references in the inspector.
Battle State
Now we have pretty much everything we need, we just need to tie it into the game itself. We will
do this through the battle states. I considered making separate states for the A.I. but since most of
the code would be the same whether or not the A.I. or the player was driving I ended up leaving
them together.
Open the base state, BattleState and add a protected eld which will show who the current driver
of a Unit is. We will assign the value in the Enter method. Plus, I added a condition that I only
listen to the Input events if the driver is the Player. This way, whenever the A.I. is driving I don’t
need to worry about input from the player interfering with anything.
A lot of the Battle States show menus to allow the player to select an ability, but we don’t need to
show them for the computer AI to pick an ability. In fact, we speci cally don’t want to show them,
or else it would be confusing and the player might think they were supposed to do something. So
add an if statement just before the call to LoadMenu to verify that it is the Human player who is
driving the current unit:
The rst state for a unit’s turn is the Command Selection State. It is in this state that we will need
to let the A.I. create the Plan of Attack. When the state Enters, we will call a new method to handle
this whenever the A.I. is driving:
The Command Selection State becomes active at a few points during a Units turn, but we only
want to generate the code to ll out a plan of attack the rst time. So we check for null and
generate a plan only then.
I put all of the code in a coroutine and added a 1 second delay before continuing to the next state.
This isn’t necessary for the computer, but by having some delays it allows a human observer to
better track what is happening. For example, at this point a new unit has been selected and the
camera will be animating to center over it. By waiting for the full second, the observer will realize
(hopefully) that it is a computer controlled unit which has been selected and that now it is just
time to continue watching.
After the brief pause, we allow one of three other states to take over. If we haven’t moved and
need to, then we continue down that path. Otherwise if we haven’t acted and need to, then we
continue down that path. If there are no move or action steps to take, we complete the turn with
the end facing state.
Of course for this code to compile I added a eld to hold the PlanOfAttack as a eld on the Turn
class. I also set the eld to null whenever the Change method is called.
When a human is driving, this state allows the cursor to be moved around the board and for a tile
to be selected using input (from a keyboard etc). We can mimic this behavior for the A.I. and it
will help the human observer understand what is about to happen. When this state is entered we
check whether or not the A.I. is driving in the Enter method. If so, we call the computers method.
All we do is loop with a small wait between each step until the cursor has reached the move
location eld which had been provided in the plan of attack.
Again, we have the same basic idea as we did with the Move Target State. The only real difference
would be when using an ability which is based on direction – then we just turn the unit to face
that direction. Otherwise we animate the cursor moving to the correct spot when it is the A.I.’s
turn:
When a human was driving for this state, we had shown a UI element that indicated the hit
chance and damage prediction of an ability. It would provide an opportunity for a user to make an
educated decision about which ability would be most effective against certain opponents.
Whenever the computer is driving, we will simply use it to display the name of the ability that
had chosen.
For the nal state, we allow the computer to decide an end facing direction. After a short pause
we apply it.
Demo
There are a few little polish items I added this time, but didn’t discuss since they are not relevant
to the A.I. For example I had the board’s tiles be parented to itself so the scene hierarchy looks a
little cleaner. I also added a few new abilities which caused me to add another ability effect and
ability effect target. Check out the repository and you can see EnemyAbilityEffectTarget and
EsunaAbilityEffect. I modi ed the Line Ability Range so that it would only extend as far as the
horizontal distance eld indicated. This way I could easily make a normal attack ability that
simply targets the rst tile in front of the user. Be sure to check out the repository commit in
order to see the full list of changes.
As one last little optional tidbit, feel free to add some placeholder code to the TurnOrderController
script which skips the turns of human controlled units. Why would I do that? Well I wouldn’t – at
least not for the nal game, but as a debug helper it can be nice to just watch the A.I. for awhile
and make sure it is acting like I expect. I didn’t commit this code to the repo, but it could be handy
in this post as well as Part 2.
When you run the project you should see something which looks like the following video:
AI Part1
Summary
This lesson was content rich, but mostly with easy to understand concepts. The main focus of
this lesson was showing the process of how an A.I. unit will choose an ability to use on any given
turn. The decision is made by use of a sequential attack pattern. In addition, we took the time to
plug the A.I. into all of the battle states and allow the A.I. decisions to be presented to the player
by animating the cursor movements and by showing the name of the chosen ability.
At this point we have fully autonomous agents, although they don’t look incredibly smart. This is
because even though they can move around the board and use abilities, they are not moving or
aiming with any real purpose. That will be the focus of Part 2, so stay tuned!
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
In A.I. Part 1, we added autonomous agents. Our enemy units could randomly move around the
board and pick and use abilities with random targets. Now it is time to make them move and aim
with a purpose. We need our enemies to be smart enough to hit multiple targets, attack from the
best angles, and avoid friendly re, etc. They should always act intelligently from the options
which are available.
Overview
Before we dive into the code, I want to describe the overall process. By understanding the whole
picture, the smaller details will hopefully make better sense. To begin we must understand that
there are a lot of options which might be available during a turn:
No targets in range
One or more allies in range
One or more foes in range
One or more of both allies and foes in range
The algorithm we will implement is a brute-force search of every possible move location and re
location or direction based on that move. Each of those entries will then be scored based on two
factors:
In scenarios where either no targets are found, or the only targets that are within range are not
desired targets, then the A.I. wont use an ability. Instead, I have the A.I. path nd to the nearest foe
and move in that direction. Otherwise, we will pick randomly from the options with the highest
score and update the plan of attack accordingly.
I want to be able to refer to general truths of the components of our abilities rather than have to
manually check the type of each. So we will add a few convenient properties to help facilitate this
idea.
Hit Rate
First, I want to know if the hit rate will be calculated based on the angle of attack or not. For this,
add the following property to the base class:
I assumed that more of the hit rate subclasses would be angle based than not, so we made the
default “true”. However, we will need to override this in the FullTypeHitRate and set it to false:
Ability Range
Likewise, I want to know whether or not the range an ability can reach is dependent on the
casters position. Again, I assume that this is the most common case, so I default the property to
true:
This time we have two subclasses which need to be overriden and set to false, both the
In niteAbilityRange and the SelfAbilityRange:
Computer Player
Open up the ComputerPlayer script. Go ahead and delete the method PlaceholderCode. Then add
a couple new elds:
The “alliance” eld is just a convenience wrapper property to get the alliance of the selected unit.
The other eld, “nearestFoe”, will be determined at a few different points within a turn.
Regardless, it searches the board for the rst foe it can nd. Note that “Foe” in this case is subject
to the perspective of the active unit. In other words, the foe of a monster is a hero, and vice versa.
We have replaced the call to the place holder method with a conditional which will ll out the
plan based on what kind of ability is intended to be used. Some abilities do not care where you
stand on the board when you use them, such as if you were targeting everyone, or targeting only
yourself. Other abilities are cast based on selecting a re location, but they don’t care what
direction you are facing when you cast it. Finally, some abilities require you to be in a particular
place and face a particular direction in order to hit the right targets. Each of these scenarios is
handled in its own method and will potentially update where a unit will move to, cast at, and or
turn to face a direction.
It’s entirely possible that we are unable to use whichever ability we selected. For example, if our
attack sequence came up as “Attack” but there were no nearby foes, then our planning phase
would indicate this by resetting the ability in our plan to null. When this occurs, I have decided to
simply move toward the nearest opponent so that targeting shouldn’t be a problem in the future.
Here we have the methods which check whether or not “position” matters or not when deciding
how to use an ability. When it is determined that an ability is position independent, then I simply
move to a random tile within the unit’s move range, because position didn’t matter. There is room
to polish this up – perhaps the unit would rather move toward or away from its foes during this
time. If you want more speci c behavior like that it shouldn’t be hard to add. I’ll show an example
of moving toward the nearest foe soon.
The next case is where the position matters, but the facing angle does not. For example, casting a
spell such as “Fire” or “Cure” can target different units based on where you move the aiming
cursor. Because you can move before ring, a unit can actually reach targets in a larger radius
than just the range of the ability by itself. Because of this I iterate through a nested loop, where an
outer loop considers every possible position a unit can move to, and an inner loop considers
every tile within ring range of that move location.
Remember that even after considering movement range and ability range, we still have an area of
effect on the ability itself. This would need to be considered next. However, there are likely to be a
lot of “overlapping” entries here. For example, whether I move one space to the left or one space to
the right, I can still re one space in front of the original location with most ranged abilities.
Therefore, I added a dictionary which mapped from a selected tile to an object which records
notes on that location such as which targets fall within range of the area of effect. I only create
and evaluate this note object the rst time I determine that a tile is within ring range. Otherwise,
I simply refer to the notes I had already taken and indicate that another tile is also a valid place to
re from.
Before I start going through the loops I recorded the tile the actor was originally placed on. Before
the method exits, I move the unit back to the original position. It’s important not to forget this
step or the game would be out of sync with the visuals in the game every time the AI took a turn.
Finally, I pass the list of options we have built up to this point to a method which can pick the
best overall option for our turn.
This last case depends both on a unit’s position on the board and the direction the unit faces
while using the selected ability. It should look pretty similar to the “PlanDirectionIndependent”
variation. The main difference here is that instead of grabbing the Ability Range component and
looping through targeted tiles, we instead loop through each of the four facing directions. Every
single entry generated will have a unique area of effect – there is no overlap or need for the
dictionary as I had last time. We can simply track each entry in a list directly.
1. List<Tile> GetMoveOptions ()
2. {
3. return actor.GetComponent<Movement>().GetTilesInRange(bc.board);
4. }
This method simply returns the list of tiles which the current actor can reach.
1. void RateFireLocation (PlanOfAttack poa, AttackOption option)
2. {
3. AbilityArea area = poa.ability.GetComponent<AbilityArea>();
4. List<Tile> tiles = area.GetTilesInArea(bc.board, option.target.pos);
5. option.areaTargets = tiles;
6. option.isCasterMatch = IsAbilityTargetMatch(poa, actor.tile);
7.
8. for (int i = 0; i < tiles.Count; ++i)
9. {
10. Tile tile = tiles[i];
11. if (actor.tile == tiles[i] || !poa.ability.IsTarget(tile))
12. continue;
13.
14. bool isMatch = IsAbilityTargetMatch(poa, tile);
15. option.AddMark(tile, isMatch);
16. }
17. }
As we were creating each Attack Option (a note on the effect area of using an ability), we needed
a way to rate it, so we could sort them later and pick the best one. We accomplish this by looping
through the area that the ability could reach from a given ring location. Any tile which is a
“legal” target for an ability gets a “mark” – for example you can “Attack” any unit whether friend or
foe, so any tile with a unit would be marked by the attack ability. However, we also indicate
whether or not the tile is determined to be a “match” (the desired target type for the given ability).
In the example before, any given unit would consider a tile with an ally is not a match, but tiles
with a foe are a match for the attack ability.
By tracking all of the marks, but also specifying which ones are matches or not, we can better
rate a move. For example, if my attack would hit exactly one foe and one ally by targeting tile ‘X’,
and exactly one foe but no allies by targeting tile ‘Y’, then the second option is better. I can tally
up a score such that marks which are matches incremenet the score and marks that are not a
match decrement the score.
Note that I intentially skip the tile on which the caster is currently standing, because that may
not be the unit’s location when it moves before ring. We will need to adjust scores based on the
caster’s location at a later point.
This method shows how to determine which marks are a match or not. An ability which targets a
tile is simply marked as true (I havent actually implemented any such abilities, so I might change
this logic later). Otherwise, I use the alliance component to determine whether or not the target
type is a match.
This is the method that actually provides a score for each of the attack options. It goes through
two “passes” of analyzing our options. On the rst pass, it scores each attack option based on
having more marks which are matches than marks which are not matches.
Whenever I nd a new “best” score, I track what the score was, and add the ability to a list of the
options which I consider to be the best. This list will be cleared if I should nd a better score, but if
I nd additional options with a tied score then I will also add them to the list.
When all of the options have been scored, it is actually possible that I wont have any entries in
my best options list. This would be the case where an ability could technically be used, but the
effect would be detrimental to the user’s party. For example, if the only option an AI unit had to
attack was one of its allies, then it would be better not to do anything than to actually perform the
ability. In these cases, I mark the plan’s abilty as null so that it wont be performed.
In the cases where I do have some bene cial options to pick from, I will then run another pass to
help trim down the options even further. There are multiple reasons for this. For example, lets say
I can attack a target unit from multiple different move locations. Some of those locations may be
from the front, while others may be from the back. If I can pick, I would want to pick an angle
from the back so that my chances of the attack hitting are greater.
By the end of this second “pass” I should have one or more options which were added to the nal
picks. Because they all share the same score, I pick any of them at random and assign the
relevant details to our plan of attack.
1. void FindNearestFoe ()
2. {
3. nearestFoe = null;
4. bc.board.Search(actor.tile, delegate(Tile arg1, Tile arg2) {
5. if (nearestFoe == null && arg2.content != null)
6. {
7. Alliance other = arg2.content.GetComponentInChildren<Alliance>();
8. if (other != null && alliance.IsMatch(other, Targets.Foe))
9. {
10. Unit unit = other.GetComponent<Unit>();
11. Stats stats = unit.GetComponent<Stats>();
12. if (stats[StatTypes.HP] > 0)
13. {
14. nearestFoe = unit;
15. return true;
16. }
17. }
18. }
19. return nearestFoe == null;
20. });
21. }
There are a few different times in a turn when I might want to know where the nearest foe is to a
given unit. For example, whenever the turn had no good targets to use the selected ability on – it
assumes that the reason is that it wasn’t close enough to the enemy, so I gure out where the
nearest foe is and attempt to move toward it. I determine this by using the board’s search method
and passing along an anonymous delegate. If it nds a tile with a unit that is a Foe (according to
the alliance component) and also isn’t KO’d, then I have found a target worth moving toward.
Whenever a board search has been run, there will be tracking data left over in the tiles. I can
iterate over the “path” in reverse until I nd a tile which happens to be included in the movement
range of the unit. This will allow me to move as close as possible to the foe who is nearest.
1. public Directions DetermineEndFacingDirection ()
2. {
3. Directions dir = (Directions)UnityEngine.Random.Range(0, 4);
4. FindNearestFoe();
5. if (nearestFoe != null)
6. {
7. Directions start = actor.dir;
8. for (int i = 0; i < 4; ++i)
9. {
10. actor.dir = (Directions)i;
11. if (nearestFoe.GetFacing(actor) == Facings.Front)
12. {
13. dir = actor.dir;
14. break;
15. }
16. }
17. actor.dir = start;
18. }
19. return dir;
20. }
After we have moved and used an ability, we need to determine an end facing direction. For this I
nd the nearest foe again. Note that it is important to do this a second time, because the foe who
was nearest before you moved is not necessarily the foe who is nearest after you have moved.
Next I loop through each of the directions until I nd a direction which has me face the foe from
the front. This way the foe is less likely to be able to attack me from the back.
Attack Option
The “notes” I take on any given location from which to evaluate the usage of an ability is also a
somewhat lengthy class. A lot of its functionality I talked through while covering the
ComputerPlayer script, but just to be clear I will break it down as well. Add a new script named
AttackOption to the same AI folder that our other scripts are located within.
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class AttackOption
6. {
7. class Mark
8. {
9. public Tile tile;
10. public bool isMatch;
11.
12. public Mark (Tile tile, bool isMatch)
13. {
14. this.tile = tile;
15. this.isMatch = isMatch;
16. }
17. }
18.
19. // Add other code here
20. }
I created another class inside of AttackOption called Mark to hold a pair of data. This is a little
cleaner and less error prone than maintaining two separate lists (one for the tiles and one for
whether or not the tile was a match). The Mark class is private to the implementation of the
AttackOption class, and is only used for convenience and readability. If other classes needed to
know about it or use it, then I would probably stick it in its own le.
There are several elds to ll out here. The “target” and “direction” elds are treated slightly
differently (or not at all) based on the type of ability being used. For example, “target” would
represent either the tile which we highlighted to use as a ring location, or the tile we would want
to move to in order to re, and direction may or may not apply.
The list of marks grows based on the number of legal targets which are within the area of effect
for an ability given the target tile and facing direction.
The list of move targets indicates which locations the unit can move to in order to cast at the
indicated target location. Direction oriented abilties will only ever have one move target in this
list.
The area targets are the list of tiles which fall within an ability’s area of effect, regardless of if
they currently have a target or not. This can be important in cases where I had overlap because I
might initially “score” an attack based on the user being in one position, but could actually “pick”
an alternate move location later. If the ability were intended for foes, then I would want to make
sure I didn’t pick an option where the ability would also hit the caster. On the other hand, if the
ability is intended for allies, then allowing the abilty to include the caster would be a nice bonus.
The “isCasterMatch” eld tracks whether or not one of the move target locations would actually
be good to move into or not.
The “bestMoveTile” and “bestAngleBasedScore” elds indicate a tile which provides the best score
to attack from. This score would be based on the idea that attacking from behind gives a greater
chance of a hit actually connecting. Attacks from the front are easier for an enemy to dodge, so
we want to naturally pick ones from the rear.
Here we have a few of the methods which we called from the ComputerPlayer script while
planning a turn based on an ability which was position dependent (or in other words, pretty
much any ability except the in nite-range and self-range ones. The AddMoveTarget method is
called to build up the list of locations which are in ring range of the current tile. Note that I don’t
actually include options that would be bad for the caster, for example I wouldn’t want to move
within the blast radius of my own attack.
The AddMark method creates an instance of the class we de ned above and adds it to a list.
Remember that a mark indicates a target (good or bad) can be hit by whatever re location was
chosen for this AttackOption instance.
Here we will provide a score by which we can sort the various options available during a turn.
Before calculating a score it also looks for the best tile to move to in order to use the ability. The
best move target will be the one which has the best score based on the second factor (the angle of
attack) – such as attacking a unit from behind gives more points than attacking from the front. In
the event that there are no good locations, then the overall score is returned early as zero.
Assuming that a good move tile is found, the score is then tallied based on how many of the
marks are a match or not. If the caster is a match for the ability then an extra point is awarded,
because we can potentially move to a location where the ability will include it.
There are two main types of abilities to consider, ones which the angle from the attacker to the
target makes a difference and ones which do not care about the angle. For the abilities where
angle does matter, we use a simple algorithm that looks a lot like what you have seen a few times
now in the Computer Player script where we sort attack options. The basic idea is that we loop
through all of the tiles we can move to, move the unit there, calculate the angles between the
caster and the targets and use those angles to generate a score. When a score is greater than the
previous best score, we reset the list of best options, and when we have considered all options, we
pick at random from tiles with the highest score. When the angle is irrelevant, we can simply
return any tile at random.
This method determines whether or not an ability’s hit rate is determined based on the angle of
the attacker to the defender (or caster and target if that makes more sense).
Here we get the angle from the caster to each of the mark locations. The resulting score is either
incremented or decremented based on whether or not the mark was an intended match. The
amount the score goes up or down is based on the angle (front, side or back).
After we have built a list of the best places to move to (scored based on the number of good marks
and the angle based scores) we can potentially further optimize the remaining selection. If the
caster is not a match (such as when we are using an offensive ability intended for an opponent)
then we wont do anything. Then we need to decide whether or not the caster can move to one of
the locations which is also included in the area of effect. If so, we remove any tile from the best
choices list which isn’t that good.
This method helps to score move target options based on the angle of attack. I could have picked
any number I wanted to balance the preferences, but I chose numbers which currently match the
general percent chance of hitting from that angle. This means that in general I favor attacks from
behind, but having a chance to hit two units from the front could still be better than targeting one
unit from behind.
Demo
With this code in place, our computer controlled units nally start looking intelligent. They will
move toward their targets, attack from angles giving good hit chances, re in locations that
maximize the number of targets, etc. Compare the sample video from this week against last
weeks video to see how far we have come.
Summary
Although we didn’t add many classes, the ones we did add were very long and complex. We broke
them down step by step and covered the “How” as in “How will a computer controlled unit know
where to move and aim in order to best use the selected ability?” With this step completed, we
have also nished the prototype impelementation of our A.I. I feel that it looks pretty good!
At this point I would like feedback from anyone who is still following along. I am considering
moving on to another project since this one is basically feature complete. If there are topics you
want me to cover that I have missed then leave a request below. If you have suggestions for other
projects you would love to see, I would like to hear that too. Either way I will probably take a much
needed break for the holidays and potentially to get a head start on whatever my next series will
be.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.
The Liquid Fire
Game Programming Blog
It’s been almost a year since the last post, but I nally have a reason to revisit this project.
Brennan Anderson wrote some amazing music after following along with the Tactics RPG project
and was generous enough to share it with the rest of us. Thanks to him, we will go ahead and add
a follow-up post that describes working with music.
Audio Animation
Audio is just data and it has attributes that you can and probably will animate. Primarily you will
animate volume. This can allow you to fade a track in or out, or crossfade between two music
tracks, etc. We already have a reusable animation system in this project which we can easily
extend for this feature. Add a new script called AudioSourceVolumeTweener and copy the
following:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AudioSourceVolumeTweener : Tweener
5. {
6. public AudioSource source
7. {
8. get
9. {
10. if (_source == null)
11. _source = GetComponent<AudioSource>();
12. return _source;
13. }
14. set
15. {
16. _source = value;
17. }
18. }
19. protected AudioSource _source;
20.
21. protected override void OnUpdate ()
22. {
23. base.OnUpdate ();
24. source.volume = currentValue;
25. }
26. }
Because the Tweener inherits from an EasingControl, it already has startValue, currentValue, and
endValue elds. All we need is a oat value to animate the volume of an audio source, so we can
use these values directly – we simply pass the currentValue of the tweener to the AudioSource’s
volume eld in the OnUpdate callback and we’re done!
In order to trigger the animation of an AudioSource’s volume, it would be nice to add some more
extensions like we have done for animating transforms, etc. Add another script named
AudioSourceAnimationExtensions and copy the following:
1. using UnityEngine;
2. using System;
3. using System.Collections;
4.
5. public static class AudioSourceAnimationExtensions
6. {
7. public static Tweener VolumeTo (this AudioSource s, float volume)
8. {
9. return VolumeTo(s, volume, Tweener.DefaultDuration);
10. }
11.
12. public static Tweener VolumeTo (this AudioSource s, float volume, float duration)
13. {
14. return VolumeTo(s, volume, duration, Tweener.DefaultEquation);
15. }
16.
17. public static Tweener VolumeTo (this AudioSource s, float volume, float duration,
Func<float, float, float, float> equation)
18. {
19. AudioSourceVolumeTweener tweener =
s.gameObject.AddComponent<AudioSourceVolumeTweener>();
20. tweener.source = s;
21. tweener.startValue = s.volume;
22. tweener.endValue = volume;
23. tweener.duration = duration;
24. tweener.equation = equation;
25. tweener.Play ();
26. return tweener;
27. }
28. }
Hopefully this pattern will look familiar, we simply overloaded the VolumeTo method with a few
different sets of parameters so you could be increasingly speci c about “how” the volume
changed. You may only care about the target volume level, but you might also want to choose
how long it takes to get there or with what kind of animation curve it animates along. The less
speci c versions pass default values to the most speci c version so that you only really
implement the function once.
For example sake, here is a sample script which cross fades between two audio sources using our
new Tweener subclass and extension. This script wont be included in the repository and it is
included merely to demonstrate the potential use of our new feature.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class CrossFadeAudioDemo : MonoBehaviour
5. {
6. [SerializeField] AudioSource fadeInSource;
7. [SerializeField] AudioSource fadeOutSource;
8.
9. void Start ()
10. {
11. fadeInSource.volume = 0;
12. fadeOutSource.volume = 1;
13.
14. fadeInSource.Play();
15. fadeOutSource.Play();
16.
17. fadeInSource.VolumeTo(1);
18. fadeOutSource.VolumeTo(0);
19. }
20. }
If you would like to test this demo, I would recommend you create a new scene. Next, add two
audio sources which are precon gured to use different audio clips. I set both of the audio sources
to NOT play on awake so I could con gure them rst. Don’t forget to hook up the references for
them to this script in the inspector. Press play. When the scene starts, it will con gure one of the
sources to have no volume and fade in, while the other will start at full volume and fade out. If
you like you can add additional parameters to the VolumeTo statements such as providing a
longer duration so that the effect is more obvious.
Audio Events
One feature I would love to see in Unity is a greater use of event driven programming. For
example, it would be great to know when an audio source loops or completes playing. Lacking
that, I can accomplish what I need with either a scheduled callback or a polling system. To
schedule a callback you can use something like MonoBehaviour.Invoke and or
MonoBehaviour.InvokeRepeating as a replacement for the lack of any completion event on the
audio source. If you’re curious, those snippets might look something like the following:
Unfortunately I found that this was a pretty fragile approach. One problem is that an Audio Clip’s
length in seconds doesn’t necessarily equate to how long an Audio Source will spend playing it.
For example, if the pitch of an audio source is modi ed, then it can play the clip in more or less
time depending on the new pitch.
Because I didn’t feel like running a bunch of tests on all of the variety of things which could
potentially modify time in one form or another to mess up the timing with the invoke call, I
decided to use the polling approach instead. This pattern is achieved through a coroutine. Add a
new script called AudioTracker and copy the following:
1. using UnityEngine;
2. using System;
3. using System.Collections;
4.
5. public class AudioTracker : MonoBehaviour {
6. #region Actions
7. // Triggers when an audiosource isPlaying changes to true (play or unpause)
8. public Action<AudioTracker> onPlay;
9.
10. // Triggers when an audiosource isPlaying changes to false without completing
(pause)
11. public Action<AudioTracker> onPause;
12.
13. // Triggers when an audiosource isPlaying changes to false (stop or played to end)
14. public Action<AudioTracker> onComplete;
15.
16. // Triggers when an audiosource repeats
17. public Action<AudioTracker> onLoop;
18. #endregion
19.
20. #region Fields & Properties
21. // If true, will automatically stop tracking an audiosource when it stops playing
22. public bool autoStop = false;
23.
24. // The source that this component is tracking
25. public AudioSource source { get; private set; }
26.
27. // The last tracked time of the audiosource
28. private float lastTime;
29.
30. // The last tracked value for whether or not the audioSource was playing
31. private bool lastIsPlaying;
32.
33. const string trackingCoroutine = "TrackSequence";
34. #endregion
35.
36. #region Public
37. public void Track(AudioSource source) {
38. Cancel();
39. this.source = source;
40. if (source != null) {
41. lastTime = source.time;
42. lastIsPlaying = source.isPlaying;
43. StartCoroutine(trackingCoroutine);
44. }
45. }
46.
47. public void Cancel() {
48. StopCoroutine(trackingCoroutine);
49. }
50. #endregion
51.
52. #region Private
53. IEnumerator TrackSequence () {
54. while (true) {
55. yield return null;
56. SetTime(source.time);
57. SetIsPlaying(source.isPlaying);
58. }
59. }
60.
61. void AudioSourceBegan () {
62. if (onPlay != null) {
63. onPlay(this);
64. }
65. }
66.
67. void AudioSourceLooped () {
68. if (onLoop != null)
69. onLoop(this);
70. }
71.
72. void AudioSourceCompleted () {
73. if (onComplete != null)
74. onComplete(this);
75. }
76.
77. void AudioSourcePaused () {
78. if (onPause != null)
79. onPause(this);
80. }
81.
82. void SetIsPlaying (bool isPlaying) {
83. if (lastIsPlaying == isPlaying)
84. return;
85.
86. lastIsPlaying = isPlaying;
87.
88. if (isPlaying)
89. AudioSourceBegan();
90. else if (Mathf.Approximately(source.time, 0))
91. AudioSourceCompleted();
92. else
93. AudioSourcePaused();
94.
95. if (isPlaying == false && autoStop == true)
96. StopCoroutine(trackingCoroutine);
97. }
98.
99. void SetTime (float time) {
100. if (lastTime > time) {
101. AudioSourceLooped();
102. }
103. lastTime = time;
104. }
105. #endregion
106. }
When you use this script it will cause a coroutine to track the playback of the audiosource on a
frame by frame basis. Note that this means you won’t catch the exact moment that a bit of audio
has completed or looped, but it should at least be very close – a game even running at 30 fps
would be within a few hundreths of a second in accuracy. I would also point out that even if you
could get an event at the exact moment an audio track completes that you would be unlikely to
do much anyway since it would occur outside of unity’s execution thread and you wouldn’t be
able to interact with any Unity objects.
It is important to note that several of the callbacks can be invoked by more than one audio event.
For example, you would get the onPlay callback anytime the audiosource changes the isPlaying
ag to true. This can happen either when Playing an audiosource for the rst time, or as a result
of Unpausing a paused audiosource. If you needed to know for certain how a callback was
obtained (such as differentiating between “unpause” and “play”, or between a play to the end and
“stop”) then you would need to wrap the relevant AudioSource methods. For example, you could
implement a “Stop” method on the tracker, which then tells the tracked source to “Stop”, so that
you would now be able to determine you had manually stopped playback instead of letting it play
to the end and stopping on its own. I decided not to wrap these calls because it would be too easy
to forget to use them and missed expectations might lead to some frustrating logic bugs.
I feel a lot more comfortable with this version over “Invoke”, because it doesn’t make any
assumptions about the timing of the audio… well except for looping. You could always set the
playback time manually which could cause the script to think it had looped. Otherwise, it should
handle all of the use cases I can think of off the top of my head.
Loop Demo
Like the earlier demo, the following script also wont be included in the repository and it is
included merely to demonstrate the potential use of audio events for looping and completion. In
this demo, I setup a temporary scene with two audio sources. One was con gured with the sound
of a laser blast, and the other an explosion. Both audiosources were set not to play on awake, and
the laser had loop enabled.
If you setup a similar scene and play it, you will see that the laser sound will play some random
number of times (based on the loopCount variable) and on each loop, the loopStep variable will
increment and I will change the pitch of the laser so that the next play through happens in a
different amount of time (but also adds a nice bit of variance – you could do this for a lot of sound
fx like footsteps, etc). When the desired number of loops has been achieved we disable the
looping and wait for the audio source to complete. When that event is triggered I tell the
explosion audio source to play.
1. using UnityEngine;
2. using System.Collections;
3.
4. public class LoopDemo : MonoBehaviour
5. {
6. [SerializeField] AudioSource laser;
7. [SerializeField] AudioSource explosion;
8.
9. AudioTracker tracker;
10. int loopCount, loopStep;
11.
12. void Start () {
13. loopCount = Random.Range(4, 10);
14.
15. tracker = gameObject.AddComponent<AudioTracker>();
16. tracker.onLoop = OnLoop;
17. tracker.Track(laser);
18.
19. laser.Play();
20. }
21.
22. void OnLoop (AudioTracker sender) {
23. laser.pitch = UnityEngine.Random.Range(0.5f, 1.5f);
24. loopStep++;
25. if (loopStep >= loopCount) {
26. laser.loop = false;
27. tracker.onComplete = OnComplete;
28. }
29. }
30.
31. void OnComplete (AudioTracker sender) {
32. explosion.Play();
33. }
34. }
Audio Sequence
The music that Brennan provided isn’t a normal music track – what I mean is that he provided
two different assets that are meant to be used together. There is an intro music track, followed by
a loopable music track. The loopable portion should play when the intro completes, and then
continue playing for as long as this scene is active. Unfortunately this creates a particular
problem for Unity, because Unity is not event driven and doesn’t allow you to interact with it on a
background thread.
You might consider using the AudioTracker to accomplish this task, but it isn’t the ideal solution.
The actual playback of the audio can complete in-between frames and in order to continue on
with the next track without any noticeable hitches we will have to use another method Unity
provides instead – PlayScheduled. This handy method has the bene t of making sure that music
can begin even between frames and also that it will already be loaded and ready when the time
comes to begin playing. Unfortunately, it isn’t a very smart method and requires a lot of hand
holding and assumptions that I had hoped to avoid. To make things trickier, an AudioSource
doesn’t provide a eld representing its current state, or a variety of other important bits of data (at
least not that I am aware of – feel free to correct me). Here are some gotchas I encountered:
isPlaying will return true even while it is waiting to play (because it is scheduled) but of
course you wont hear anything, nor will the time eld be updated
isPlaying will return false when it is paused and when it is stopped
UnPause will cause a paused audiosource to set isPlaying back to true, but not a stopped
audiosource
There is no eld that indicates the difference between a paused or stopped audiosource
There is no eld indicating whether an audiosource is currently scheduled to play or not
There is nothing to tell you when a scheduled audiosource is scheduled to begin
You can pause a scheduled audiosource, but it doesn’t delay the scheduled start time
accordingly
In order to help manage all of this I created a few new classes. Create a new script called
AudioSequenceData and copy the following:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class AudioSequenceData {
5.
6. #region Fields & Properties
7. public double startTime { get; private set; }
8. public readonly AudioSource source;
9.
10. public bool isScheduled {
11. get {
12. return startTime > 0;
13. }
14. }
15.
16. public double endTime {
17. get {
18. return startTime + source.clip.length;
19. }
20. }
21. #endregion
22.
23. #region Constructor
24. public AudioSequenceData (AudioSource source) {
25. this.source = source;
26. startTime = -1;
27. }
28. #endregion
29.
30. #region Public
31. public void Schedule (double time) {
32. if (isScheduled)
33. source.SetScheduledStartTime(time);
34. else
35. source.PlayScheduled(time);
36. startTime = time;
37. }
38.
39. public void Stop () {
40. startTime = -1;
41. source.Stop();
42. }
43. #endregion
44. }
This class helps to control and track information on a single AudioSource. While Unity provided
methods to schedule them, they didn’t provide a way to check when it was scheduled after the
fact (again unless I missed it somewhere). Using this class, I can schedule a clip to play at a
speci c time, but then if I need to reschedule it, it will know it had already been scheduled and
use the appropriate method to modify the schedule instead.
Next, we need something that can manage a list of these Data objects, and also manage pausing
and resuming the sequence so that future scheduled clips will still play when you expect them to.
Create a new script named AudioSequence and copy the following:
1. using UnityEngine;
2. using System.Collections;
3. using System.Collections.Generic;
4.
5. public class AudioSequence : MonoBehaviour {
6.
7. #region Enum
8. private enum PlayMode
9. {
10. Stopped,
11. Playing,
12. Paused
13. }
14. #endregion
15.
16. #region Fields
17. Dictionary<AudioClip, AudioSequenceData> playMap = new Dictionary<AudioClip,
AudioSequenceData>();
18. PlayMode playMode = PlayMode.Stopped;
19. double pauseTime;
20. #endregion
21.
22. #region Public
23. public void Play (params AudioClip[] clips) {
24. if (playMode == PlayMode.Stopped)
25. playMode = PlayMode.Playing;
26. else if (playMode == PlayMode.Paused)
27. UnPause();
28.
29. double startTime = GetNextStartTime();
30. for (int i = 0; i < clips.Length; ++i) {
31. AudioClip clip = clips[i];
32. AudioSequenceData data = GetData(clip);
33. data.Schedule(startTime);
34. startTime += clip.length;
35. }
36. }
37.
38. public void Pause () {
39. if (playMode != PlayMode.Playing)
40. return;
41. playMode = PlayMode.Paused;
42.
43. pauseTime = AudioSettings.dspTime;
44. foreach (AudioSequenceData data in playMap.Values) {
45. data.source.Pause();
46. }
47. }
48.
49. public void UnPause () {
50. if (playMode != PlayMode.Paused)
51. return;
52. playMode = PlayMode.Playing;
53.
54. double elapsedTime = AudioSettings.dspTime - pauseTime;
55. foreach (AudioSequenceData data in playMap.Values) {
56. if (data.isScheduled)
57. data.Schedule( data.startTime + elapsedTime );
58. data.source.UnPause();
59. }
60. }
61.
62. public void Stop () {
63. playMode = PlayMode.Stopped;
64. foreach (AudioSequenceData data in playMap.Values) {
65. data.Stop();
66. }
67. }
68.
69. public AudioSequenceData GetData (AudioClip clip) {
70. if (!playMap.ContainsKey(clip)) {
71. AudioSource source = gameObject.AddComponent<AudioSource>();
72. source.clip = clip;
73. playMap[clip] = new AudioSequenceData(source);
74. }
75. return playMap[clip];
76. }
77. #endregion
78.
79. #region Private
80. AudioSequenceData GetLast () {
81. double highestEndTime = double.MinValue;
82. AudioSequenceData lastData = null;
83. foreach (AudioSequenceData data in playMap.Values) {
84. if (data.isScheduled && data.endTime > highestEndTime) {
85. highestEndTime = data.endTime;
86. lastData = data;
87. }
88. }
89. return lastData;
90. }
91.
92. double GetNextStartTime () {
93. AudioSequenceData lastToPlay = GetLast();
94. if (lastToPlay != null && lastToPlay.endTime > AudioSettings.dspTime)
95. return lastToPlay.endTime;
96. else
97. return AudioSettings.dspTime;
98. }
99. #endregion
100. }
At the top of this script we provided a PlayMode enum that could track the state of the whole
sequence – whether Stopped or Playing etc. This helps overcome the lack of state information on
AudioSources but also helps because this script manages multiple audiosources, some which
may have already completed (and therefore be stopped).
When you want to add one or more AudioClips to the sequence, just call Play and pass them
along. It shouldn’t matter if the sequence is already playing or paused, it will still add them to the
end of the list and schedule them for playback accordingly.
I also provided Pause and Unpause which provide a convenient way to temporarily stop playback
of an audiosource. This wont stop a scheduled playback, but it will reschedule the playback when
you resume playing so that each track will play one after the other.
If you want to stop playback, including the scheduling of playback, you can use the Stop method.
You can get the AudioSequenceData for any clip by using the GetData method. This can let you
know whether or not a clip is scheduled to play, and when it should start and stop playing. For the
most part you probably wont need this, but its there for special cases.
The private method, GetLast returns the audio source that has the latest end time. It will be used
to gure out the new start time of a clip which you would want to play at the end of the sequence.
The private method, GetNextStartTime will return the endTime of the last audio clip in the list if
there is one – but it is possible that the endTime has completed in the past. To be safe, the
method will return only values that are greater than or equal to the current
AudioSettings.dspTime value so that new calls to play will start now or in the future.
Music Player
Now that we have a way to seamlessly play two (or more) music tracks together, I wanted to
create a simple component that could automatically play music just like Brennan provided it.
Using this script, it should be about as easy to setup your music as it would have been if it were a
single le. Add a new script called MusicPlayer and copy the following:
1. using UnityEngine;
2. using System.Collections;
3.
4. public class MusicPlayer : MonoBehaviour {
5. public AudioClip introClip;
6. public AudioClip loopClip;
7. public AudioSequence sequence { get; private set; }
8.
9. void Start () {
10. sequence = gameObject.AddComponent<AudioSequence>();
11. sequence.Play(introClip, loopClip);
12. AudioSequenceData data = sequence.GetData(loopClip);
13. data.source.loop = true;
14. }
15. }
Now we just need to incorporate this script and the music assets into our game:
Extra
As a side note, if you use an audio mixer (new in Unity 5), you can globally adjust the volume or
audio effects of any audio source that uses it. This setup requires little more than an exposed
parameter and a UI script on your canvas to modify it – be sure to check out Unity’s nice video
tutorials that show how. This solves most if not all of my other needs for an Audio Controller such
as knowing when to mute or change volume for music and or sound fx.
Summary
In this post we provided several reusable components related to audio and music in particular.
First we created a new Tweener to allow us to programmatically fade in or out music using any
speci ed volume, duration and animation curve we desire. Then we created a script which
tracked the playback of an audio source via a couroutine so that you could get callbacks for audio
based events like when it begins playing, stops playing, loops or completes. Finally we created a
system that could allow us to play a sequence of audioclips without any gaps – perfect for
playing the new music assets that we added to the project.
All of these scripts are “fresh” (read as “not battle tested” or “use at your own risk”) but should
provide a helpful starting point at a minimum. If you nd any bugs let me know and I’ll attempt to
x it.
Don’t forget that the project repository is available online here. If you ever have any trouble
getting something to compile, or need an asset, feel free to use this resource.