RogueLike C# in Unity (2/13): The generic Entity, the render functions, and the map

Part 2 is here. This week we’ll create a generic Entity from where we’ll create all our entities, we’ll skip the original render functions from the original Python version, as won’t be used here, and finally we’ll create a basic map where we’ll be moving around.

Don’t forget to check out the previous parts of the tutorial:

The RogueLike camera

Before anything, one of the little differences with the Python version will be the camera: At the moment when we try to move the player outside of the visible screen the camera doesn’t follow, this is expected as is fixed. We’ll fix this by making the main game camera a child of the player transform. This way when the player moves, the camera will move as well.

The main problem with this is that the main camera is a game object that will be instantiated before the player Entity, this means that we cannot assign the camera to the player when the game starts, as the player instance will not exist at that point.

For fixing this we’ll create a new script and attach it to the main camera:

public class CameraController : MonoBehaviour
{
    public Transform target; // The Player Transform
    private bool isPlayerFound;

    void Start()
    {
        isPlayerFound = false; // Will be false by default as the Camera is already in the Inspector before the Player Entity is instantiated.
    }

    private void Update()
    {
        if (target != null)
        {
            return; // If the Player has been found means that its Transform != null, so we can get out of the statement
        } 
        else if (target == null && isPlayerFound == false)
        {
            target = GameObject.FindWithTag("Player").transform; // Find the Player
            gameObject.transform.SetParent(target); // Assign the camera game object as a child of the Player Transform
            isPlayerFound = true; // Switch the bool so this is not triggered again
        }
    }
}

You’ll find this snippet in my GitHub as well.

The Map: First attempt via Unity Prefabs.

This part of the code was entirely deprecated in favor of TileMaps (second attempt), but I’ve added this first attempt via prefabs as well for those who are interested. If you’re not interested you can jump directly to “The Map: Second attempt via Unity TileMaps”

The first solution that came to mind was to set a game object for each tile, and construct a map with these. For this you do something like:

public Transform _floorHolder;

public void FloorSetup(int width, int height)
    {
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                GameObject _floorTile = Instantiate(floorTile, new Vector3(x, y, 0), Quaternion.identity);
                _floorTile.transform.SetParent(_floorHolder);
            }
        }
    }

There’s a few things happening here:

  • First you create a variable named _floorHolder that will be used as a parent for all floor tile game objects. You’ll want to do something like this to organize your hierarchy, as a simple 80×80 tile map will generate 6400 objects in your Inspector ( only for floor tiles!).
  • Then we’ll loop through X-width and Y-high in order to generate the declare the necessary vectors to instantiate our game objects.
  • Then we’ll instantiate and assign our tile game objects.
  • And finally, in the last line of code at _floorTile.transform.SetParent(_floorHolder); we’ll add these freshly created tile game objects as children of _floorHolder, so these won’t clutter the workspace.

The same can be done for Walls, but in this case we cannot just loop through X-width and Y-height as we only want to fill up the map borders:

    public void WallsSetup(int width, int height) {

        GameObject _test_wallObject = Resources.Load<GameObject>("Prefabs/Wall1");

        for (int y = width; y >= -1 ; y--)
        {
            GameObject _wallTile = Instantiate(_test_wallObject, new Vector3(width, y, 0), Quaternion.identity);
            _wallTile.transform.SetParent(_wallHolder);
        }
        for (int y = width; y >= -1; y--)
        {
            GameObject _wallTile = Instantiate(_test_wallObject, new Vector3(-1, y, 0), Quaternion.identity);
            _wallTile.transform.SetParent(_wallHolder);
        }
        for (int x = -1; x < height; x++)
        {
            GameObject _wallTile = Instantiate(_test_wallObject, new Vector3(x, -1, 0), Quaternion.identity);
            _wallTile.transform.SetParent(_wallHolder);
        }
        for (int x = -1; x < height; x++)
        {
            GameObject _wallTile = Instantiate(_test_wallObject, new Vector3(x, height, 0), Quaternion.identity);
            _wallTile.transform.SetParent(_wallHolder);
        }
    }

Once this is done we’ll have a new map, where each tile (floor, or wall) is a different game object. This comes with problems as well, as we’ll be using physic objects with physic bodies and colliders, new issues will be introduced:

We’ll fix this behaviour by switching our walls act as static rigid bodies and a bit of code:

private Vector3 _lastKnownPlayerPosition; // We use this to track players last position before their next move

public Void MovePlayer(string direction){
    _lastKnownPlayerPosition = player.transform.localPosition; 
}    

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.tag == "Wall")
    {
    player.transform.position = new Vector3(_lastKnownPlayerPosition.x, _lastKnownPlayerPosition.y, 0);
    }
}

This part checks if the player has touched a wall, if this happens then “teleports” the player to its last known position. The reason for this is because when the player collides with the wall, physics come into play and the player vector will end un-aligned (no longer in full-integer vectors, but in floating points) breaking the whole game logic afterwards as movement relies on integer vectors. Most likely this also means that enemies or other entities that have a rigid body will have to go through the same bit of code, but that’s a problem for the future at this point.

The Map: Second attempt via Unity TileMaps.

“Pre-optimization is the root of all evil”. Yes, I agree, and in my short experience sometimes I know I’ve lost too much time optimizing something that didn’t need any optimization to start with, but this is not the case here. This implementation is not only way more efficient than using prefabs, but also seems to be the best tool for the work:

First: How do we know that to use TileMaps is more performant than Prefabs? In this case I used Stopwatch() and Unity.Diagnostics to get some data about how much time did each method need to complete the map creation. Checking how long it takes to create a simple 106x30tiles grid via Prefabs vs via Tilemaps outputted 200ms vs 14 ms respectively.

Just generating the floor map is already 15 times faster by using TileMaps, and while 200ms or 14ms is not that much for the human eye, the difference will definitely make an impact as we move forward and more ingredients are added to the stew. For example adding the walls to the map just added 2 additional ms, while Prefabs was almost almost doubling the initial calculation and reaching 400ms, that’s already 25 times faster once walls come into play.

16ms loading time for TileMaps vs 400ms via Prefabs when creating our map

Running Unity.diagnostics

This is the bit of code I ran to check how long the methods will need from start to finish:

using System.Diagnostics; // enables System.Diagnostics.StopWatch
using System; // enables System.TimeSPan

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        FloorSetup(106, 30); // Method we want to track
        WallsSetup(106, 30); // Method we want to track
        stopwatch.Stop();
        TimeSpan ts = stopwatch.Elapsed;
        int _ms = ts.Milliseconds; // <- Set a breakpoint here + run the debugger.

Remember to set a breakpoint in Visual Studio (or your IDE) at int _ms , so you’ll be able to check the data.

After checking the docs and a few random tutorials on YouTube, I got some general ideas of how TileMaps work:

  • You can create directly in your hierarchy a 2D TileMap game object, this will generate a Grid object automatically with some components (Transform, Grid) that will act as something like a TileMap manager. Seems to work a bit like the UI canvas elements, where your canvas UI elements are children of Canvas, and if you try to create directly an UI element in your hierarchy, the canvas parent object is created by default. The same applies here: Your TileMap elements will be children of this Grid object.
  • This Grid object has a default child object called TileMap, consider this a type of a layer, where you can stack TileMaps on top of each other. Each TileMap has a bunch of components (Transform, TileMap, TileMap Renderer), but and more can be added like for example a TileMap collisions 2D component.
  • In Unity > Window > Tile Palette, is where we find all the Tile painting tools we’ll use to work with tiles. When we “create a palette” where basically creating a collection of tiles where we can choose from at any given time. Each palette is stored as an object in your project
Under palettes, you’ll find your tiles as tilename.asset
The end result is the same with a massive performance improvement. Here we’re using 2 TileMaps, one for the floorTiles and one for the wallTiles

Creating our Floor via TileMaps

At this point we only have to adapt the previous code we have written for Prefabs, and use TileMaps instead:

public class GridGenerator : MonoBehaviour
{
	
    public Tilemap floorMap;
    public Tile floorTile; 

	public void Start()
    {
    	FloorSetup(width, height);
    }


    public void FloorSetup(int width, int height)
    {

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
     
                floorMap.SetTile(new Vector3Int(x, y, 0), floorTile);

            }
        }
    }
}

Creating our Walls via TileMaps

The same concept than our previous Prefab example will apply here, we’ll only fill up the borders of the map:

public class GridGenerator : MonoBehaviour
{
	
    public Tilemap wallMap;
    public Tile wallTile; 

	public void Start()
    {
    	WallSetup(width, height);
    }


    public void WallSetup(int width, int height)
    {

        for (int y = width; y >= -1; y--)
        {
            wallMap.SetTile(new Vector3Int(width, y, 0), wallTile);
        }
        for (int y = width; y >= -1; y--)
        {
            wallMap.SetTile(new Vector3Int(-1, y, 0), wallTile);
        }
        for (int x = -1; x < height; x++)
        {
            wallMap.SetTile(new Vector3Int(x, -1, 0), wallTile);
        }
        for (int x = -1; x < height; x++)
        {
            wallMap.SetTile(new Vector3Int(x, height, 0), wallTile);
        }
    }
}

Tilemap collisions

We’ll see that the initial problem we had with Prefabs, where our player would ignore physical bodies, will apply here again. With Prefabs we solved this by adding a collision body to the walls, and with TileMaps will be no different, but in this case we need to use a different collider and put the walls in a different TileMap as well, or our colliders would affect the floor tiles too, which we don’t want:

Switched the collider type from sprite to grid, otherwise only the drawn sprite will act as a collider, not the whole tile
We can see now how the colliders are attached to the wall tiles, I enabled isTrigger so we can use the same implementation we’ve already written before. Remember to change the Tag to “Wall”

And is working again, with the difference that now is much more faster and performant:

Working Tilemap collisions

You can see in the previous GIF that there’s a little oddity: The player (or any Entity) is spawned not in the center of the tiles, but in the junction of these. As our TileMap is generated from (0,0,0) that will be the left bottom corner of our first tile, when we generate entities in non-float positions we’ll see that these are aligned perfectly with the tiles, which is something we do not want in this scenario.

To resolve this final issue we’ll create a new function within Entity.cs that “reallocates” our entities to fit nicely in our map:

        Vector3 reallocateEntity(int ax, int ay)
        {

            return new Vector3(ax + 0.5f, ay + 0.5f, 0);

        }
        entityLocation = reallocateEntity(aX, aY);

This way our entities will show in the center of our tiles.

And that’s all for this week! Do not miss the next week tutorial, where we’ll be changing our Map functions to create levels procedurally, instead of playing the current square-shaped level:

Join 241 other subscribers

Comments

6 responses to “RogueLike C# in Unity (2/13): The generic Entity, the render functions, and the map”

  1. Sartoris Avatar
    Sartoris

    Great, can’t wait 🙂 One other thing I’ve been wondering about (and I know this is outside of the scope of the original roguelike tutorial) is how to go about eventually adding an overworld, a large map that the player traverses and then descends into dungeons he comes across. If possible, I’d love to see your implementation of something in this vein, perhaps after you finish with the main parts of the tutorial.

    1. Gabriel Maldonado Avatar
      Gabriel Maldonado

      That actually would be great. At the moment I can’t say as I’m going through the tutorial for the first time, but is definitely something to add to the backlog and develop in the future. This should be easy to implement if we play with Unity scenes, for example instantiating the game in a scene that represents the world (world-scene), and then each time we go into one dungeon we’ll switch to a dungeon-scene. We also only would need one of each, as the GridGenerator will create all the dungeons. Then the same GridGenerator could check if we’re in one scene or the other, and generate the map accordingly with different tiles. Ultimately we also want to play with map seeding so both the general world as well as the instances of dungeons created each time we go into a different one are saved somewhere and we get the same one each time we visit them, instead of freshly generated maps (this map seeding is implemented in the tutorial 3, which is not in the original Roguelike one).

  2. Sartoris Avatar
    Sartoris

    Thank you for the two episodes! When can we expect the next one?

    1. Gabriel Maldonado Avatar
      Gabriel Maldonado

      Thanks! Will be up soon (in the next day or two), I made this mistake of writing it offline while in a plane and I closed the tab before landing and saving the changes, 100% facepalm 😀

      1. Sartoris Avatar
        Sartoris

        Oof, that’s a shame… Thanks for the reply, I’m looking forward to the next episode. There is definitely a lack of tutorials for roguelikes in Unity, especially ones that use the new tile maps system.

      2. Gabriel Maldonado Avatar
        Gabriel Maldonado

        Yeah, doing the FOV part with tile maps was definitely challenging as well, but ultimately came up nicely and works, post coming soon as well 😀

Leave a Reply

%d