Tilemap interaction at runtime

Now that we have somewhat generated a tilemap for our world, this tutorial will introduce
an example on how to actually interact with this tilemap during our gameplay.
Some in this example, I will be doing the following:

  • Create a player character that can free move in any direction using physics movement (rigidbody2d)
  • Drop snow tiles where he walks (as an example interaction with the tilemap)
  • General code cleanup (will be starting from a cleaned up base)

New improved cleaned up base code

I would like to start this tutorial on a more cleaned-up version of the tile code.
I have made a few changes to the existing code, this can all be nicely seen here in this commit:
https://github.com/Venom0us/Code2DTutorials/commit/e5d1008e49ae14712f7be905570950965163224e

Quick sum-up for the major changes to code:

  • Added “IsDirty” property on TilemapStructure to see if a tilemap has changes that aren’t displayed yet to the graphic object (UnityTilemap)
  • Removed a lot of clutter and code duplication regarding the tiledata (TileTypes)
  • Renamed a few methods and removed some properties to make code more readable
  • Added exceptions a lot more clear to read, to understand whats wrong if config is missing etc
  • Added a more generic way of adding new tilesets using AddTileSet method in TileGrid.cs (GroundTiles,ObjectTiles, etc)
  • Added UpdateTile(x,y) and UpdateTiles(Vector2Int[]), UpdateTiles() methods to apply changes to the graphic
  • Added overloads to SetTile to update the tile graphic (default false), isDirty will be set by default true
  • Added some more comments to methods to better explain what they do
  • Moved TileTypes to its own class for easier expansion

Note: This tutorial will continue on from this new cleaned up base.
So if you want to follow along, you can get the source files here:
https://github.com/Venom0us/Code2DTutorials/tree/e5d1008e49ae14712f7be905570950965163224e

Player creation and movement

I quickly created a small 16×16 player png file to use as the player sprite, it can also be downloaded from the github repository here:
https://github.com/Venom0us/Code2DTutorials/blob/master/Assets/Textures/Player.png

I set this texture up like so:

Also added a custom physics shape to help the polygon collider define the initial collider shape for the player like so:

Next I just simply dragged this texture into the sceneview, I tried moving it somewhere onto a grass field based on my generation (by running the map and moving the player in runtime, and copying the position).
Also make sure to set the sprite render order to atleast 2 (both grids are at 0 and 1, and player should be rendered on top both) or you won’t see the player at all when the map is rendered, like so:

Next I also made the MainCamera a child of this player sprite like so:

This will make it so the MainCamera will follow the player where he walks.
This are the settings for the camera, I adjusted the Size to 10, so it zooms out a bit
Also make sure the camera x, y position are at 0 (so centered on the player) like so:

Next up we add a polygon collider 2d to the player gameobject, and also a rigidbody2D.
It is important to make sure the gravityscale is at 0 so the player won’t just fall down.
Also collision detection should be on Continous, this prevents weird collider interactions, and getting stuck on colliders etc..
Freeze rotation is also a good one, don’t want to spin into weird angles when colliding with objects.

Here comes the fun bit, actually creating a controller script to move the player using the rigidbody, and doing some tilemap interaction. Lets begin with the script.
We can create a new script called PlayerController on the player gameobject, you can do this from the inspector (it will generate a monobehaviour template that looks something like this:)

using UnityEngine;

public class PlayerController : MonoBehaviour
{
	// Start is called before the first frame update
	void Start()
	{

	}

	// Update is called once per frame
	void Update()
	{

	}
}

This is nice, because we will need both methods. To start I would like to put this within the Assets.Player namespace so lets do that first:

using UnityEngine;

namespace Assets.Player 
{
	public class PlayerController : MonoBehaviour
	{
		// Start is called before the first frame update
		void Start()
		{

		}

		// Update is called once per frame
		void Update()
		{

		}
	}
}

We want access to a few things:

  • The grid script, which can give us access to all the tilemap connected to it
  • The rigidbody to apply movement
  • Some float variable that we can set the player speed on

Lets add these 3 things, we can easily add all these using unity’s serialization like so:

using UnityEngine;

namespace Assets.Player 
{
	public class PlayerController : MonoBehaviour
	{
	    [SerializeField]
        private TileGrid _grid;
		[SerializeField]
        private Rigidbody2D _rigidbody;
		[SerializeField]
        private float _movementSpeed;
		
		// Start is called before the first frame update
		void Start()
		{

		}

		// Update is called once per frame
		void Update()
		{

		}
	}
}

Now if you save and look in the inspector (assuming ofcourse the script is on the player gameobject) you will see these 3 fields popup here:

You can drag or select these components into the fields, I also dragged our Grid gameobject onto the grid field, for movement speed I took 4

Next up we will make the character able to walk in any direction using rigidbody movement. To do this, unity provides us some helper input functionality that
gives us information on keypresses, we are interested in up,down,left,right (or horizontal/vertical). Unity gives us access to the following:

Input.GetAxisRaw("Horizontal");
Input.GetAxisRaw("Vertical");

Which basically gives us either -1, 0 or 1 as value for both, the Horizontal means keys left/right and the Vertical means keys up/down these keys can be configured within unity’s project settings:

Since I use azerty, I use q/d for left/right and z/s for up/down

Now that this is configured we can apply this code in our update method (this will be triggered every frame, and we are interested in retrieving our keypresses every frame.

using UnityEngine;

namespace Assets.Player 
{
	public class PlayerController : MonoBehaviour
	{
	    [SerializeField]
        private TileGrid _grid;
		[SerializeField]
        private Rigidbody2D _rigidbody;
		[SerializeField]
        private float _movementSpeed;
		
		// Start is called before the first frame update
		void Start()
		{

		}

		// Update is called once per frame
		void Update()
		{
			Apply2DPhysicsMovement();
		}
		
		private void Apply2DPhysicsMovement()
        {
            var xMove = Input.GetAxisRaw("Horizontal");
            var yMove = Input.GetAxisRaw("Vertical");
            var moveVector = new Vector2(xMove * _speed, yMove * _speed);
            _rigidbody.velocity = movement;
        }
	}
}

So we created a new method to handle our movement, and we call it in Update()
This method retrieves the xMove (vector) and yMove (vector) so remember
these values are horizontal either -1 for left, 0 for no movement or 1 for right
this is the same for vertical up/down
So we add these to with speed for example -1 * 4 = -4 means we are going in a velocity vector of -4x y0 so we apply this velocity to our rigidbody, and it will push us in this direction based on the movement speed.

You should now be effectively able to move your character around the map, and collide with things such as trees and deep water tiles.

Tilemap interaction

Next we want to be able to manipulate the tilemap based on maybe an action that our player does, so in our example when a player moves over a tile I would like it to change to a snow tile. So in this case we are talking specifically about the ground tiles and not the object tiles.

First thing to do, is get a reference to our ground tilemap, this is easily done since we already have access to the TileGrid object in our script. We can call _grid.GetTilemap(TilemapType.Ground);

using UnityEngine;

namespace Assets.Player 
{
	public class PlayerController : MonoBehaviour
	{
	    [SerializeField]
        private TileGrid _grid;
		[SerializeField]
        private Rigidbody2D _rigidbody;
		[SerializeField]
        private float _movementSpeed;
		
		private TilemapStructure _groundMap;
		
		// Start is called before the first frame update
        private void Start()
        {
            _groundMap = _grid.GetTilemap(TilemapType.Ground);
        }

		// Update is called once per frame
		void Update()
		{
			Apply2DPhysicsMovement();
		}
		
		private void Apply2DPhysicsMovement()
        {
            var xMove = Input.GetAxisRaw("Horizontal");
            var yMove = Input.GetAxisRaw("Vertical");
            _rigidbody.velocity = new Vector2(xMove * _speed, yMove * _speed);
        }
	}
}

We do this in Start method, as its only called once, and we want to keep it cached in a global variable so we can keep accessing it during the lifespan of the player.

Now that we have access to the TilemapStructure of the ground map, we can use the methods available such as SetTile(x,y) lets do that! But to know which tile to set, we need to find the player’s position on the tilemap.

We can do this simply by grabbing the gameobject’s transform position
but since this is a Vector2 which uses floats as type, we must convert these positions to integers to fit our tilemap coordinate system. Its very simple, we just floor the floats to ints! like so:

var xPos = Mathf.FloorToInt(transform.position.x);
var yPos = Mathf.FloorToInt(transform.position.y);

We use Floor instead of Round because if we are standing in the topright corner of the tile, it will round already to the next tile, while we are still technically standing on this tile. So we always want the Floor coordinate.

Now the simple part, we call SetTile with these coordinates, and we provide the tiletype we want to change to, and since we are only updating one tile at a time, we might aswel update the graphic aswell by supplying true as parameter to updateTile like so:

_groundMap.SetTile(xPos, yPos, (int)GroundTileType.Snow, true);

And it will look something like this, (note I put it in a seperate method to keep the code readable:

using UnityEngine;

namespace Assets.Player 
{
	public class PlayerController : MonoBehaviour
	{
	    [SerializeField]
        private TileGrid _grid;
		[SerializeField]
        private Rigidbody2D _rigidbody;
		[SerializeField]
        private float _movementSpeed;
		
		private TilemapStructure _groundMap;
		
		// Start is called before the first frame update
        private void Start()
        {
            _groundMap = _grid.GetTilemap(TilemapType.Ground);
        }

		// Update is called once per frame
		void Update()
		{
			Apply2DPhysicsMovement();
			DropSnow();
		}
		
		private void Apply2DPhysicsMovement()
        {
            var xMove = Input.GetAxisRaw("Horizontal");
            var yMove = Input.GetAxisRaw("Vertical");
            _rigidbody.velocity = new Vector2(xMove * _speed, yMove * _speed);
        }
		
		private void DropSnow()
        {
            // Grab the player position, floor the float to an int
            // We should floor, because rounding means that if we stand in the upper right corner of the tile
            // It will already give us the coordinates of the next tile which is incorrect since we're still standing on the previous tile
            var xPos = Mathf.FloorToInt(transform.position.x);
            var yPos = Mathf.FloorToInt(transform.position.y);

            // Set the tile to snow, there is a InBounds check in this method, and it won't trigger map updates if the type doesn't change
            _groundMap.SetTile(xPos, yPos, (int)GroundTileType.Snow, true);
        }
	}
}

If you now walk around with the player, you will notice that the tiles underneath the player are changing to snow!

Hope you liked this tutorial, and that it made it clear how to manipulate the tilemap during gameplay.

The sourcecode for this tutorial can be found here:
https://github.com/Venom0us/Code2DTutorials/releases/tag/v10

Leave a comment

Design a site like this with WordPress.com
Get started