Sharing means caring: Things I wish I knew before starting with Tilemaps & 2D (2,5D)

Hey everyone,

due to the fact that I was sitting here more googling for some knowledge too many hours the last weeks, instead of being able to have a relativly smooth workflow (+ some additional/minor googling for sure), I intend to provide a major head-start for all beginners (and some stuff for intermediate/advanced) with this thread.

I summaries here all the stuff that I was able to find spreaded all over the interwebs, was it even UnityForum, Stackoverflow, Stackexchange, Reddit or Blogs and in addition those things I found out my self trying and learning the hard way.

So please find my guidance through the world of Tilemaps and things I wish I knew before (or at least finding them faster).

Please feel free to ask any questions, maybe I can fill that gap too or I will find it out sooner or later, extending this thread over time.
And very important: My methods / knowledge is not exclusivly perfect, so for sure I would appreciate to tell/teach me if somebody sees something awkward or broken, so we can exchange and provide a good guide to everbody, because I’m absolutly no Unity Expert so far :slight_smile:

As told there will be some basics, some intermediate things, heading to advanced stuff.

*But a note in advance *But a note in advance

I really won’t point out the very very basic stuff, e.g. “how to paint on a Tilemap”, because there are sooo many Tilemap starter guides, in the docs or even on YT.
To get started with all the necessary basic I can recommend this tutorial sequence under:
2D World Building w/ Tilemap & Cinemachine - Unity Learn
I’m heading for all the stuff that was super super spreaded through the web and less or none existing and I found out myself.

'nough said! Let’s begin ! :smile:

(EDIT Important note: unfortunately Unity Forum only allows 20 images maximum since a while, so every image-spoiler after the 20th will contain a link to the image of interest, I am sorry for that :frowning: )

Overview:

Beginner:

  1. How to rotate a Tile while painting ?
  2. How to redraw an existing Tile seed from your Tilemap with the Random Brush ?
  3. How to get rid of physics shape on a single Tile of a whole spritesheet, while the others do have auto-generated physics shape or custom physics shape - the easy way ?
  4. How to get absolute Tile coordinates, clicking in the Scene View, while working in Editor Mode ?
  5. a) How to rotate an already painted Tile On Runtime ?
  6. b) How to move/copy a rotated Tile including the rotation On Runtime ?

Intermediate:
6. How to simulate player & terrain depth for a player / NPC that fits not perfectly to a “good” amount of grid units (height and width) ?
7. How to move a Player and NPC (randomly) around and prevent them from pushing each other away ?
8. How to get multiple prefabs into a single Tilepalette and draw with default brush as you would draw any other tile?

Expert:
9. How to swap sprites for an animated object in the Animator / Animator Controller / Animation Clip, including blend trees - ON RUNTIME ?


1. How to rotate a Tile while painting ?

Per default on Windows & German keyboard settings, it’s the “ß” Button.
This also work while having Tiles in a multi-seleciton.

Tile rotation GIF

(for later reupload: rotate_tile_painting.gif)

2. How to redraw an existing Tile seed from your Tilemap with the Random Brush ?

I already find the built-in Random Brush quite cool to use, but what I found even cooler is the following.

  • Select the Random Brush under the Tilepalette
  • Tick “Pick Random Tiles”
  • Go to your Tilemap with some Tiles in an area you like the seed of the tiles (e.g. ground floor / enviromental tiles)
  • Hold down CTRL and select the area
  • Untick “Pick Random Tiles”
  • Now you are able to
    a.) draw random Tiles from the seed of your previous selected area
    b.) draw in a large rectangle random tiles
    c.) size down to smaller rectangle size and still use the pre-selected Tile seed
    A GIF showing the way


(for later reupload: redraw_random_tile_seed.gif)

3. How to get rid of physics shape on a single Tile of a whole spritesheet, while the others do have auto-generated physics shape or custom physics shape - the easy way ?

Basic knowledge about Tile physics shape

I won’t point out physics shapes and their customization anyhow in this thread, because the documentation from Unity docs does it very well.
You will find the basics here everything here:
Unity - Manual: Sprite Editor: Custom Physics Shape
As explained in the docus and videos from above, just extend your Tilemap with a “Tilemap Collider 2D” and if it fits your needs an additional “Composite Collider 2D”.

So far I was “forced” to double all my Tilemaps, as soon as single Tile should not have a collision, while most of the others or at least one Tile has a collision, because Unity auto-generate a physics shape based on the sprites shape of a tile.

Where it is a cool and strong feature for a smooth workflow, you don’t always want a generated physics shape on your Tile.

Unticking “Generate Physics Shape” on the imported sprite does literally nothing.
You may tick or untick, the behaviour will be the same.
But why ?
It’s as easy as stupid (stupid in relation to the fact how impossible to troublshoot / trick around this on your own if you don’t know better).

Texture Type Sprite (2D and UI)


(for later reupload: sprite_import.png)

While most of us (I assume) import a 2D Sprite usually as “Sprite (2D and UI)”, Unity then uses this information to classify the Collider Type of each generated Tile you receive when you drag & drop the imported Sprite into a Tilepalette (the pop-up asking you where to store all generated Tiles for the Tilepalette).

So if you go into your project hierachy into those folders and search for the little purple Tile assets and select one, the you will see, that every single Tile has its own Collider Type setting.
Collider Type setting


(for later reupload: tile_asset.png)

When importing the spritesheet into the Tilepalette Unity will set all Tileassets with Collider Type “Sprite” per default. This is why you can’t remove the physics shape from a single tile in the Sprite Editor.

If you choose Collider Type “None” you will be able to paint this specific Tile without a physics shape from the same Tilepalette & imported spritesheet.
If you need to have the same Tile sometime with physics shape and sometimes without I would recommend to extend your spritesheet by this Tile another time and then have both Tile assets, one with and one without Collider Type “Sprite”.

4. How to get absolute Tile coordinates, clicking in the Scene View, while working in Editor Mode ?

While the docs and the internet are keeper of this information somewhere, it is not very good outlined, as if you might not know how to find (because most posts are about “On Runtime”,not “In Editor Mode”) or even if it’s there, it might not look like the answer for you, because it does not work after Copy&Pasting.
Most of the stuff I have found just did not work properly.

In the spoiler you will find a final script, that can be really copy&pasted into a new script and attached to any GameObject where you find it good to be.
It doesnt really matter where it is attached, as long as a Grid or a Tilemap is referenced in the Inspector.

Another GIF showing the truth


(for later reupload: tile_position_scene_view.gif)

Be aware, that if you choose to have the referenced Tilemap not at position 0/0/0, that the results might not be as expected if you forget about it at some point.
For easier handling in general I would recommend to have both Grid and Tilemap at position 0/0/0, but its not perfectly necessary.

Tile coordinates in a Tilemap script

using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;

[ExecuteInEditMode]
public class GetTilemapCoordinate : MonoBehaviour
{
    public Grid grid;
    public Tilemap map;
    public bool Active = false; //this is just a toggle, to be able to disable the script when not needed

    public void ToggleActive()
    {
        if(Active)
            SceneView.duringSceneGui += GetMousePosition;
        else
            SceneView.duringSceneGui -= GetMousePosition;

    }

    public void OnValidate()
    {
        ToggleActive();
    }

    public void GetMousePosition(SceneView scene)
    {
        Event e = Event.current;
        if (e != null)
        {
            if (Event.current.type == EventType.MouseDown)
            {
                Vector3Int position = Vector3Int.FloorToInt(HandleUtility.GUIPointToWorldRay(Event.current.mousePosition).origin);
                Vector3Int gridCellPos = grid != null ? grid.WorldToCell(position) : Vector3Int.zero;
                Vector3Int mapCellPos = map != null ? map.WorldToCell(position) : Vector3Int.zero;

                Debug.Log("Clicked Tile position in Grid: "+ gridCellPos);
                Debug.Log("Clicked Tile position in Tilemap: "+ gridCellPos);
            }
        }
    }
}

5. a) How to rotate an already painted Tile On Runtime ?

Once found out how its quite a one-liner, but to find out how it brought me to the middle of nowhere sometimes :smile:

//assuming you have a Tilemap referenced in the var tileMap
Vector3Int pos = new Vector3Int(1, 2, 0);
float rotation = 90f; //degrees presented in a float
tileMap.SetTransformMatrix(pos, Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f,0f, rotation), Vector3.one));

5. b) How to move/copy a rotated Tile including the rotation On Runtime ?

If you SetTile on a Tilemap with return value of GetTile, you might think the rotation will be adopted to the newly generated Tile.
But it’s not how it works.
Unity generates a new Tile based on the attached sprite of the received Tiledata, not caring for any transformation/rotation.

So here it got a little bit tricky in advance, but then again its a one-liner^_^

//assuming you have Tilemaps referenced in the vars tileMapSource & tileMapTarget
//for sure they can be the same or just use one referenced Tilemap and var
Vector3Int tileSourcePos = new Vector3Int(1, 2, 0);
Vector3Int tileTargetPos = new Vector3Int(3, 5, 0);     
tileMapTarget.SetTile(tileTargetPos , tileMapSource.GetTile(tileSourcePos ));
tileMapTarget.SetTransformMatrix(tileTargetPos , Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f,0f, tileMapSource.GetTransformMatrix(tileSourcePos ).rotation.eulerAngles.z), Vector3.one));

6. How to simulate player & terrain depth for a player / NPC that fits not perfectly to a “good” amount of grid units (height and width) ?

At this point you might ask yourself yourself, “sorry what was the question?”.
This is where we leave beginners tricks and move on to intermediate (I would say at least ;)).

So everybody heading for some jump’n’run platformer or other plane frontal 2D game, you can leave now or take a break :stuck_out_tongue:
In this specific case we talk about 2,5D simulated depth.

Imagine you have a top-down scenario.
Have you every struggled with the issue that your player(-sprite) - moving around - does not hide behind trees or roofs or roof-planks intuitive and smoothly?
Maybe you have struggled with the problem, that you were not able to make your player move in front of a pillar and behind that pillar smoothly, gaining a very cool depth experience and you were always forced to trade off.

Usually these trade-offs are either shrinking sprites accordingly, so that you can workaround with tilemap layers/orders, draw-order bottom-to-top in the project setting and dont position tiles “wrong”.

This doesnt sound very satisfying, so did it not for me.
My player sprite is what it is: ~1,8 units tall
And I want the players head to hide behind a treetops or roof planks as it make sense and not the other way around, forcing sense through sprite adjustment.

But just let’s have a look on an example, before this text grows to a roman.
GIF with magical depth GIF with magical depth

(for later reupload: terrain_and_player_depth.gif)

What do we see there that’s supposed to be cool ?
If you have a near look onto the scene view (upper area) you will see the grid and the moments when the players sprite exactly reaches a line / the next tile. At the same time you see when the players head is still on a single tile, but the e.g. the plank (on the right side) moves in front of the players sprite for some magical reason.

This can’t be reached with any of the built-in functionalities, is it even drawing order, tilemap layers / oders or anything else.
You will never achieve it without doing heavy workaround, shifiting Tiles all around away from their snapped and common positions, trying to trick around it with the same sprite multiple times or whatever you already tried.
Trust me!:wink:
I can just draw these once and leave them snapped into their default position in the grid like every other tile.

But how do I do it ?

honor the pioneers

This is the moment where I want to name and value creativitRy , he built the fundament of this to work, I just played around with it and brought some more flexibility into it after some adjustment.
Link of the git repository:
TilemapHeightTest/Assets/TilemapHeightTest/Scripts/TileHeightManager.cs at master · creativitRy/TilemapHeightTest · GitHub

Pre-Requisites:
Tilemap Orientation is: XY
Transparency Sort Mode: Custom Axis
Transparency Sort Axis: X:0 | Y:1 | Z:0

I attached the adopted Scripts in the TileHeightManager.rar , so feel free to use and adopt them.

You will get a new Asset to create, so called TileHeightGroup
TileHeightGroup Create


(for later reupload: tileHeightGroupCreate.png)

A valid filling for this scriptable object (and as I use it for the preview GIF above) can be like that:
TileHeightGroup in use


(for later reupload: tileHeightGroupFilled.png)

But what do I fill there?

Basically its an array, so far so good, each array element contains 1 Sprite and 1 float value.
The sprite MUST be the reference to the sprite that you took from your imported spritesheet and with that you are exactly painting through the Tilepalette.

referenced tile sprites of the spritesheet according to the array above


(for later reupload: tileHeightGroupPillars.png)

You are free on how you configure the array, so you don’t need to pick all sprites.

To be more precise: You actually pick only the tiles for that you intend to have special depth and configuration for.

According to my preview GIF, the top-corner pillar (holding the roof) is here “house_pillars_0” and the vertical downwards hanging plank is here “house_pillars_4” (which is just rotated and usually horizontally aligned) .

“What does the float value do now ?”
To not go to deep into coding, let me say it like that
“The higher the float value, the earlier the selected sprite will move to front when moving something towards it”.
Per default and for none configured sprites this value can be measured as zero float (0f).
So e.g. -1f would make the sprite get much later moved to the front of the players sprite.

“How to use the TileHeightManager Script now?”
Add a component of that script to any kind of object, e.g. the grid.
It might look like that (I have it configured like that at the moment, to make it work like on the preview GIF)
TileHeightManager script configuration


(for later reupload: tileHeightManagerConfiguration.png)

Add the pre-configured TileHeightGroups (see picture of “RoofPlanks” and one more above) to the Tile Height Groups list.

Add the tilemaps to the Tilemaps list, which have painted tiles that should be considered & are relevant for the effect. You don’t need to add each and every tilemap.
This makes it even possible to use the identical tile on different tilemaps, so that the effect will not be forced on sprite base.

Overlay is just an completely empty Tilemap which has a higher order in layer compared to the players sprite layer (in my case, you can adopt it to fit your needs)

Whats left now is the relation between the moving object and the configured stuff until now.
This will be achieved by the following:
(add this to your Update() methods within the objects that should be taken into account for the depth evaluation, e.g. Player / NPC)

//assuming you have your player referenced in playerObject and the SpriteRenderer of the player referenced in spriteRenderer
TileHeightManager.Instance.ReportPosition(playerObject.transform, spriteRenderer.sprite.bounds); //according to the last update, now you report the transform and not the position

UPDATE 12th Nov. 2019:

I updated the TileHeightManager to make it much more reliable and performant.
Also I extended the features, that you may have multiple objects, that are moving around and just report to the TileHeightManager instance, then once per frame it will evaluate which Tiles should move to front for all “reporting” objects (e.g. Player and several NPC walking around).

There was an issue, having 2 “reporting” objects next to each other, so the clearing of the Overlay-Tilemap and moving-to-front into the Overlay-Tilemap has made the TileHeightManager struggling.

It’s a minor change to update your code after download.
You just swap the TileHeightManager.cs from the download with your existing (if) and change the lines “TileHeightManager.Instance.ReportPosition” of your reporting objects.
Now you report the transform in the first parameter and not the position to the TileHeightManager, of a certain object.

7. How to move a Player and NPC (randomly) around and prevent them from pushing each other away ?

First a quick preview :slight_smile:

Player and NPC moving when not colliding


(for later reupload: player_npc_walking.gif)

Here find an example PlayerController and NPCController.
I removed animator stuff and everything thats not perfectly relevant to the basic question.
So don’t wonder if you can’t just copy&paste to have the identical behaviour (it terms of animation) as from my preview GIF above.

Both the Player object and NPC object Rigidbodies can have Body Type Dynamic and both have a CircleCollider2D attached.

PlayerController

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("General:")]
    [Space]
    public bool npcTackled = false;
    public Collision2D tackledNPC;


    [Header("Movement Settings:")]
    [Space]
    public float movementBaseSpeed = 1.0f;
    public Vector2 movementDirection = Vector2.zero;
    public float movementSpeed = 0.0f;
    public bool canMove = true;

    [Header("References:")]
    [Space]
    public Rigidbody2D playerRB;


    void Update()
    {
        if (canMove)
        {
            ProcessMovementInputs();
            Move();   
        }
    }
    #region Movement Handling
    void ProcessMovementInputs()
    {
        //reset that we are moving
        movementSpeed = 0.0f;

        //get the absolut inpuit from arrow keys to decide in which direction to move the player
        movementDirection.x = Input.GetAxisRaw("Horizontal");
        movementDirection.y = Input.GetAxisRaw("Vertical");

        //if the movement direction is not equal to the zero vector we will define the movmentspeed and declare that the player is actually moving
        if (movementDirection != Vector2.zero)
        {
            //clamp the movementdirections magnitude between 0 and 1, so nobody cheat with special input devices (xbox controllers), and assign it as the movementspeed
            movementSpeed = Mathf.Clamp(movementDirection.magnitude, 0.0f, 1.0f);
            //normalize the movement direction, so we are not unrealisticly moving double as fast when using diagonal movement direction
            movementDirection.Normalize();
        }
    }

    void Move()
    {
        //only move the palyer into the direction when he currently not in contact with an NPC
        if (!npcTackled)
        {
            playerRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
        }
        else
        {
            //get the relative position of the NPC to the player
            Vector2 positionRelative = transform.InverseTransformPoint(tackledNPC.transform.position);
            //if we are stucking at the NPC we need to trick around, so we can leave the NPC's colliding shape again
            //we do this by checking movementDirection (where the player would go to) and get the distance between the NPC's relative position and the movementDirection
            float moveRelative = Vector2.Distance(positionRelative, movementDirection);
            //as if the player is moving away from the NPC the moveRelative will get > 1, so we can assign the normal movement flow
            //if the player would go into the NPC with his movementDirection again, then the moveRelative would be < 1, so we assign vector2.zero velocity to his RB
            if (moveRelative > 1.0f)
            {
                playerRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
            }
            else
                playerRB.velocity = Vector2.zero;
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        //only care for collision with NPC
        //other collisions will be treated by the collider components (static structures, that cant get pushed)
        if (collision.transform.tag == "NPC")
        {
            npcTackled = true;
            //save the currently tackled NPC for later uses, e.g. relative position and talking with the NPC
            tackledNPC = collision;
        }
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
        if (npcTackled)
        {
            npcTackled = false;
            tackledNPC = null;
        }
    }
    #endregion

}

NPCController

using System;
using UnityEngine;

public class NPCController : MonoBehaviour
{
    [Header("Movement Settings:")]
    [Space]
    public bool freeMoving = true;
    public float movementFrequenceThreshold = 1.0f;
    public float movementFrequence = 0.1f;
    public float movementBaseSpeed = 1.0f;
    public float movementDuration = 1.0f;
    public Vector2 movementDirection = new Vector2(0.0f, 0.0f);

    public float movementSpeed;
    public float movementFrequenceCounter = 0.0f;
    public float movementDurationCounter = 0.0f;
    public bool shouldMove = false;
    public bool tackled = false;

    [Header("References:")]
    [Space]
    public Rigidbody2D npcRB;

    void Update()
    {
        if (!tackled)
        {
            if (freeMoving)
            {
                ProcessAutoMovement();
                Move();
            }
            else
                movementSpeed = 0.0f;
        }
        else
        {
            movementSpeed = 0.0f;
            movementDirection = Vector2.zero;
            npcRB.velocity = Vector2.zero;
        }
    }

    void ProcessAutoMovement()
    {
        if (movementFrequenceCounter > movementFrequenceThreshold)
        {
            movementFrequenceCounter = 0.0f;
            shouldMove = true;

            for (int i = 0; i < 2; i++)
            {
                int randomizer = UnityEngine.Random.Range(0, 4);
                switch (randomizer)
                {
                    case 0:
                        movementDirection.x += 1.0f;
                        break;
                    case 1:
                        movementDirection.x -= 1.0f;
                        break;
                    case 2:
                        movementDirection.y += 1.0f;
                        break;
                    case 3:
                        movementDirection.y -= 1.0f;
                        break;
                    default:
                        movementDirection = Vector2.zero;
                        break;
                }
            }

            movementSpeed = Mathf.Clamp(movementDirection.magnitude, 0.0f, 1.0f);
            movementDirection.Normalize();
        }
        else
            movementFrequenceCounter += movementFrequence;
    }

    void Move()
    {
        if (shouldMove)
        {
            if (movementDurationCounter < movementDuration)
            {
                npcRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
                movementDurationCounter += Time.deltaTime;
            }
            else
            {
                movementDurationCounter = 0.0f;
                shouldMove = false;
                npcRB.velocity = Vector2.zero;
                movementSpeed = 0.0f;
            }
        }
    }



    void OnCollisionEnter2D(Collision2D collision)
    {
        tackled = true;
        if (collision.transform.tag == "Player")
        {
            Vector2 positionRelative = transform.InverseTransformPoint(collision.transform.position);
            movementDirection = positionRelative;
        }
    }

    private void OnCollisionStay2D(Collision2D collision)
    {
        if (!(collision.transform.tag == "Player"))
            tackled = false;
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
        tackled = false;
    }
}

(Note: now we are leaving intermediate and floating slowly to advanced topics. Means this will be no copy & paste => solve-issue thing )

8. How to get multiple prefabs into a single Tilepalette and draw with default brush as you would draw any other tiles?

So what is easy and most might do:

  • drawing prefbas with the “Prefab Brush”
  • drag & drop a prefab that has a SpriteRenderer & Grid component attached into the Tilepalette module, which will then generate a new Tilepalette with only this single prefab + using the “Prefab Brush” again

What I didn’t like there:

  1. I didn’t want to change always to prefab brush, when swapping between all the Tilepalettes when painting much, then it’s getting annoying
    2a. I didn’t want to have 1 Prefab brush for each and every individual type of sprite + prefab
    2b. if you attach multiple prefabs to one Prefab Brush, then it just randomly picks one prefab out of it, but I want to choose which one to draw
    2c. combine 2a & 2b, means if you have lets say 100 different items you would draw on a floor that have some sort of interaction (a prefab attached), this would mean you would have 100 different Prefab Brushes IF YOU (AS I DO) intend to draw exactly then something where it is supposed to be

TLDR:
I wanted a single tilepalette, let’s call it “Pickup Items” and want some tiles on it, as for example stick, stone, healthpotion, needles, sword, armor … etc. where I can sort all my items that have a prefab and if I want to draw on my tilemap then I wanted to have the same workflow as I do have with every other form of tiles →
just select it in the tilepalette and start drawing + add more tiles easily into the tilepalette if needed

TLDR GIFs

  1. totally common drag & drop of tiles into a common tilepalette

(for later reupload: prefabTile01.gif)

  1. completely normal drawing with the default Brush and the erase tool and everything as usual

    (for later reupload: prefabTile02.gif)

  2. enter Playmode shows all the magic, both painted objects , the Stone and Wood are somehwat interactable objects

    (for later reupload: prefabTile03.gif)

Keep reading to get some little know-how to do it

TLDR GIFs

:slight_smile:

But how to do this ?
Well Unity is tricky sometimes, but in this particular case it’s super sneaky at the same time.

  1. create a new tilepalette and give a name you like
  2. go to the tiles where you would like to have a prefabs attached
    note: if you intend to have the identical tile, one just to draw with and one with a prefab attached, while drawing, then you have to duplicate it, they do not behave the same and you cannot reuse one for the other, just duplicate them to avoid complications!
  3. so now that you have your “stick” and your “stone” and your “sword” tile sorted…
  4. create a prefab of your choice. it doesnt matter what sort of prefab, but for my case I keep it close to the example mentioned and created a “PickupTile” prefab, that currently does the following
    PickupTile prefab
public class PickupTile : MonoBehaviour
{
    public PickupTileType pickupTileType;
    private void Start()
    {
        //get the painted tile from the position where you've painted it in the scene view
        Sprite paintedTile = GetComponentInParent<Tilemap>().GetSprite(Vector3Int.FloorToInt(this.transform.position));
        //attach a spriterenderer to the prefab and assign the painted tile the the SR
        this.gameObject.AddComponent<SpriteRenderer>().sprite = paintedTile;
        //add a PolygonCollider2D, unity uses the physics shape of the imported sprite to automatically generate a polygon collider around it
        this.gameObject.AddComponent<PolygonCollider2D>();
        //save the information what type of PickupTile we have painted here to the prefab itself
        if (!Enum.TryParse<PickupTileType>(paintedTile.name, true, out pickupTileType))
            throw new Exception("Could not find \"" + paintedTile.name + "\" in the Enum PickupTileType, please check your Enum or Spritename!");
        //remove the spriterenderer again, because I don't need it, I will work with painted Tile on the tilemap, so my prefab does not need to display itself at all
        Destroy(this.GetComponent<SpriteRenderer>());
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        switch (pickupTileType)
        {
            case PickupTileType.Wood:
                Debug.Log("OMG finally I found some wood, time to make a campfire!");
                break;
            case PickupTileType.Stone:
                Debug.Log("That rock might hurt if it hits the head, mhhh");
                break;
        }
     
    }
    public enum PickupTileType
    {
        Wood,
        Stone       
    }
}

Another note:
It’s really just an example for the purpose to show that you may use the same prefab on different type of tiles to paint - in my example “wood” & “stone” and you could react on both with the same class & prefab, but still can just paint them as you want, fully flexible and dynamical.

Your are absolutly free to create a “wood”-prefab and a “stone”-prefab.
But - again - I just wanna do some explicit example how cool it could be :stuck_out_tongue:

So now we have our prefab, but how we get it attached?
5. ENTER THE MATRIX - just joking :smile: but… Enter Debug Mode !
How to enter Debug Mode


(for later reupload: debug-mode02.png)

  1. check on your tile , in my example “Stone”-tile and then you will see the following
    A tiles properties in Debug mode


(for later reupload: debug-mode04.png)

  1. you already see and will find the property “Instance Game Object”
  2. this will do all the magic for you, so drag & drop your prefab(s) to this property
  3. drag & drop the so created tiles into a tilepalette you wish and use the default brush as you do for every other thing
  4. that’s it, check out the spoiler “some Anti-TLDR GIFs” above. in combination with the presented code you will understand what’s happening.

9. How to swap sprites for an animated object in the Animator / Animator Controller / Animation Clip, including blend trees - ON RUNTIME ?

When I was on my way to create my first nearly duplicate NPC, just with another sprite for his animation / movement, I just thought this gonna be easy.
But then Unity said “no” :smile:

Basically it’s impossible to swap a sprite of an animation clip on runtime.

But the only thing that is possible LateUpdate() your object and swap the sprite in the sprite renderer according to some logic to pick the right sprite when swapping.

I don’t like this approach, because I know there will be many NPCs acting the same - in regards of their Animation Clip -, just with another sprite and then the CPU will consume time for every single NPC in LateUpdate() on each and every frame.
Which sounds not satisfying in long-term performance things.

Note: if you know yet already, that you won’t have too many objects that will have another sprite, but the same behaviour / animation, then you are OK using this approach.
For more information just google “unity lateupdate change animation sprite” or follow this article how to do it:

But again I don’t like this appraoch, as it just consumes CPU for “no reason” while it could do something more relevant.

For everyone else, keep reading :slight_smile:

Download the AnimatedSpriteSwapper.rar while you are following this article.

In advance some preview
Animator & Animation states

There is nothing special with the Animator Component.

(for later reupload: animator01.png)

Here the animation states of the root layer, with one state “NPC_Movement”

(for later reupload: animationclip01.png)

Now the “NPC_Movement” state with his blendtree.

(for later reupload: animationclip02.png)

First of all we need to setup some GraphicBundleContainer.cs

GraphicBundleContainer

Just attach it to some gameobject where you think it’s good for your.
It doesnt matter where it is, you just need a reference to it on hand for later purposes in the AnimatedSpriteSwapper.cs

It’s basically just a list of a generic class with 3 properties Name, SpriteSheet & SpriteList, filled in the inspector like below.

Where the Name property has no use, but substituting “Element 0” … with a real name for organizational aspects in the inspector.

https://www.bilder-upload.eu/upload/347551-1593347686.png

(for later reupload: graphicBundleContainer.png)

Now we get to the tricky part.

If the approach on top is not good enough, what else can we do ?
It’s as fiddly - for instance - as powerful.
We generate the animation clips and the animator controller on startup out of code, based on a once preconfigured animator controller.
some more crediting

Thanks to VirtuaBoza, he inspired me to this approach when I found his git.
GitHub - VirtuaBoza/SpriteSheetSwapping: Experimenting with sprite swapping in Unity

If you don’t have any BlendTrees and no multiple layers in your Animation clip, then his repo is already super near to a copy&paste solution.

What do we need to do so ?

First of all we wanna define all of our Animation types, so in which state an animated object will be

//setup all your AnimationType, if you are on 8 direction, then feel free to extend this
public enum AnimationType
    {
        Idle_Up,
        Idle_Right,
        Idle_Down,
        Idle_Left,
        Run_Up,
        Run_Right,
        Run_Down,
        Run_Left
    }

Then we should know our sprite dimensions of the sprites that we are swapping.
To keep the explanation at least a little easier, I won’t put some light onto sprites that have different dimension.

We define our sprite dimensions as the following, in this case 3 columns and 4 rows.

Here an example sprite to understand the configutation
exmaple sprite with 3 columns and 4 rows


(for later reupload: grayhairguy.png)

SpriteSheetAnimationInfo

setup all your SpriteSheetAnimationInfo for each and every AnimationType, if you are on 8 direction, then feel free to extend this
the numbers returned in SpriteSheetAnimationInfo should be understand as startindex (counting from 0) and the range of the sprite dimensions, but it depends on how you pre-laoded your GraphicBundleContainer SpriteList
imagine you have a character spritesheet with 3 columns and 4 rows, where each row represents one direction (up,right,down,left) and motion and and where the second sprite of a row is an idle sprite or a direction
but anyways your GraphicBundleContainer must be setup appropriately, or the SpriteList of each loaded sprite. I load it as from top-left corner down to bottom-right corner, per row.

then for example your configuration would exactly look like below

private SpriteSheetAnimationInfo GetSpriteStartIndexAndRange(AnimationType animationType)
{
    switch (animationType)
    {
        case AnimationType.Run_Up:
            return new SpriteSheetAnimationInfo(9, 3);
        case AnimationType.Run_Right:
            return new SpriteSheetAnimationInfo(6, 3);
        case AnimationType.Run_Down:
            return new SpriteSheetAnimationInfo(0, 3);
        case AnimationType.Run_Left:
            return new SpriteSheetAnimationInfo(3, 3);

        case AnimationType.Idle_Up:
            return new SpriteSheetAnimationInfo(10, 1);
        case AnimationType.Idle_Right:
            return new SpriteSheetAnimationInfo(7, 1);
        case AnimationType.Idle_Down:
            return new SpriteSheetAnimationInfo(1, 1);
        case AnimationType.Idle_Left:
            return new SpriteSheetAnimationInfo(4, 1);
        default:
            throw new InvalidEnumArgumentException();
    }
}


public struct SpriteSheetAnimationInfo
{
    public SpriteSheetAnimationInfo(int startIndex, int range)
    {
        StartIndex = startIndex;
        Range = range;
    }

    public int StartIndex
    {
        get; private set;
    }
    public int Range
    {
        get; private set;
    }
}

Now we need to generate all the Animation clips for all of the Animation types.

I won’t explain every step in the code, as I commented itself already and with some little bit of you googling/reading into the class AnimationClip you will understand it.
Generating the Animation Clips

public void CreateAnimationClips()
{
    //generate all the animation clips according to your number of animation types (e.g. Idle_Down & Run_Down ..)

    foreach (AnimationType animationType in AnimationType.GetValues(typeof(AnimationType)))
    {
        //give the animation clip a unique name
        var animClip = new AnimationClip { name = $"{spritesheetForAnimation.name} {animationType}" };

        //just some generous setting to the editorCurveBinding, "propertyName" is m_Sprite, because SpriteRenderer has it's sprite stored in m_Sprite

        var spriteBinding = new EditorCurveBinding
        {
            type = typeof(SpriteRenderer),
            path = string.Empty,
            propertyName = "m_Sprite"
        };

        //please find what the method does as explained before
        var startAndRange = GetSpriteStartIndexAndRange(animationType);

        //distringuish between idling and moving
        var spriteKeyFrames = startAndRange.Range > 1 ? new ObjectReferenceKeyframe[startAndRange.Range + 2] : new ObjectReferenceKeyframe[1];
        float timeValue = 0f;

        //moving case
        if (startAndRange.Range > 1)
        {
            //setup all the keyframes at a certain position

            //initate the first frame at time 0 and the last frame at time 1, because thats the way I want it
            spriteKeyFrames[0] = new ObjectReferenceKeyframe();
            spriteKeyFrames[0].time = 0f;
            spriteKeyFrames[0].value = loadedSprites[startAndRange.StartIndex + 1];

            spriteKeyFrames[startAndRange.Range + 1] = new ObjectReferenceKeyframe();
            spriteKeyFrames[startAndRange.Range + 1].time = 1f;
            spriteKeyFrames[startAndRange.Range + 1].value = loadedSprites[startAndRange.StartIndex + 1];

            //now iterate through the number of keyframes in between, if you have 2 movement frames then you are as OK as your are with 10 in between, it works for both
            for (int i = 1; i < startAndRange.Range + 1; i++)
            {
                timeValue += 1f / (startAndRange.Range + 1);
                spriteKeyFrames[i] = new ObjectReferenceKeyframe();
                spriteKeyFrames[i].time = timeValue;
                spriteKeyFrames[i].value = loadedSprites[i - 1 + startAndRange.StartIndex];
            }
        }
        else //idling case
        {
            spriteKeyFrames[0] = new ObjectReferenceKeyframe();
            spriteKeyFrames[0].time = 0f;
            spriteKeyFrames[0].value = loadedSprites[startAndRange.StartIndex];
        }

        //bind the recent generated keyframes to the animationclip
        AnimationUtility.SetObjectReferenceCurve(animClip, spriteBinding, spriteKeyFrames);

        //if you want looping for your anim, then do this
        var animClipSettings = new AnimationClipSettings { loopTime = true };
        AnimationUtility.SetAnimationClipSettings(animClip, animClipSettings);

        //assign the framerate and looping
        animClip.frameRate = FPS;
        animClip.wrapMode = WrapMode.Loop;

        //add the animation clip that we've just generated to the dictionary for later use
        animationClipsDictionary.Add(animationType, animClip);
    }
}

Last but not least, the heavy part comes along.
Generate the Animator Controller, including layers, parameters, state machine, states, transitions, blendtrees & all motions.

Generating the Animator Controller

private void GenerateNewAnimatorControllerContent()
{
    //get the original animator controller
    AnimatorController rootAnimatorController = (AnimatorController)spriteAnimator.runtimeAnimatorController;

    //setup a new animator controller and set basic properties, like the layer, name and it's parameters the same as the origin
    newAnimatorController = new AnimatorController();
    newAnimatorController.name = rootAnimatorController.name;
    newAnimatorController.AddLayer(rootAnimatorController.layers[0]);
    newAnimatorController.parameters = rootAnimatorController.parameters;

    //new need the statemachine from the new controller
    AnimatorStateMachine newStateMachine = newAnimatorController.layers[0].stateMachine;

    //as well as the state you have seen in the preview "NPC_Movement", of you have multiple state in the first layer, then nest the all the following code and iterate through "states"
    ChildAnimatorState rootAnimatorState = rootAnimatorController.layers[0].stateMachine.states[0];

    //generate a new animator steate
    AnimatorState newAnimState = new AnimatorState();

    //name it the same as the orgin "NPC_Movement"
    newAnimState.name = rootAnimatorState.state.name;

    //get the original blendtree from the NPC_Movement state
    BlendTree originRootBlendtree = (BlendTree)rootAnimatorState.state.motion;

    //our new blendtree, with all properties copied from the origin
    BlendTree newRootBlendtree = new BlendTree();
    newRootBlendtree.name = originRootBlendtree.name;
    newRootBlendtree.blendType = originRootBlendtree.blendType;
    newRootBlendtree.blendParameter = originRootBlendtree.blendParameter;
    newRootBlendtree.blendParameterY = originRootBlendtree.blendParameterY;

    //now we iterate through all the childrens of the blend tree, which is basically the middle line of the blendtree picture from the preview
    foreach (ChildMotion firstLevelChilds in originRootBlendtree.children)
    {
        //calling it "firstlevel" , because "zerolevel" is the NPC_Movement itself
        BlendTree firstLevelBlendTree = (BlendTree)firstLevelChilds.motion;

        //copy the basic properties from the current origin blendtree
        BlendTree newFirstLevelBlendTree = new BlendTree();
        newFirstLevelBlendTree.blendType = firstLevelBlendTree.blendType;
        newFirstLevelBlendTree.name = firstLevelBlendTree.name;
        newFirstLevelBlendTree.blendParameter = firstLevelBlendTree.blendParameter;

        //now iterate through all the children the current child blendtree
        AnimationType animType;
        foreach (ChildMotion secondLevelChild in firstLevelBlendTree.children)
        {
            //now we will make use our pre-setup AnimationType and pre generated animatino clips, stored in the dictionary
            //if we cannot parse the name of the origin motion into an animationtype, the we fail over, so check if you origin motions/clips are called the same as from the enum below
            if (!Enum.TryParse<AnimationType>(secondLevelChild.motion.name, out animType))
            {
                throw new InvalidEnumArgumentException();
            }
            //pickup the animation clip from the dictionary
            AnimationClip newSecondLevelMotion = animationClipsDictionary[animType];

            //add the new animation clip to the current child blendtree
            newFirstLevelBlendTree.AddChild(newSecondLevelMotion, secondLevelChild.threshold);
        }

        //now add the whole child blendtree to the root blendtree at position as like the origin
        newRootBlendtree.AddChild(newFirstLevelBlendTree, new Vector2(firstLevelChilds.position.x, firstLevelChilds.position.y));
    }

    //assign the new generated blendtree as the motion of the state
    newAnimState.motion = newRootBlendtree;

    //add the fresh state to the new controller
    newStateMachine.AddState(newAnimState, rootAnimatorState.position);

    //set this state as the default state (only use this if thats the case for you, you might want to do a check on the state name or something to evaluate the default state for one of your states)
    newStateMachine.defaultState = newAnimState;

    //finally assign the newly generated controller to the current animator
    spriteAnimator.runtimeAnimatorController = newAnimatorController;
}

Cool right?
It’s not as much as it could be if you imagine this the first time.

Thats basically it.

Here is the inspector setup of my example.
AnimatedSpriteSwapper

Just drag&drop the texture spritesheet you want for this new gameobject (and for sure you have configured in the GraphicBundleContainer
https://www.bilder-upload.eu/upload/065ae7-1593347607.png
(for later reupload: animatedspriteswapper.png)

There is a third class “AllAnimatedAnimatorControllers.cs” which is necessary and holds all AnimatorControllers, to avoid duplicate generation, gaining performance improvment if you have multiple NPC with the same spritesheet. You don’t need to configure it somehow, just add it to your lib.

Now after you have read all this, you might wanna check the attached code :wink:
There are more explanations for instance.

And again: This is just an example, your animation might be completly different, maybe you don’t have any blend trees or more nested blendtrees.
What I wanna expose by showing all this is how you can do this in general.
The limitations are up on your mind, so feel free to adopt the attached scripts to fit your needs better.

After you got this all runing (or before ^_^) I can highly recommend to checkout the repo from VirtuaBoza linked above in the spoiler.
His approach is more generic, unfortunately not including blendtrees, but maybe you can implement it yourself, to have some super duper powerfull omni-generic AnimationController generator :smile:

Let me know if you have any questions in regards of this, because it’s absolutly not trivial :slight_smile:


So far so good !
That’s it for now :smile:

Please let me know if you have any questions and any advices what I can do better or maybe something is completly wrong, so feel free to teach me and I will update this thread.

KR,
blu3

5133266–513518–AnimatedSpriteSwapper.rar (4.51 KB)
5133266–651125–TileHeightManager.rar (1.56 KB)

31 Likes

Thanks for sharing; I’m not sure what the criteria is for pinning threads but I will inquire and try to get someone to contact you.

An excellent guide! Well done, amigo!
Hopefully your guide does get pinned because this has a lot of potential to expand to cover all areas. I see a lot of FAQ’s about TileMapping so this will be a great help to the community!

Thanks to the Unity guys and mods for helping me getting this pinned.
I’m super glad to have such a cool team around :slight_smile:

I will try to constantly work on this thread in a certain way.

And as all of us I’m still learning myself everyday, so excuse me in advance if something is not working perfectly as explained or expected.
Just keep me (us) in line and we will make this thread great :slight_smile:

4 Likes

** Article / Thread Update **
Please find changes for Point 6.

** NEW **
“How to swap sprites in an animator / animator controller / animation clip on runtime ?”

Please find a new guidline @ Point 8 :slight_smile:

1 Like

** Article / Thread Update **
Point 8 was updated and bug fixed, also some performance improvement, please download the new Version.
You just need to substitute the AnimatedSpriteSwapper.cs and add the AllGeneratedAnimatorControllers.cs to your lib.

2 Likes

I was able to accomplish point 8 with Animator Override Controllers. Perhaps your situation was/is more complex though.
https://docs.unity3d.com/Manual/AnimatorOverrideController.html

Thanks for looking into this spryx, but I guess you got the thing I am issuing in the point 8 not like it was meant to be (sometimes it’s not that easy to entirly explain, but I try hard :smile:)

I am totally aware of the override controllers, but they do not help in the case I have mentioned.
Because what my solution provides is to absolutly and perfectly do nothing after beeing setup once, but drag & drop a substituting spritesheet to the AnimateSpriteSwapper component and it will generate all the Animation Clips and build all the Animator Controller logic for you.

The Override Controller unfortunatly lacks this specific aspect:
Have an identicaly Animation Clip just with another sprite.

And to be honest this is for the most simple cases the most work you would do. Simple “storm-clicking” and repetetive dragging & dropping sprite tiles and aligning them into the N-th-hundred Animation Clip to just have the absolute same behaviour as hundred times before, but with a new sprite.

I would recommend your solution in the “special” cases where an Animator Controller is super overly complex, but then ends up into some overseeable amount of Animation Clips while you won’t have too many different looking objects that should behave absolutly the same.

Otherwise, if you have - like - …orcs, ogre, blobs, elves, npc guy 1 to npc guy 200, which are all acting the same way regarding their animation controller & clips & fps & keyframes, then I would kinda insist on using my approach.
The logic how fast and when the different states should take place in an animator controller should be outside of the animator controller anyways. This is why my approach is still not static, but just super on-the-fly usuable if setup once under the mentioned circumstances :slight_smile:

Feel free to ask if I still couldn’t explain it clearly enough :slight_smile:

KR
blu3

** NEW **
“8. How to get multiple prefabs into a single Tilepalette and draw with default brush as you would draw any other tiles?”

Please find a new guidline @ Point 8 :slight_smile:
(Note: previous point 8 moved to 9 to keep the order of skill in the guide - check the overview section)

Have fun with it and let me know if it helped :slight_smile:

KR
blu3

@blu3drag0n Hi! and thank you for sharing your knowledge! :slight_smile:

Would you mind to open another thread ?
This question can be answered in several ways and is not too related to this article / guidlines.
We can talk in private but I recommend to get more input but mine^_^

I guess I can do that. Do you know the answer to my question?

A cool guide, many thanks for devoting time and effort to create it!

Things I wish I knew before starting with tilemaps:

There is no way for a tilemap to draw a mask.
They can be masked themselves, but not mask other sprites or tilemaps.

What do you mean exactly? Could you give examples?

I’m working on a space game, ships fly over starbases. Ships have a shadow sprite, sitting ordered between the starbase and the ships themselves. There’s a starfield behind the starbases.

Starbases are a tilemap. I can’t clip the shadows to the starbases, because there’s no way a tilemap can draw a mask, so the shadows of my ships are COVERING THE STARS, which looks very, very wrong.
If the starbases were a single huge sprite, I could add a spritemask renderer component and then have the shadows draw only inside the mask, but then I’d lose all the benefits of tile mapping.

Tilemaps can cast or receive shadows. You must select your tilemap and set the Inspector to “Debug” and scroll down to Tilemap Renderer to see that. Now I don’t know if it’ll work as you wish and even if it works at all. :slight_smile:

Hi, here are another hints on adjusting individual tiles on tilemap.

For a long time, I had Tile Palette on the same screen area as inspector, so I did not see I can manipulate individual tiles, that I placed onto tilemap.
But if you select Tile Palette tool to “select” (1.) and then select tile on tile map (2.), then in your inspector you will see properties of this individual tile. You can change its transform and tint. But look down - Lock Color is set to true. It means, that you can’t change tint for now (I will get to it later).

For now, you can change position, which is offset from tile center, rotation and scale, bacause transform is not locked. For example like this:


Now, if you want to change tint, you have to select tile asset, switch inspector to debug mode (1.) and unlock color (2). As you can see, you can lock/unlock transform changes here as well.

Return back to tilemap and select tile again. Now, Lock Color should be set to false and you can change tint for tile:

3 Likes

There is also one much needed info on how to attach / add custom data on individual tiles on the tile map. Which can be used in case such as to store cost data for navigation path finding, or other game play purpose. At the moment, I can’t think of any better way than trying to wrap up tiles with custom class and maintain another “set” of custom tilemap that stores wrapped version of the tile. But to be honest, this method is not intuitive, because :

  1. You would need to rescan the tilemap and update custom tilemap everytime the tilemap changes.
  2. You have to maintain your own custom tilemap that basically wraps the existing tilemap.

We should be able to add / define / store our own custom attributes on individual tile of the tilemap without having to recreate our own version of one… If anyone has any other better way to handle this then it will be interesting to know.