GameDev Blog #4: Moving Units on the Land Tiles

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. Last post I created some land tiles and aligned them in a grid/map of sorts. This post, I hope to get units to be able to move on those tiles when the player clicks on them.

This project’s GitHub can be found here. The zip archive for this specific post can be found here.

Setting Up the Land Tiles

I want Unity to detect when a user clicks on a land tile in the same way it detects when the player clicks on a unit. To do this, I will first add a “Land” layer to Unity, and then apply that layer to the land tile prefab.

First, select any of the land tiles, go to the Inspector window, and then click on the drop down next to “Layer” at the top. Select “Add Layer…”

Then, add a new layer called “Land.” I added mine at the #9 layer slot. I’m not sure if this matters that much. I figured if Units were at “10” and land was at “9” that my Units layer would be “above” my land layer when clicking, but I believe this number is just a ID/reference number for the layer and nothing else. If these were “Sorting Layers,” a different thing within Unity, then I believe the layer number matters in terms of what is displayed on top of what. Anyway, add the land layer.

Select a land tile again and select “9: Land” from the drop down menu.

Right now the land layer was only applied to the single land tile you had selected. It was not applied to the prefab. You can save this change to the prefab, though, by using “Overrides.” Make sure you still have the land tile selected, and then in the Inspector window click on the drop down labeled “Overrides.” Then, click on “Apply All”

Now the hex-tile-land prefab has its layer set to “Land.” If you double check other tiles in your scene now, you’ll see that they all have the Land layer set.

Mouse Interaction with Land Tiles

With the land layer created and applied to the land tiles, the MouseClickManager.cs script will be modified so that it will detect when the user clicks on units AND land tiles.

First, create a new LayerMask variable called “landLayer” that is a serialized field so that it will show up in the Inspector.

[SerializeField]
private LayerMask landLayer;

Save MouseClickManager.cs and then go back into the Unity Editor. Select the MouseClickManager object in the hierarchy, and then add “Land” as the “Land Layer” variable in the attached MouseClickManager.cs script.

To detect when a user clicks on a unit, MouseClickManager.cs uses a RaycastHit2D variable called rayHitUnit that detects hits on the Units layer. For detecting user clicks on land tiles, you’ll do the same thing but it will detect hits on the Land layer. This will be done in the Update() function after you detect when the left mouse button is pressed down.

RaycastHit2D rayHitUnit = Physics2D.Raycast(mousePosition2d, Vector2.zero, Mathf.Infinity, unitLayer);
RaycastHit2D rayHitLand = Physics2D.Raycast(mousePosition2d, Vector2.zero, Mathf.Infinity, landLayer);

Then, after the if statement that detects if rayHitUnit’s collider is not null, create an else if statement that detects if rayHitLand’s collider is not null. This will tell you if the user clicked on a land tile.

else if (rayHitLand.collider != null)
{
	Debug.Log("User clicked on land tile.");
}

Save MouseClickManager.cs and then start the game in Unity. Click on a land tile, and you should see that Unity is detecting your mouse clicks.

Adding Units Back In

When you first add the units back into the game by dragging and dropping the prefab into the scene, you’ll see that they are, well, huge relative to the land tiles.

After some trial and error, I found that setting the X/Y scale for the infantry to 0.25, and the X/Y scale for the tank to 0.4 gave me units that were a size I wanted for the map.

I want both of the units to be small enough so the two of them can be placed on the same tile without overlapping.

To make sure everything renders correctly, I want to make sure that the units are in a sorting layer “above,” or at a higher number than, the land tiles. Select one of the units and set its “Order in Layer from 0 to 2.

Do that for both the infantry and the tank, and apply the override to both so it applies to the prefab. This will also save the new scale values for the two units. Then, make sure to make the same scale and sorting layer changes to the tank and infantry outline prefabs.

Then, select a land tile, and set its order in layer to 1, then apply the override.

This should, for now, prevent the land tiles from displaying in front of units, so that you can always see a unit when it’s on the screen.

Positioning The Unit Relative to a Land Tile

When units are moved onto a land tile, I want the unit to be positioned in a specific spot relative to the land tile. This way tanks will always be placed in the “tank” position on a tile, and infantry will always be placed in the “infantry” position on a tile.

To test where I wanted my relative positions, I selected “hex-tile-land” which was located a X:-3.45, Y:1.5. I first set the tank and infantry unit to that same location. This causes them to be one on top of the other on the tile.

I decided to have the tank unit on the top half of the tile and the infantry unit on the bottom half. The units should have the same “X” position as the tile they are on, but their “Y” position will be different. The tank’s “Y” value will be higher, and the infantry’s “Y” value will be lower.

I tested what these relative “Y” positions should be by moving my units up and down until I got something I liked. I settled on the tank being the land tile’s Y+0.5, and the infantry being the land tile’s Y-0.5. This results in the units looking like this:

Now you can clearly see where the tank is and where the infantry is. The Y+/-0.5 will be used later when repositioning units after they are moved.

Beginning to Move the Units

I want the game to move a player’s units when the player has at least one unit selected, and the player then left clicks on a land tile.

So, in MouseClickManager.cs, change the else if statement that detects if the rayHitLand collider is not null to also check if the unitsSelected list has a count greater than 0. I also included a check to make sure that the rayHitUnit collider IS null, so that the player doesn’t move units when they click on another unit.

else if (rayHitLand.collider != null && unitsSelected.Count > 0 && rayHitUnit.collider == null)
{
	Debug.Log("User clicked on land tile.");
}

Save MouseClickManager.cs and test this out in the Unity Editor. Make sure that you only see the debug message about click on a land tile when you have at least one unit selected. If you run into any issues with units or their outlines not displaying correctly, or you can’t click on a unit, double check the following in the prefab of the unit/outline:
1.) The Layer is set to Units
2.) The scale is set correctly. Infantry is 0.25. Tank is 0.4
3.) The “Order in Layer” for the sorting layer is set to “2”

Movement Function

In the UnitScript.cs script, I am going to add a function called “MoveUnit” that will move individual units to a specified land object. The function will be public, and it will accept the land tiles GameObject as a parameter. The MoveUnit function will then place the tank and infantry into their designated area relative to the land tile’s position.

public void MoveUnit(GameObject LandToMoveTo)
{
	Vector3 temp = LandToMoveTo.transform.position;
	if (gameObject.tag == "tank")
	{
		temp.y += 0.5f;
		gameObject.transform.position = temp;
	}
	else if (gameObject.tag == "infantry")
	{
		temp.y -= 0.5f;
		gameObject.transform.position = temp;
	}
}

The MoveUnit script detects if the unit is a tank or infantry based on its “tag.” You may be wondering what that is, because so far I have failed to mention it. So, here is how to create the tank and infantry tags.

Creating Tags

Select the infantry unit in the scene. In the Inspector window, click on the “Tag” drop down at the top.

Click on the “+” sign to add a new tag.

Type in “infantry” and click save. Tags are case sensitive. Make sure the case of your tag matches the code you are using to check for tags.

You can also click on the “+” sign again and add the “tank” tag here.

Select the infantry unit in the scene and go back to the Inspector. Click the tag drop down and select “infantry.” Then apply the override so it is applied to the infantry prefab.

Do the same for the tank unit to make sure it as the tank tag and apply its override.

Call the Movement Function

The MoveUnit function in UnitScript.cs will be called by MouseClickManager.cs. So, when a user clicks on a land tile when they have units selected, the MoveUnit function will be called for each individual unit.

I decided, for whatever reason, to create a new function in MouseClickManager.cs that will called UnitScript’s MoveUnit function. It is called “MoveAllUnits” and takes the land tile’s GameObject as a parameter.

void MoveAllUnits(GameObject landClicked)
{
	if (unitsSelected.Count > 0)
	{
		foreach (GameObject unit in unitsSelected)
		{
			UnitScript unitScript = unit.GetComponent<UnitScript>();
			unitScript.MoveUnit(landClicked);
		}
	}
}

MoveAllUnits will first check that the count of unitsSelected is greater than 0. This should always be true when the function is called, but I added the check anyway. A foreach loop is then used to iterate through each unit in unitsSelected. That unit’s UnitScript is then used to called MoveUnit, and the “landClicked” GameObject is passed to MoveUnit.

I changed my else if statement that detects when the player wants to move units to the following:

else if (rayHitLand.collider != null && unitsSelected.Count > 0 && rayHitUnit.collider == null) // if the player has selected units previously and clicks on a land, check if the units can be moved)
{
	MoveAllUnits(rayHitLand.collider.gameObject);
	ClearUnitSelection();
}

The else if statement is only true when all of the following conditions are met:
1.) The user clicked on a land object
2.) The user has previously selected units
3.) The user did NOT click on a unit object

If all of the above is true, then MoveAllUnits is called and the gameObject of rayHitLand is passed as a parameter. After MoveAllUnits completes, I did use ClearUnitSelection() to clear the unit selection.

Feast your eyes on the unit movement in this video:

That’s all for now. You can move units! Hurray!

Next Steps

Right now, the player can move a unit to any tile on the map. Next, I want to add some limitations on where a unit can be moved to. Some of those limitations:
1.) A unit can only move to an adjacent tile
2.) There can be no more than 5 TOTAL units on a tile

I also want to do something to handle multiple units of the same type on a tile. Showing a bunch of infantry or tanks on one tile gets cluttered in my very simple visual setup. I want multiple units to “collapse” to a single unit, visually. After the units “collapse,” some text will pop up next to the unit like “x2” or “x3” to show that there are 2 or 3 units of that type on the tile.

Then, when the player clicks on that unit type that has multiple units, the units will “expand” and show all the units spread out on the tile and the text disappears. If the player de-selects the unit, all the units collapse back down. Confusing? Well, hopefully I can explain it better in my blog post!