Tactics RPG Path Finding

Pathfinding can be a relatively advanced task, mostly because the logic takes a moment to grasp. We will be using a form of pathfinding 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 flying 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.

using UnityEngine;
using System.Collections;

public enum Directions
{
	North,
	East,
	South,
	West
}

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 first parameter entry in the method with a this keyword. When you invoke the extension method, you treat it as if that first 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.

using UnityEngine;
using System.Collections;

public static class DirectionsExtensions
{
	public static Directions GetDirection (this Tile t1, Tile t2)
	{
		if (t1.pos.y < t2.pos.y)
			return Directions.North;
		if (t1.pos.x < t2.pos.x)
			return Directions.East;
		if (t1.pos.y > t2.pos.y)
			return Directions.South;
		return Directions.West;
	}

	public static Vector3 ToEuler (this Directions d)
	{
		return new Vector3(0, (int)d * 90, 0);
	}
}

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 first tile in order to reach the second tile like this:

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.

Directions d = Directions.North;
Vector3 r = d.ToEuler();

Tile

The Tile component was created in an earlier lesson, but will need to be modified 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 field to the Tile script.

public GameObject content;

In addition, I will add a few fields which will be useful for our pathfinding and pathfollowing code. I want the fields to be public, but I dont want them to appear in the inspector so I will use a special tag to hide them. The first field prev stores the tile which was traversed to reach it. You can loop through the tiles for as long as the prev field is not null to determine the entire path taken to reach any given location. The second field distance stores the number of tiles which have been crossed to reach this point.

[HideInInspector] public Tile prev;
[HideInInspector] public int distance;

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:

using UnityEngine;
using System.Collections;

public class Unit : MonoBehaviour 
{
	public Tile tile { get; protected set; }
	public Directions dir;

	public void Place (Tile target)
	{
		// Make sure old tile location is not still pointing to this unit
		if (tile != null && tile.content == gameObject)
			tile.content = null;
		
		// Link unit and tile references
		tile = target;
		
		if (target != null)
			target.content = gameObject;
	}

	public void Match ()
	{
		transform.localPosition = tile.center;
		transform.localEulerAngles = dir.ToEuler();
	}
}

Board

The core of our pathfinding code will exist in the Board script. If any of you have worked with pathfinding before, you may wonder why I didn’t make use of a more efficient 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 flow 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 first 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.

TacticsRPG_06_PathFindingSteps_zpsamuzadjp

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 first 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:

public List<Tile> Search (Tile start, Func<Tile, Tile, bool> addTile)
{
	List<Tile> retValue = new List<Tile>();
	retValue.Add(start);

	// Add more code here

	return retValue;
}

This method will return a list of Tiles, starting from a specific 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 first 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 pathfinding fields. 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 efficient path.

void ClearSearch ()
{
	foreach (Tile t in tiles.Values)
	{
		t.prev = null;
		t.distance = int.MaxValue;
	}
}

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.

ClearSearch();
Queue<Tile> checkNext = new Queue<Tile>();
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.

start.distance = 0;
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.

while (checkNow.Count > 0)
{
	Tile t = checkNow.Dequeue();
	// Add more code here
}

Before we flesh out the while loop, add an array of Points (one in each cardinal direction) as a private field to the Board class. Be sure NOT to add it inside of the while loop or it will be making a lot of unnecessary extra allocations.

Point[] dirs = new Point[4]
{
	new Point(0, 1),
	new Point(0, -1),
	new Point(1, 0),
	new Point(-1, 0)
};

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.

for (int i = 0; i < 4; ++i)
{
	Tile next = GetTile(t.pos + dirs[i]);
	// Add more code here
}

The implementation for GetTile follows

public Tile GetTile (Point p)
{
	return tiles.ContainsKey(p) ? tiles[p] : null;
}

Within the inner for loop, we will first 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 efficient (shorter distance) path – remember that if the tile has not been touched it should still hold its reset value of int.MaxValue.

if (next == null || next.distance <= t.distance + 1)
	continue;

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.

if (addTile(t, next))
{
	next.distance = t.distance + 1;
	next.prev = t;
	checkNext.Enqueue(next);
	retValue.Add(next);
}

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.

if (checkNow.Count == 0)
	SwapReference(ref checkNow, ref checkNext);

The implementation for SwapReference is below:

void SwapReference (ref Queue<Tile> a, ref Queue<Tile> b)
{
	Queue<Tile> temp = a;
	a = b;
	b = temp;
}

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 fields to represent a highlight and default color:

Color selectedTileColor = new Color(0, 1, 1, 1);
Color defaultTileColor = new Color(1, 1, 1, 1);

Following are the methods which can loop through the tiles and highlight them or un-highlight them.

public void SelectTiles (List<Tile> tiles)
{
	for (int i = tiles.Count - 1; i >= 0; --i)
		tiles[i].GetComponent<Renderer>().material.SetColor("_Color", selectedTileColor);
}

public void DeSelectTiles (List<Tile> tiles)
{
	for (int i = tiles.Count - 1; i >= 0; --i)
		tiles[i].GetComponent<Renderer>().material.SetColor("_Color", defaultTileColor);
}

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 flying 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.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public abstract class Movement : MonoBehaviour
{

}

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 fields are implemented as follows:

public int range;
public int jumpHeight;
protected Unit unit;
protected Transform jumper;

We will assign some of the references in the Awake method:

protected virtual void Awake ()
{
	unit = GetComponent<Unit>();
	jumper = transform.FindChild("Jumper");
}

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.

public virtual List<Tile> GetTilesInRange (Board board)
{
	List<Tile> retValue = board.Search( unit.tile, ExpandSearch );
	Filter(retValue);
	return retValue;
}

The ExpandSearch method will be overridable but also offer a base implementation which compares the distance traveled against the range of the character.

protected virtual bool ExpandSearch (Tile from, Tile to)
{
	return (from.distance + 1) <= range;
}

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.

protected virtual void Filter (List<Tile> tiles)
{
	for (int i = tiles.Count - 1; i >= 0; --i)
		if (tiles[i].content != null)
			tiles.RemoveAt(i);
}

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.

public abstract IEnumerator Traverse (Tile tile);

We will also add a reusable bit of animation code which causes a character to rotate to face a specified direction. This method causes the unit to rotate in the fastest direction.

protected virtual IEnumerator Turn (Directions dir)
{
	TransformLocalEulerTweener t = (TransformLocalEulerTweener)transform.RotateToLocal(dir.ToEuler(), 0.25f, EasingEquations.EaseInOutQuad);
	
	// When rotating between North and West, we must make an exception so it looks like the unit
	// rotates the most efficient way (since 0 and 360 are treated the same)
	if (Mathf.Approximately(t.startValue.y, 0f) && Mathf.Approximately(t.endValue.y, 270f))
		t.startValue = new Vector3(t.startValue.x, 360f, t.startValue.z);
	else if (Mathf.Approximately(t.startValue.y, 270) && Mathf.Approximately(t.endValue.y, 0))
		t.endValue = new Vector3(t.startValue.x, 360f, t.startValue.z);

	unit.dir = dir;
	
	while (t != null)
		yield return null;
}

Walk Movement

Create a concrete subclass of Movement called WalkMovement.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class WalkMovement : Movement 
{
	// Add code here
}

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.

protected override bool ExpandSearch (Tile from, Tile to)
{
	// Skip if the distance in height between the two tiles is more than the unit can jump
	if ((Mathf.Abs(from.height - to.height) > jumpHeight))
		return false;

	// Skip if the tile is occupied by an enemy
	if (to.content != null)
		return false;

	return base.ExpandSearch(from, to);
}

Next we must provide the implementation of the animation along a path. I created it sequentially using nested coroutines.

public override IEnumerator Traverse (Tile tile)
{
	unit.Place(tile);

	// Build a list of way points from the unit's 
	// starting tile to the destination tile
	List<Tile> targets = new List<Tile>();
	while (tile != null)
	{
		targets.Insert(0, tile);
		tile = tile.prev;
	}

	// Move to each way point in succession
	for (int i = 1; i < targets.Count; ++i)
	{
		Tile from = targets[i-1];
		Tile to = targets[i];

		Directions dir = from.GetDirection(to);
		if (unit.dir != dir)
			yield return StartCoroutine(Turn(dir));

		if (from.height == to.height)
			yield return StartCoroutine(Walk(to));
		else
			yield return StartCoroutine(Jump(to));
	}

	yield return null;
}

IEnumerator Walk (Tile target)
{
	Tweener tweener = transform.MoveTo(target.center, 0.5f, EasingEquations.Linear);
	while (tweener != null)
		yield return null;
}

IEnumerator Jump (Tile to)
{
	Tweener tweener = transform.MoveTo(to.center, 0.5f, EasingEquations.Linear);

	Tweener t2 = jumper.MoveToLocal(new Vector3(0, Tile.stepHeight * 2f, 0), tweener.easingControl.duration / 2f, EasingEquations.EaseOutQuad);
	t2.easingControl.loopCount = 1;
	t2.easingControl.loopType = EasingControl.LoopType.PingPong;

	while (tweener != null)
		yield return null;
}

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 flown over. All we need to implement is the animation for traversing the path:

using UnityEngine;
using System.Collections;

public class FlyMovement : Movement 
{
	public override IEnumerator Traverse (Tile tile)
	{
		// Store the distance between the start tile and target tile
		float dist = Mathf.Sqrt(Mathf.Pow(tile.pos.x - unit.tile.pos.x, 2) + Mathf.Pow(tile.pos.y - unit.tile.pos.y, 2));
		unit.Place(tile);

		// Fly high enough not to clip through any ground tiles
		float y = Tile.stepHeight * 10;
		float duration = (y - jumper.position.y) * 0.5f;
		Tweener tweener = jumper.MoveToLocal(new Vector3(0, y, 0), duration, EasingEquations.EaseInOutQuad);
		while (tweener != null)
			yield return null;

		// Turn to face the general direction
		Directions dir;
		Vector3 toTile = (tile.center - transform.position);
		if (Mathf.Abs(toTile.x) > Mathf.Abs(toTile.z))
			dir = toTile.x > 0 ? Directions.East : Directions.West;
		else
			dir = toTile.z > 0 ? Directions.North : Directions.South;
		yield return StartCoroutine(Turn(dir));

		// Move to the correct position
		duration = dist * 0.5f;
		tweener = transform.MoveTo(tile.center, duration, EasingEquations.EaseInOutQuad);
		while (tweener != null)
			yield return null;

		// Land
		duration = (y - tile.center.y) * 0.5f;
		tweener = jumper.MoveToLocal(Vector3.zero, 0.5f, EasingEquations.EaseInOutQuad);
		while (tweener != null)
			yield return null;
	}
}

Teleport Movement

Create our third and final concrete subclass of Movement called TeleportMovement. As with flying, the only thing we need to implement is the animation for path traversal.

using UnityEngine;
using System.Collections;

public class TeleportMovement : Movement 
{
	public override IEnumerator Traverse (Tile tile)
	{	
		unit.Place(tile);

		Tweener spin = jumper.RotateToLocal(new Vector3(0, 360, 0), 0.5f, EasingEquations.EaseInOutQuad);
		spin.easingControl.loopCount = 1;
		spin.easingControl.loopType = EasingControl.LoopType.PingPong;

		Tweener shrink = transform.ScaleTo(Vector3.zero, 0.5f, EasingEquations.EaseInBack);

		while (shrink != null)
			yield return null;

		transform.position = tile.center;

		Tweener grow = transform.ScaleTo(Vector3.one, 0.5f, EasingEquations.EaseOutBack);
		while (grow != null)
			yield return null;
	}
}

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 field to keep track of the currently selected unit and a property to wrap the currently selected tile.

public GameObject heroPrefab;
public Unit currentUnit;
public Tile currentTile { get { return board.GetTile(pos); }}

Init Battle State

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.

IEnumerator Init ()
{
	board.Load( levelData );
	Point p = new Point((int)levelData.tiles[0].x, (int)levelData.tiles[0].z);
	SelectTile(p);
	SpawnTestUnits(); // This is new
	yield return null;
	owner.ChangeState<SelectUnitState>(); // This is changed
}

void SpawnTestUnits ()
{
	System.Type[] components = new System.Type[]{ typeof(WalkMovement), typeof(FlyMovement), typeof(TeleportMovement) };
	for (int i = 0; i < 3; ++i)
	{
		GameObject instance = Instantiate(owner.heroPrefab) as GameObject;

		Point p = new Point((int)levelData.tiles[i].x, (int)levelData.tiles[i].z);

		Unit unit = instance.GetComponent<Unit>();
		unit.Place(board.GetTile(p));
		unit.Match();

		Movement m = instance.AddComponent(components[i]) as Movement;
		m.range = 5;
		m.jumpHeight = 1;
	}
}

Select Unit State

For the purposes of this demo, we are adding and inserting a state which wont be part of the final game flow. 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.

using UnityEngine;
using System.Collections;

public class SelectUnitState : BattleState 
{
	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		SelectTile(e.info + pos);
	}
	
	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		GameObject content = owner.currentTile.content;
		if (content != null)
		{
			owner.currentUnit = content.GetComponent<Unit>();
			owner.ChangeState<MoveTargetState>();
		}
	}
}

Move Target State

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.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MoveTargetState : BattleState
{
	List<Tile> tiles;
	
	public override void Enter ()
	{
		base.Enter ();
		Movement mover = owner.currentUnit.GetComponent<Movement>();
		tiles = mover.GetTilesInRange(board);
		board.SelectTiles(tiles);
	}
	
	public override void Exit ()
	{
		base.Exit ();
		board.DeSelectTiles(tiles);
		tiles = null;
	}
	
	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		SelectTile(e.info + pos);
	}
	
	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		if (tiles.Contains(owner.currentTile))
			owner.ChangeState<MoveSequenceState>();
	}
}

Move Sequence State

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.

using UnityEngine;
using System.Collections;

public class MoveSequenceState : BattleState 
{
	public override void Enter ()
	{
		base.Enter ();
		StartCoroutine("Sequence");
	}
	
	IEnumerator Sequence ()
	{
		Movement m = owner.currentUnit.GetComponent<Movement>();
		yield return StartCoroutine(m.Traverse(owner.currentTile));
		owner.ChangeState<SelectUnitState>();
	}
}

Summary

There was a lot to cover in this lesson, with lots of little setup material. Extension methods were introduced for the first time. The pathfinding algorithm was covered in detail, and made good use of passing along a delegate as a parameter for search validation. Finally we added and modified 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.

67 thoughts on “Tactics RPG Path Finding

    1. Glad you liked it! Feel free to translate and share it, as long as it remains free and as long as I remain credited as the original author. A link back to my blog is always appreciated too.

  1. In both your project download and my “follow along” project I get the following errors when testing out the battle.scene.

    When the scene loads:

    NullReferenceException: Object reference not set to an instance of an object
    MoveTargetState.Enter () (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:12)
    StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:40)
    StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
    StateMachine.ChangeState[MoveTargetState] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    InitBattleState+c__Iterator1.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:20)

    Whenever I click:

    NullReferenceException: Object reference not set to an instance of an object
    MoveTargetState.OnFire (System.Object sender, .InfoEventArgs`1 e) (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:31)
    InputController.Update () (at Assets/Scripts/Controller/InputController.cs:31)

    1. Let’s see if I can help you figure out what the problem is. I went ahead and downloaded a fresh copy of my repository and then checked out the commit “b1eecb68c1a7db006768529b42dbaea94c7dca92” which correlates to this part of the lesson checkin. I do not get any exceptions when running the scene. What version of Unity are you using? I am using Unity version 5.1.1f1 at the moment. If you are on a version before version 5 then your prefabs will almost certainly be broken and could explain why the repository project didn’t work for you.

      If you are using a compatible version of Unity then let’s see if we can solve the problem a different way. Since we know where the NullReferenceException is occurring, I usually try to solve it using Debug.Log calls like this:


      public override void Enter ()
      {
      base.Enter ();
      if (owner == null)
      Debug.Log("owner is null");
      if (owner.currentUnit == null)
      Debug.Log("currentUnit is null");
      Movement mover = owner.currentUnit.GetComponent();
      if (mover == null)
      Debug.Log("mover is null");
      if (board == null)
      Debug.Log("board is null");
      tiles = mover.GetTilesInRange(board);
      board.SelectTiles(tiles);
      }

      * note that in the code post above that some of the code is not displayed properly. The important thing to grasp is that you are checking each object against null. When you figure out “what” object is null we can next figure out “why” it is null.

    2. I’ve just had the exact same error and after going through every script and replacing the code; found out that I’d attached a copy of the BattleController script to the Main Camera in the hierarchy meaning there were two copies on the scene and one of them with no references. Hope it helps.

  2. This is out of scope for this project, but how would you do npc/ai movement in a free movement game (non-tiled)? Pathfinding, steering behaviours or a combination of both?

    1. I have tried various combinations of steering behaviours and way-points etc, but I haven’t personally found satisfactory results with it. The steering stuff was always too finicky and caused a lot of jittery and wobbly movement. However I have used Unity’s Navigation Mesh with pretty good results. If you haven’t looked at that you should give it a try.

  3. Hi Jon,

    I’m in a similar situation as @davooslice, but somewhat different (I didn’t get anything from MoveTargetState). I’m using Unity 5.2.3 (free).

    When I run the game, it displays the Board I created but it spawns a Hero Unit outside of it (somewhere without a Tile), and the Debug shows the following error:

    NullReferenceException: Object reference not set to an instance of an object
    InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:34)
    InitBattleState+c__Iterator1.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:18)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    InitBattleState:Enter() (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:42)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    BattleController:Start() (at Assets/Scripts/Controller/BattleController.cs:17)

    line 34 in my InitBattleState is “unit.Place(board.GetTile(p));” from the “SpawnTestUnits();” method, which seems to cause all the other stuff.

    PS: I did reference the heroPrefab to the GameController object in the Hirerachy, and I did review if all my code is following exactly what u said in the post and the repository.

    1. Since you’re actually seeing the unit get created It sounds like the unit is not null. The next candidate on line 34 would be the board. You also see the board, so I am guessing that is not null either, but you should verify that your state’s property is correctly wrapping the GameControllers reference. Try doing Debug lines printing out whether each object is null or not as I mentioned to davooslice.

      1. I suspect that whatever broke your ability to move the TileSelectionIndicator is probably related to the fact that setup didn’t complete so once you fix the null ref issue everything should be fine.

  4. Hi, I’ve been following this tutorial to help me create my own tactic RPG and it’s been really helpful. Thanks for taking to time to write this guide.

    I was having no problems until now, trying to implement the path finding system. It’s hard to follow which codes go where and the project i downloaded as a reference has different coding so it just confused me more. Right where I need to input the algorithm code, after the diagram, I get lost.

    1. Hey Robbie, I’m glad you’ve been enjoying it. The code for the searching algorithm goes into the “Board.cs” script.

      The project link that was on the intro page was just the initial prototype (I plan to update this page soon). The easiest way to help fill in any gaps in what I’ve described and what you should be doing is to look at the repository which has a check-in for each of these blog posts.

      You can grab the repository here: My Repository

      It is a git repository, hopefully you are familiar with git. If not, I would recommend getting a copy of something like SourceTree to help you manage it. In addition I have a blog post on the topic in general which might help:
      Intro to Source Control

      1. Thanks for the quick reply. Both links you provided gave 404 errors saying the link is dead.

        I went through this lesson again and the coding in Board.cs seems to be where I’m having trouble and I think I just have the codes in the wrong places or I’m missing something obvious

  5. Hey, thanks again for all the help. I managed to fix all the errors on the board.cs and I was able to make my heroes move. Problem now seems to be similar to what davooslice and Nelson had. The board, 3 heroes and the selection indicator show up and work fine but the fly and teleport heroes don’t physically move (no animation) even though their location shows being different.

    I get 3 errors when moving my heroes, one after the first hero successfully moves:

    NullReferenceException: Object reference not set to an instance of an object
    TransformAnimationExtensions.MoveToLocal (UnityEngine.Transform t, Vector3 position, Single duration, System.Func`4 equation) (at Assets/Scripts/Common/Animation/TransformAnimationExtensions.cs:40)
    WalkMovement+c__Iterator8.MoveNext () (at Assets/Scripts/View Model Component/Movement/WalkMovement.cs:66)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator6:MoveNext() (at Assets/Scripts/View Model Component/Movement/WalkMovement.cs:47)

    One when trying to move the teleport hero:

    NullReferenceException: Object reference not set to an instance of an object
    TransformAnimationExtensions.RotateToLocal (UnityEngine.Transform t, Vector3 euler, Single duration, System.Func`4 equation) (at Assets/Scripts/Common/Animation/TransformAnimationExtensions.cs:51)
    TeleportMovement+c__Iterator5.MoveNext () (at Assets/Scripts/View Model Component/Movement/TeleportMovement.cs:10)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator2:MoveNext() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:15)
    UnityEngine.MonoBehaviour:StartCoroutine(String)
    MoveSequenceState:Enter() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:40)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:24)
    MoveTargetState:OnFire(Object, InfoEventArgs`1) (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:32)
    InputController:Fire(Int32) (at Assets/Scripts/Controller/InputController.cs:64)
    InputController:Update() (at Assets/Scripts/Controller/InputController.cs:56)

    And one while trying to move the fly hero:

    NullReferenceException: Object reference not set to an instance of an object
    FlyMovement+c__Iterator4.MoveNext () (at Assets/Scripts/View Model Component/Movement/FlyMovement.cs:14)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator2:MoveNext() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:15)
    UnityEngine.MonoBehaviour:StartCoroutine(String)
    MoveSequenceState:Enter() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:40)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:24)
    MoveTargetState:OnFire(Object, InfoEventArgs`1) (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:32)
    InputController:Fire(Int32) (at Assets/Scripts/Controller/InputController.cs:64)
    InputController:Update() (at Assets/Scripts/Controller/InputController.cs:56)

    Any idea what I did wrong? I feel like I skipped something obvious.

    1. Unfortunately the problem could be anywhere. I’m guessing that it is a problem either in the way you setup the scene or the prefabs. For example, if you didn’t set up your Hero prefab’s hierarchy the same way I did, or if you had a typo in the name of one of the game objects, etc. then that could cause a null ref. Look in the “Awake” method of the Movement class and see how it looks through the hierarchy for a child named “Jumper” as an example. You could put a debug log in there to make sure it is finding everything it needs.

      Also, try to grab a copy of the repository. You can double click any of the check-ins to see how the project was set up at that time, including the scenes and prefabs. That should be the biggest helper.

  6. Outstanding CODE WRITING! (I’m a computer Science student on my 2nd year) such an AMAZING tutorial, sorry for the caps but i’m overwhelmed and its the first time I find myself so fascinated and I actually READ every single word you write.

    about the bugs people mentioned above, so I stumbled into both of them and managed to Debug using your advice, it seems that when you follow this kind of path finding guides at 3 am you seem to miss bits of explanations.

    to resolve this issue ::

    “When I run the game, it displays the Board I created but it spawns a Hero Unit outside of it (somewhere without a Tile), and the Debug shows the following error:

    NullReferenceException: Object reference not set to an instance of an object
    InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:34)
    InitBattleState+c__Iterator1.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:18)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    InitBattleState:Enter() (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:42)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    BattleController:Start() (at Assets/Scripts/Controller/BattleController.cs:17)

    line 34 in my InitBattleState is “unit.Place(board.GetTile(p));” from the “SpawnTestUnits();” method, which seems to cause all the other stuff.”

    Solution == go to your Hero and Monster Prefabs and make sure you actually Add the UNIT component (the Unit.cs script, otherwise it points to Null).

    2nd issue is as such :: ” I get 3 errors when moving my heroes, one after the first hero successfully moves:

    NullReferenceException: Object reference not set to an instance of an object
    TransformAnimationExtensions.MoveToLocal (UnityEngine.Transform t, Vector3 position, Single duration, System.Func`4 equation) (at Assets/Scripts/Common/Animation/TransformAnimationExtensions.cs:40)
    WalkMovement+c__Iterator8.MoveNext () (at Assets/Scripts/View Model Component/Movement/WalkMovement.cs:66)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator6:MoveNext() (at Assets/Scripts/View Model Component/Movement/WalkMovement.cs:47)

    One when trying to move the teleport hero:

    NullReferenceException: Object reference not set to an instance of an object
    TransformAnimationExtensions.RotateToLocal (UnityEngine.Transform t, Vector3 euler, Single duration, System.Func`4 equation) (at Assets/Scripts/Common/Animation/TransformAnimationExtensions.cs:51)
    TeleportMovement+c__Iterator5.MoveNext () (at Assets/Scripts/View Model Component/Movement/TeleportMovement.cs:10)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator2:MoveNext() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:15)
    UnityEngine.MonoBehaviour:StartCoroutine(String)
    MoveSequenceState:Enter() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:40)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:24)
    MoveTargetState:OnFire(Object, InfoEventArgs`1) (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:32)
    InputController:Fire(Int32) (at Assets/Scripts/Controller/InputController.cs:64)
    InputController:Update() (at Assets/Scripts/Controller/InputController.cs:56)

    And one while trying to move the fly hero:

    NullReferenceException: Object reference not set to an instance of an object
    FlyMovement+c__Iterator4.MoveNext () (at Assets/Scripts/View Model Component/Movement/FlyMovement.cs:14)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    c__Iterator2:MoveNext() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:15)
    UnityEngine.MonoBehaviour:StartCoroutine(String)
    MoveSequenceState:Enter() (at Assets/Scripts/Controller/Battle States/MoveSequenceState.cs:9)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:40)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/Path/StateMachine.cs:24)
    MoveTargetState:OnFire(Object, InfoEventArgs`1) (at Assets/Scripts/Controller/Battle States/MoveTargetState.cs:32)
    InputController:Fire(Int32) (at Assets/Scripts/Controller/InputController.cs:64)
    InputController:Update() (at Assets/Scripts/Controller/InputController.cs:56)

    Any idea what I did wrong? I feel like I skipped something obvious. ”

    Solution for this Issue :: in InitBattleState.cs check for the following code ill post and make sure its exactly the same

    IEnumerator Init()
    {
    board.Load(levelData);
    Point p = new Point((int)levelData.tiles[0].x, (int)levelData.tiles[0].z);
    SelectTile(p);
    SpawnTestUnits(); // MAKE SURE YOU ACTUALLY ADDED THIS LINE!!!
    yield return null;
    owner.ChangeState(); // MAKE SURE YOU ACTUALLY CHANGED THIS LINE!!!
    }

    Good luck to everyone, and realy realy realy good job Jon. I wish you will keep up adding tutorials.
    maybe expand outside of the Battle state into a Sandbox world? 😀

    1. Daniel, you are very kind! Thanks – I am always glad to hear you enjoy this since I worked so hard on it! Also, thanks for taking the time to write the solutions to the other people’s issues, I hope it helps them. It’s hard for me to know where people get stuck because it’s hard to look at the content with fresh eyes.

  7. Hello! These lessons are awesome, I’m really learning a lot!
    There’s just something wrong with this chapter: is the flying unit supposed to float up, move, then land? Because mine doesn’t float, and instead clips right through the terrain. Have I skipped something…?
    There’s nothing in the console tab, and I’m using Unity 5.1.0f3.

    Thanks in advance!

    1. Yep, the flying unit should float up, move, then land. One possibility is that you missed something with the setup of the enemy prefab. Download the project from the repository and compare your setup against the one there so you can make sure that the hierarchy and connections all match.

      If everything there matches, you might try adding some console logs to the FlyMovement’s Traverse method. Verify that the “y” position to animate to isn’t zero, etc.

  8. The y position is correct. I’ve also noticed that the walking unit doesn’t jump, and the teleporting unit doesn’t spin. I’ll try and compare my project with the repository, thanks!

  9. The only difference between my current FlyMovement.cs and yours is that mine (and the code snippet in your post) has “unit.place(tile)”, whereas the repository version has “yield return StartCoroutine(base.Traverse(tile))”. But this can’t work, since base.Traverse is an abstract method, so I went to check Movement.cs as well, and found that this time there were several key differences between the code snippets and the repo. Is this simply not supposed to work correctly until further on in the project? 😀

    P.S. All this said and done, setting the Hero prefab’s Sphere object as a child of the Jumper object, instead of a sibling, fixed everything. My bad.

    1. I’m glad you got it working. Also, in case you run into any issues in the future, it is worth pointing out that each step of the tutorial should work on its own, although of course features are added and improved on over time in this series.

      Also, the repository allows you to “checkout” the project like it would have looked at any chapter of this tutorial. Grab yourself a copy of something like Atlassian Sourcetree if you haven’t already. This will make it easy to see the various commits and check out the one you want as well as to easily see exactly what code was added/removed etc between each lesson.

    1. Looking great 🙂

      There will be little things all over the project that will need to be updated. Currently everything is designed around the concepts of cardinal directions like ‘N’,’E’,’S’,’W’. It doesn’t make as much sense to use that on a Hex Grid since you might use ‘NE’ and not ‘E’ in order to get the 6 different directions. It might be that you use some other enum, or that you simply resort to angles (in degrees or radians) for everything.

      You could also still navigate the board using keyboard input if you wanted, such as “left” moving the cursor down-left, and “right” moving the cursor up-right. Up and down would work normally. I would want to keep this as a backup just in case some board configurations or unit sizes made it difficult to click a tile you wanted to target.

      To use a mouse, I would begin by using Unity’s event system (the same one used on canvas buttons). You can attach a 3D raycaster to your camera to handle this. It would be easy to make each tile include its own collider and event and post an event when detecting a mouse click. Using the battle states, you could basically just register to listen for those clicks when it is time for the user to make a move, and then unregister after input has been provided. Don’t forget to either remove colliders from units, put their colliders on a different layer, or use logic so you can click units or tiles.

  10. Hi sir jon! Can I ask where exactly should I place the animation of the “Attack” and the “Skill” animations? I’m tried to place it on the “Ability” script, but everytime 1 character attacks, everyone else attacks on the same time. Oh and btw i’m using boolean as parameters. I’m already able to place the walk anim on the Walk movement.Though now i’m having a hard time where to put the animation of attack. Can you please guide me? Thanks to your project, i’ve learned a lot. More power to you, sir! 😀

  11. Hi, I’m really enjoying this tutorial! One question though. I’m still wrapping my head around the pahfinding algorithm, so please excuse me if this is really basic. Say I have different tile types (I’ve already successfully implemented this in my own project) of grass and water. I don’t want the units to be able to travel as far in water as they can on grass (unless they are a swimming type unit, then I’d want it to be reversed). How would the implementation of this work? Would it greatly complicate the algorithm? I would want it to be visually represented when the tiles are highlighted by a unit.

    Thanks!

    1. Great question- One suggestion would be to modify the way that distance is added. Currently I always add exactly “1” to the distance when adding a new tile:
      next.distance = t.distance + 1;
      … but you could add another method reference (kind of like we passed the “ExpandSearch” method) which would return a custom travel cost. This way it is generic and the different types of units can have different movement bonuses or penalties based on their own type and the terrain they traveled on. Perhaps it would look something like this:
      next.distance = t.distance + travelCost(t);
      … where “travelCost” was a method that accepted a tile parameter (the tile you would be traversing) and would return a value for the cost for the current unit to move across it.

      1. Perfect, this is exactly what I needed! I think digging deeper into it is helping me understand it much better as well.

        By the way, I do quite a bit of teaching and advocacy of programming for audio producers (I started out as a game composer and have been learning programming for a few years to help with implementation and audio system programming in Unity and Unreal), so I’d love to share your tutorials as a resource for learning with students. I stumbled upon them by accident, but they are really good and helping me learn a lot. Let me know if there is any specific link you’d want me to use or anything like that.

        1. Glad to help Brennan 🙂 Also always glad to connect with another pro. Do you have a portfolio site or something? Would be happy to hear some of your work.

          You are free to link to the content however you think is most helpful, whether that is the home page, projects, or a specific category etc.

      2. Absolutely! I have my personal site which is a tiny bit outdated, but the SoundCloud player on it is current. Feel free to shoot me an email, would love to connect if we are in a similar area (I’m based out of San Francisco, but travel to LA and Austin every once in a while).

        http://brennananderson.com/

      3. Thanks! FIY, once I finish the tutorial, I fully plan on writing some SNES inspired music for my version. I’d be happy to send it over if you’d like to include it in your version.

      4. Yeah, no problem! I don’t know when I will be done with it yet, but I will keep you posted. I started about a week ago, and I am currently working on the area of effect lesson.

      5. Hey there, so I am making a bunch of progress on my own version of this project. I have finished your main tutorial (did about a week ago) and have been adding a bunch of my own things. I’ll be taking a look at your shop and bestiary tutorials, then I plan to put a full, albeit short, game with a story, map screen, sufficient menus, etc.

        I also just finished writing the battle music for it. You can listen to the music at the link below, and if you want to PM me, I can send you the audio files.

        https://soundcloud.com/brennan-a/voiceless-confrontation

  12. Hey, I’m really enjoying this tutorial, its really helpful for trying to figure out grid based systems. Going through this phase of the tutorial however everything is working fine but it seems that for me my spawned units X position is 1 point lower than where they can be selected on the board. In other words visually they appear in 1 X position lower than where the system says.

    Ive been trying many things to fix it however havent fully figured out whats causing it. Any help would be appreciated.

    Thanks

  13. I’m yet another guy who only found your tutorial in 2017, long after you were done with it.

    I’m having some weird problem in taht with the first board I created (just randomly, for testing the pathfinding code), the tile selection wouldn’t move to some parts of the board (I thought due to geography, but most notably south), and when I selected a place to the north, the units would instead turn east and move.

    Later I replaced the board with a flat one, and when I tried again, the units refuse to move at all.

    Do you have any idea where I could be going wrong here?
    I copy-pasted the code (while reading both it and the blog) rather than typing it myself, so I can’t find any typos.

    1. I haven’t heard any problems similar to this yet… My initial guess for some of the issues, like not being able to navigate far enough in one direction or having your units turn to face the wrong direction, makes me wonder if your prefabs are setup incorrectly. Look through all of the GameObjects in the tile, tile selection indicator, and unit prefabs and make sure that they are all centered around the origin (X,Y,Z all zero) and also that the rotation is zeroed out. That wouldn’t cause them to not be able to move at all on a different board though. Not really sure what happened there, maybe your game states got out of whack somehow. Have you tried downloading the demo project and comparing against it yet?

      1. Well, that was dumb of my part.
        Tripple checked again, then noticed the transform for the battle controller gameobject wasn’t reset.
        Once it was reset, everything worked.

  14. Hey! First off, this tutorial series is awesome, thank you so much.

    My question is that in the board script I’m getting an error in the “GetTilesInRange” section.

    Inside the following:
    List retValue = board.Search(unit.tile, ExpandSearch);

    Expanded search is giving me an error saying “Argument2: cannot convernt from ‘method group’ to ‘Func’

    Any idea what I’ve done wrong here? I’ve tried checking it against the latest version as well, which adds a ton of stuff to board, however it still retains the highlight.

    Cheers,

    Alex

  15. My spouse and I stumbled over here different web page and thought I might
    as well check things out. I like what I see so i am just following you.
    Look forward to going over your web page yet again.

  16. Hi there, long time reader, first time posting.
    I was thinking about modifying the scripts a little, to include jumping pits/high cost (assuming different terrain types) tiles in the pathfinding for walking units.

    The idea is to add an exception to the Board.cs Search method: check the next tile in the row/column if the tile being checked is too low/ the cost assigned is high.

    Do you think this approach is a good one? Is there a better one in your mind? Looking forward to your reply.

    1. Yes, that would work fine. Specifically, I would probably add the new code to the same place where I set the “distance” of a “next” tile. You already have a reference to where you are standing and trying to get to, so it should be easy to compare jump heights and add a penalty. The same would also work with penalties on differing terrain types like entering water or mud etc.

  17. Have you ever thought about including a little bit
    more than just your articles? I mean, what you say is fundamental and all.

    Nevertheless just imagine if you added some
    great images or video clips to give your posts
    more, “pop”! Your content is excellent but with
    pics and videos, this blog could undeniably be one of the best in its niche.

    Wonderful blog!

    1. Thanks, I am glad you like it! Adding the things that make a blog “pop” has been one of the number one requests from my fans, so it is definitely something I am concerned about. Of course creating those extra pics and videos, like every other aspect of this process, take a lot of time and would greatly benefit from someone with that skillset. This poses a challenge to me as a solo dev who isn’t focused on art. Still, as I have continued blogging over time I have increased my efforts at including more. With enough support from patrons I might even be able to pay for help, who knows 🙂

  18. I do not even know how I stopped up right here, however I believed this post
    was great. I don’t know who you might be however
    certainly you’re going to a well-known blogger when you are not already.
    Cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *