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
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 !
(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
)
Overview:
Beginner:
- How to rotate a Tile while painting ?
- How to redraw an existing Tile seed from your Tilemap with the Random Brush ?
- 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 ?
- How to get absolute Tile coordinates, clicking in the Scene View, while working in Editor Mode ?
- a) How to rotate an already painted Tile On Runtime ?
- 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
//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
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!
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
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:
- 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
- totally common drag & drop of tiles into a common tilepalette
(for later reupload: prefabTile01.gif)
-
completely normal drawing with the default Brush and the erase tool and everything as usual
(for later reupload: prefabTile02.gif) -
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
But how to do this ?
Well Unity is tricky sometimes, but in this particular case it’s super sneaky at the same time.
- create a new tilepalette and give a name you like
- 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! - so now that you have your “stick” and your “stone” and your “sword” tile sorted…
- 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
So now we have our prefab, but how we get it attached?
5. ENTER THE MATRIX - just joking but… Enter Debug Mode !
How to enter Debug Mode
(for later reupload: debug-mode02.png)
- 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)
- you already see and will find the property “Instance Game Object”
- this will do all the magic for you, so drag & drop your prefab(s) to this property
- drag & drop the so created tiles into a tilepalette you wish and use the default brush as you do for every other thing
- 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”
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
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
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
Let me know if you have any questions in regards of this, because it’s absolutly not trivial
So far so good !
That’s it for now
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)