GameDev Blog #6: Limiting Unit Movement

Disclaimer: I am neither a professional game developer nor am I a professional programmer. I doubt I am following best practices. This is just what worked for me at the time. Hopefully it works for you if you’re playing along while reading!

Version of Unity used: 2019.4.13f1 Personal

The previous posts in this series can be found under the CardConquest category on this blog. The github for the project can be found here. The zip archive for this specific post can be found here.

As it stands right now, the player can move units from one land tile to any other land tile in the game. I want to make it so that the player can only move their unit to an adjacent tile. Basically, I want it so the player can only move a distance of 1 tile away from their current position.

Getting Initial Position

In order to calculate the distance a unit is moving, you first need to know the unit’s starting location. The land tile a unit is on is currently stored in UnitScript.cs’s “currentLandOccupied.” However, currentLandOccupied isn’t set until a unit is moved. This creates a problem in that (for right now), when the “game” starts, non of the unit’s will have moved since they were manually placed at specific tiles. So, their currentLandObject value will be null. That will cause issues when calculating movement distances, so I am going to create a quick function to get the land tile the unit is on when the game starts.

The land tile a unit starts on will be detected by firing a Raycast from the unit’s position and checking if it hit anything in the “Land” layer. So, first, UnitScript.cs will need to have the Land layer saved in a variable. Add the following near where the other global variables are:

[SerializeField]
private LayerMask landLayer;

Then, open the infantry prefab in Unity’s Inspector window and set the “Land Layer” to Land. Do the same for the tank prefab.

A new function will be created in UnitScript.cs called GetStartingLocation. A RaycastHit2D will be cast from the unit’s transform.position. If that raycast hit’s an object in the “Land” layer, it will be used to pass that land object to the UpdateUnitLandObject function. UpdateUnitLandObject will then add the land object to currentLandOccupied, as well as call all the necessary functions in LandScript.cs to make sure the unit is recognized as “occupying” the land tile. The code is as follows:

void GetStartingLandLocation()
{
	RaycastHit2D landBelow = Physics2D.Raycast(transform.position, Vector2.zero, Mathf.Infinity, landLayer);
	if (landBelow.collider != null)
	{
		UpdateUnitLandObject(landBelow.collider.gameObject);
	}
}

Then, just add the call to GetStartingLocation in UnitScript.cs’s Start() function, and when the game starts, the unit will be occupying a land tile and their currentLandOccupied value will be set.

Calculating Movement Distance

I want the limit for unit movement to be 1 tile away from their starting location. Based on the hex map setup, that means a unit can move up to 6 different tiles surrounding their current tile. The max distance above or below a tile on the Y axis should be “3”, and the max distance to either side of a tile on the X axis should be “2.3.” These numbers come from when I was placing the tiles on my makeshift “grid” earlier.

So, you could try and calculate out the distance moved on the X and Y axes and see if one or the other is over the limit, or you can use a builtin function of the Vector3 type in Unity called distance. Distance takes two vector3 values and calculates the distance between them. Every land tile has a transform.position that is its vector3 location, so if you provide the transform.position of currentLandOccupied and the land tile the user clicked on, you should be able to use Vector3.Distance to calculate the distance.

To determine what the maximum “distance” a unit should move to stay within a 1 tile radius, I added the following debug code to the end of the UpdateUnitLandObject function, just before the currentLandOccupied is replaced with the new land unit the user moves to.

float disFromCurrentLocation = Vector3.Distance(LandToMoveTo.transform.position, currentLandOccupied.transform.position);
Debug.Log("Unit moved distance of: " + disFromCurrentLocation.ToString("0.00"));

Based on the debug output, the maximum distance a unit should be able to move is 3.00f.

Check if All Units Can Move

Since a player can select and move multiple units at once, you need to make a “can a unit move this far” check for every unit the player has selected. I decided to have a function in UnitScript.cs have a bool function called CanAllSelectedUnitsMove that will return true or false if the units can move. If all units can move, it returns true. If just one unit is too far away, it returns false.

Create MouseClickManager.cs Instance

First, in order to make this check, UnitScript.cs needs access to the unitsSelected list in MouseClickManager.cs. To do this, an instance of MouseClickManager.cs will need to be created.

In MouseClickManager.cs, add the following to the top where you declare your variables:

public static MouseClickManager instance;

Now, this instance will need to be created. Create a function called MakeInstance:

void MakeInstance()
{
	if (instance == null)
		instance = this;
}

And then called MakeInstance from MouseClickManager.cs’s Start()/Awake() function. Now, after the game has started, other scripts can access public variables and functions of MouseClickManager.cs, such as accessing the unitsSelected list.

Back to CanAllSelectedUnitsMove Function

Back in UnitScript.cs, the CanAllSelectedUnitsMove function can be created. The code is as follows:

public bool CanAllSelectedUnitsMove(GameObject landUserClicked)
{
	bool canMove = false;
	foreach (GameObject unit in MouseClickManager.instance.unitsSelected)
	{
		UnitScript unitScript = unit.GetComponent<UnitScript>();
		float disFromCurrentLocation = Vector3.Distance(landUserClicked.transform.position, unitScript.currentLandOccupied.transform.position);
		if (disFromCurrentLocation < 3.01f)
		{
			Debug.Log("SUCCESS: Unit movement distance of: " + disFromCurrentLocation.ToString("0.00"));
			canMove = true;
		}
		else
		{
			Debug.Log("FAILURE: Unit movement distance of: " + disFromCurrentLocation.ToString("0.00"));
			canMove = false;
			return canMove;
		}
	}
	return canMove;
}

CanAllSelectedUnitsMove starts by create a boolean variable “canMove” and setting it to false. Then:
1.) A Foreach loop is used to iterate through each unit in the unitsSelected list
2.) the UnitScript from the unit is stored in unitScript
3.) The distance from the unit’s currentLandOccupied to the new land is calculated
4.) If the distance is less than 3.01, set canMove to true
5.) If the distance is greater than or equal to 3.01, set canMove to false AND return canMove. Returning canMove essentially exits out of the loop

Now, CanAllSelectedUnitsMove will need to be called in MouseClickManager.cs when the unit is moved. MouseClickManager.cs will use the first unit in the unitsSelected list to call CanAllSelectedUnitsMove. If CanAllSelectedUnitsMove returns true, then MoveAllUnits will be called.

if (unitsSelected[0].GetComponent<UnitScript>().CanAllSelectedUnitsMove(rayHitLand.collider.gameObject))
{
	MoveAllUnits(rayHitLand.collider.gameObject);
}  

And now, all units can only move 1 space at a time. Video!

Limiting by Total Units on Tile

I also wanted to limit the number of units that could be on a single tile. I wanted there to only be 5 total units allowed on a tile. This involves adding one more simple check to CanAllSelectedUnits move:

LandScript landScript = landUserClicked.GetComponent<LandScript>();
int totalUnits = MouseClickManager.instance.unitsSelected.Count + landScript.tanksOnLand.Count + landScript.infantryOnLand.Count;
if (totalUnits > 5)
{
	Debug.Log("Too many units to move.");
	canMove = false;
	return canMove;
}

All the new check does is:
1.) First, get the land script of the land object the user is moving to
2.) Add the count of unitsSelected to the counts of the land tiles tanksOnLand and infantryOnLand lists
3.) If the total is greater than 5, return canMove as false

And, voila! Now only 5 total units can occupy a land tile. Great!

Next Steps

Next, I think I will do something simple like have a title screen that game starts in, and maybe add in an Esc/pause menu. See you later!