Search Unity

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

Discussion in '2D' started by blu3drag0n, Nov 2, 2019.

  1. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    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.

    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:
    https://learn.unity.com/tutorial/re...emachine?language=en#5c7f8528edbc2a002053b6af
    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 ! :D

    --------------------------------------------------

    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 ?
    5. 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.

    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​

    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 ?

    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:
    https://docs.unity3d.com/Manual/CustomPhysicsShape.html
    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 "Generade 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).


    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.

    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.

    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.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using UnityEngine.Tilemaps;
    4.  
    5. [ExecuteInEditMode]
    6. public class GetTilemapCoordinate : MonoBehaviour
    7. {
    8.     public Grid grid;
    9.     public Tilemap map;
    10.     public bool Active = false; //this is just a toggle, to be able to disable the script when not needed
    11.  
    12.     public void ToggleActive()
    13.     {
    14.         if(Active)
    15.             SceneView.duringSceneGui += GetMousePosition;
    16.         else
    17.             SceneView.duringSceneGui -= GetMousePosition;
    18.  
    19.     }
    20.  
    21.     public void OnValidate()
    22.     {
    23.         ToggleActive();
    24.     }
    25.  
    26.     public void GetMousePosition(SceneView scene)
    27.     {
    28.         Event e = Event.current;
    29.         if (e != null)
    30.         {
    31.             if (Event.current.type == EventType.MouseDown)
    32.             {
    33.                 Vector3Int position = Vector3Int.FloorToInt(HandleUtility.GUIPointToWorldRay(Event.current.mousePosition).origin);
    34.                 Vector3Int gridCellPos = grid != null ? grid.WorldToCell(position) : Vector3Int.zero;
    35.                 Vector3Int mapCellPos = map != null ? map.WorldToCell(position) : Vector3Int.zero;
    36.  
    37.                 Debug.Log("Clicked Tile position in Grid: "+ gridCellPos);
    38.                 Debug.Log("Clicked Tile position in Tilemap: "+ gridCellPos);
    39.             }
    40.         }
    41.     }
    42. }


    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 :D

    Code (CSharp):
    1.  
    2. //assuming you have a Tilemap referenced in the var tileMap
    3. Vector3Int pos = new Vector3Int(1, 2, 0);
    4. float rotation = 90f; //degrees presented in a float
    5. tileMap.SetTransformMatrix(pos, Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0f,0f, rotation), Vector3.one));
    6.  
    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^_^

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

    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 :p
    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.
    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 ?

    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:
    https://github.com/creativitRy/Tile...ilemapHeightTest/Scripts/TileHeightManager.cs

    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

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

    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.

    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)

    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)
    Code (CSharp):
    1.  
    2. //assuming you have your player referenced in playerObject and the SpriteRenderer of the player referenced in spriteRenderer
    3. TileHeightManager.Instance.ReportPosition(playerObject.transform, spriteRenderer.sprite.bounds); //according to the last update, now you report the transform and not the position
    4.  
    5.  
    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 :)


    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.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class PlayerController : MonoBehaviour
    4. {
    5.     [Header("General:")]
    6.     [Space]
    7.     public bool npcTackled = false;
    8.     public Collision2D tackledNPC;
    9.  
    10.  
    11.     [Header("Movement Settings:")]
    12.     [Space]
    13.     public float movementBaseSpeed = 1.0f;
    14.     public Vector2 movementDirection = Vector2.zero;
    15.     public float movementSpeed = 0.0f;
    16.     public bool canMove = true;
    17.  
    18.     [Header("References:")]
    19.     [Space]
    20.     public Rigidbody2D playerRB;
    21.  
    22.  
    23.     void Update()
    24.     {
    25.         if (canMove)
    26.         {
    27.             ProcessMovementInputs();
    28.             Move();    
    29.         }
    30.     }
    31.     #region Movement Handling
    32.     void ProcessMovementInputs()
    33.     {
    34.         //reset that we are moving
    35.         movementSpeed = 0.0f;
    36.  
    37.         //get the absolut inpuit from arrow keys to decide in which direction to move the player
    38.         movementDirection.x = Input.GetAxisRaw("Horizontal");
    39.         movementDirection.y = Input.GetAxisRaw("Vertical");
    40.  
    41.         //if the movement direction is not equal to the zero vector we will define the movmentspeed and declare that the player is actually moving
    42.         if (movementDirection != Vector2.zero)
    43.         {
    44.             //clamp the movementdirections magnitude between 0 and 1, so nobody cheat with special input devices (xbox controllers), and assign it as the movementspeed
    45.             movementSpeed = Mathf.Clamp(movementDirection.magnitude, 0.0f, 1.0f);
    46.             //normalize the movement direction, so we are not unrealisticly moving double as fast when using diagonal movement direction
    47.             movementDirection.Normalize();
    48.         }
    49.     }
    50.  
    51.     void Move()
    52.     {
    53.         //only move the palyer into the direction when he currently not in contact with an NPC
    54.         if (!npcTackled)
    55.         {
    56.             playerRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
    57.         }
    58.         else
    59.         {
    60.             //get the relative position of the NPC to the player
    61.             Vector2 positionRelative = transform.InverseTransformPoint(tackledNPC.transform.position);
    62.             //if we are stucking at the NPC we need to trick around, so we can leave the NPC's colliding shape again
    63.             //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
    64.             float moveRelative = Vector2.Distance(positionRelative, movementDirection);
    65.             //as if the player is moving away from the NPC the moveRelative will get > 1, so we can assign the normal movement flow
    66.             //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
    67.             if (moveRelative > 1.0f)
    68.             {
    69.                 playerRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
    70.             }
    71.             else
    72.                 playerRB.velocity = Vector2.zero;
    73.         }
    74.     }
    75.  
    76.     private void OnCollisionEnter2D(Collision2D collision)
    77.     {
    78.         //only care for collision with NPC
    79.         //other collisions will be treated by the collider components (static structures, that cant get pushed)
    80.         if (collision.transform.tag == "NPC")
    81.         {
    82.             npcTackled = true;
    83.             //save the currently tackled NPC for later uses, e.g. relative position and talking with the NPC
    84.             tackledNPC = collision;
    85.         }
    86.     }
    87.  
    88.     private void OnCollisionExit2D(Collision2D collision)
    89.     {
    90.         if (npcTackled)
    91.         {
    92.             npcTackled = false;
    93.             tackledNPC = null;
    94.         }
    95.     }
    96.     #endregion
    97.  
    98. }
    99.  

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. public class NPCController : MonoBehaviour
    5. {
    6.     [Header("Movement Settings:")]
    7.     [Space]
    8.     public bool freeMoving = true;
    9.     public float movementFrequenceThreshold = 1.0f;
    10.     public float movementFrequence = 0.1f;
    11.     public float movementBaseSpeed = 1.0f;
    12.     public float movementDuration = 1.0f;
    13.     public Vector2 movementDirection = new Vector2(0.0f, 0.0f);
    14.  
    15.     public float movementSpeed;
    16.     public float movementFrequenceCounter = 0.0f;
    17.     public float movementDurationCounter = 0.0f;
    18.     public bool shouldMove = false;
    19.     public bool tackled = false;
    20.  
    21.     [Header("References:")]
    22.     [Space]
    23.     public Rigidbody2D npcRB;
    24.  
    25.     void Update()
    26.     {
    27.         if (!tackled)
    28.         {
    29.             if (freeMoving)
    30.             {
    31.                 ProcessAutoMovement();
    32.                 Move();
    33.             }
    34.             else
    35.                 movementSpeed = 0.0f;
    36.         }
    37.         else
    38.         {
    39.             movementSpeed = 0.0f;
    40.             movementDirection = Vector2.zero;
    41.             npcRB.velocity = Vector2.zero;
    42.         }
    43.     }
    44.  
    45.     void ProcessAutoMovement()
    46.     {
    47.         if (movementFrequenceCounter > movementFrequenceThreshold)
    48.         {
    49.             movementFrequenceCounter = 0.0f;
    50.             shouldMove = true;
    51.  
    52.             for (int i = 0; i < 2; i++)
    53.             {
    54.                 int randomizer = UnityEngine.Random.Range(0, 4);
    55.                 switch (randomizer)
    56.                 {
    57.                     case 0:
    58.                         movementDirection.x += 1.0f;
    59.                         break;
    60.                     case 1:
    61.                         movementDirection.x -= 1.0f;
    62.                         break;
    63.                     case 2:
    64.                         movementDirection.y += 1.0f;
    65.                         break;
    66.                     case 3:
    67.                         movementDirection.y -= 1.0f;
    68.                         break;
    69.                     default:
    70.                         movementDirection = Vector2.zero;
    71.                         break;
    72.                 }
    73.             }
    74.  
    75.             movementSpeed = Mathf.Clamp(movementDirection.magnitude, 0.0f, 1.0f);
    76.             movementDirection.Normalize();
    77.         }
    78.         else
    79.             movementFrequenceCounter += movementFrequence;
    80.     }
    81.  
    82.     void Move()
    83.     {
    84.         if (shouldMove)
    85.         {
    86.             if (movementDurationCounter < movementDuration)
    87.             {
    88.                 npcRB.velocity = movementDirection * movementSpeed * movementBaseSpeed;
    89.                 movementDurationCounter += Time.deltaTime;
    90.             }
    91.             else
    92.             {
    93.                 movementDurationCounter = 0.0f;
    94.                 shouldMove = false;
    95.                 npcRB.velocity = Vector2.zero;
    96.                 movementSpeed = 0.0f;
    97.             }
    98.         }
    99.     }
    100.  
    101.  
    102.  
    103.     void OnCollisionEnter2D(Collision2D collision)
    104.     {
    105.         tackled = true;
    106.         if (collision.transform.tag == "Player")
    107.         {
    108.             Vector2 positionRelative = transform.InverseTransformPoint(collision.transform.position);
    109.             movementDirection = positionRelative;
    110.         }
    111.     }
    112.  
    113.     private void OnCollisionStay2D(Collision2D collision)
    114.     {
    115.         if (!(collision.transform.tag == "Player"))
    116.             tackled = false;
    117.     }
    118.  
    119.     private void OnCollisionExit2D(Collision2D collision)
    120.     {
    121.         tackled = false;
    122.     }
    123. }
    124.  


    (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
    1. totally common drag & drop of tiles into a common tilepalette


    2. completely normal drawing WITH the Default Brush and the erase tool and everything as usual


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


    keep reading to get some little know-how to do it :)

    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
    Code (csharp):
    1.  
    2. public class PickupTile : MonoBehaviour
    3. {
    4.     public PickupTileType pickupTileType;
    5.     private void Start()
    6.     {
    7.         //get the painted tile from the position where you've painted it in the scene view
    8.         Sprite paintedTile = GetComponentInParent<Tilemap>().GetSprite(Vector3Int.FloorToInt(this.transform.position));
    9.         //attach a spriterenderer to the prefab and assign the painted tile the the SR
    10.         this.gameObject.AddComponent<SpriteRenderer>().sprite = paintedTile;
    11.         //add a PolygonCollider2D, unity uses the physics shape of the imported sprite to automatically generate a polygon collider around it
    12.         this.gameObject.AddComponent<PolygonCollider2D>();
    13.         //save the information what type of PickupTile we have painted here to the prefab itself
    14.         if (!Enum.TryParse<PickupTileType>(paintedTile.name, true, out pickupTileType))
    15.             throw new Exception("Could not find \"" + paintedTile.name + "\" in the Enum PickupTileType, please check your Enum or Spritename!");
    16.         //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
    17.         Destroy(this.GetComponent<SpriteRenderer>());
    18.     }
    19.     private void OnCollisionEnter2D(Collision2D collision)
    20.     {
    21.         switch (pickupTileType)
    22.         {
    23.             case PickupTileType.Wood:
    24.                 Debug.Log("OMG finally I found some wood, time to make a campfire!");
    25.                 break;
    26.             case PickupTileType.Stone:
    27.                 Debug.Log("That rock might hurt if it hits the head, mhhh");
    28.                 break;
    29.         }
    30.        
    31.     }
    32.     public enum PickupTileType
    33.     {
    34.         Wood,
    35.         Stone        
    36.     }
    37. }
    38.  
    39.  

    Another note:
    It's really just an example for the purpse 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 dynamcal.
    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 :p

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


    6. check on your tile , in my example "Stone"-tile and then you will see the following


    7. you already see and will find the property "Instance Game Object"
    8. this will do all the magic for you, so drag & drop your prefab(s) to this property
    9. drag & drop the so created tiles into a tilepalette you wish and use the default brush as you do for every other thing
    10. 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" :D

    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:
    https://www.erikmoberg.net/article/unity3d-replace-sprite-programmatically-in-animation

    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
    There is nothing special with the Animator Component.

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


    Now the "NPC_Movement" state with his blendtree.

    First of all we need to setup some GraphicBundleContainer.cs
    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.

    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.
    Thanks to VirtuaBoza, he inspired me to this approach when I found his git.
    https://github.com/VirtuaBoza/SpriteSheetSwapping

    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
    Code (CSHARP):
    1.  
    2. //setup all your AnimationType, if you are on 8 direction, then feel free to extend this
    3. public enum AnimationType
    4.     {
    5.         Idle_Up,
    6.         Idle_Right,
    7.         Idle_Down,
    8.         Idle_Left,
    9.         Run_Up,
    10.         Run_Right,
    11.         Run_Down,
    12.         Run_Left
    13.     }
    14.  
    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

    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
    Code (CSHARP):
    1.  
    2. private SpriteSheetAnimationInfo GetSpriteStartIndexAndRange(AnimationType animationType)
    3. {
    4.     switch (animationType)
    5.     {
    6.         case AnimationType.Run_Up:
    7.             return new SpriteSheetAnimationInfo(9, 3);
    8.         case AnimationType.Run_Right:
    9.             return new SpriteSheetAnimationInfo(6, 3);
    10.         case AnimationType.Run_Down:
    11.             return new SpriteSheetAnimationInfo(0, 3);
    12.         case AnimationType.Run_Left:
    13.             return new SpriteSheetAnimationInfo(3, 3);
    14.  
    15.         case AnimationType.Idle_Up:
    16.             return new SpriteSheetAnimationInfo(10, 1);
    17.         case AnimationType.Idle_Right:
    18.             return new SpriteSheetAnimationInfo(7, 1);
    19.         case AnimationType.Idle_Down:
    20.             return new SpriteSheetAnimationInfo(1, 1);
    21.         case AnimationType.Idle_Left:
    22.             return new SpriteSheetAnimationInfo(4, 1);
    23.         default:
    24.             throw new InvalidEnumArgumentException();
    25.     }
    26. }
    27.  
    28.  
    29. public struct SpriteSheetAnimationInfo
    30. {
    31.     public SpriteSheetAnimationInfo(int startIndex, int range)
    32.     {
    33.         StartIndex = startIndex;
    34.         Range = range;
    35.     }
    36.  
    37.     public int StartIndex
    38.     {
    39.         get; private set;
    40.     }
    41.     public int Range
    42.     {
    43.         get; private set;
    44.     }
    45. }
    46.  

    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.
    Code (csharp):
    1.  
    2. public void CreateAnimationClips()
    3. {
    4.     //generate all the animation clips according to your number of animation types (e.g. Idle_Down & Run_Down ..)
    5.  
    6.     foreach (AnimationType animationType in AnimationType.GetValues(typeof(AnimationType)))
    7.     {
    8.         //give the animation clip a unique name
    9.         var animClip = new AnimationClip { name = $"{spritesheetForAnimation.name} {animationType}" };
    10.  
    11.         //just some generous setting to the editorCurveBinding, "propertyName" is m_Sprite, because SpriteRenderer has it's sprite stored in m_Sprite
    12.  
    13.         var spriteBinding = new EditorCurveBinding
    14.         {
    15.             type = typeof(SpriteRenderer),
    16.             path = string.Empty,
    17.             propertyName = "m_Sprite"
    18.         };
    19.  
    20.         //please find what the method does as explained before
    21.         var startAndRange = GetSpriteStartIndexAndRange(animationType);
    22.  
    23.         //distringuish between idling and moving
    24.         var spriteKeyFrames = startAndRange.Range > 1 ? new ObjectReferenceKeyframe[startAndRange.Range + 2] : new ObjectReferenceKeyframe[1];
    25.         float timeValue = 0f;
    26.  
    27.         //moving case
    28.         if (startAndRange.Range > 1)
    29.         {
    30.             //setup all the keyframes at a certain position
    31.  
    32.             //initate the first frame at time 0 and the last frame at time 1, because thats the way I want it
    33.             spriteKeyFrames[0] = new ObjectReferenceKeyframe();
    34.             spriteKeyFrames[0].time = 0f;
    35.             spriteKeyFrames[0].value = loadedSprites[startAndRange.StartIndex + 1];
    36.  
    37.             spriteKeyFrames[startAndRange.Range + 1] = new ObjectReferenceKeyframe();
    38.             spriteKeyFrames[startAndRange.Range + 1].time = 1f;
    39.             spriteKeyFrames[startAndRange.Range + 1].value = loadedSprites[startAndRange.StartIndex + 1];
    40.  
    41.             //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
    42.             for (int i = 1; i < startAndRange.Range + 1; i++)
    43.             {
    44.                 timeValue += 1f / (startAndRange.Range + 1);
    45.                 spriteKeyFrames[i] = new ObjectReferenceKeyframe();
    46.                 spriteKeyFrames[i].time = timeValue;
    47.                 spriteKeyFrames[i].value = loadedSprites[i - 1 + startAndRange.StartIndex];
    48.             }
    49.         }
    50.         else //idling case
    51.         {
    52.             spriteKeyFrames[0] = new ObjectReferenceKeyframe();
    53.             spriteKeyFrames[0].time = 0f;
    54.             spriteKeyFrames[0].value = loadedSprites[startAndRange.StartIndex];
    55.         }
    56.  
    57.         //bind the recent generated keyframes to the animationclip
    58.         AnimationUtility.SetObjectReferenceCurve(animClip, spriteBinding, spriteKeyFrames);
    59.  
    60.         //if you want looping for your anim, then do this
    61.         var animClipSettings = new AnimationClipSettings { loopTime = true };
    62.         AnimationUtility.SetAnimationClipSettings(animClip, animClipSettings);
    63.  
    64.         //assign the framerate and looping
    65.         animClip.frameRate = FPS;
    66.         animClip.wrapMode = WrapMode.Loop;
    67.  
    68.         //add the animation clip that we've just generated to the dictionary for later use
    69.         animationClipsDictionary.Add(animationType, animClip);
    70.     }
    71. }
    72.  

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

    Code (csharp):
    1.  
    2.  
    3. private void GenerateNewAnimatorControllerContent()
    4. {
    5.     //get the original animator controller
    6.     AnimatorController rootAnimatorController = (AnimatorController)spriteAnimator.runtimeAnimatorController;
    7.  
    8.     //setup a new animator controller and set basic properties, like the layer, name and it's parameters the same as the origin
    9.     newAnimatorController = new AnimatorController();
    10.     newAnimatorController.name = rootAnimatorController.name;
    11.     newAnimatorController.AddLayer(rootAnimatorController.layers[0]);
    12.     newAnimatorController.parameters = rootAnimatorController.parameters;
    13.  
    14.     //new need the statemachine from the new controller
    15.     AnimatorStateMachine newStateMachine = newAnimatorController.layers[0].stateMachine;
    16.  
    17.     //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"
    18.     ChildAnimatorState rootAnimatorState = rootAnimatorController.layers[0].stateMachine.states[0];
    19.  
    20.     //generate a new animator steate
    21.     AnimatorState newAnimState = new AnimatorState();
    22.  
    23.     //name it the same as the orgin "NPC_Movement"
    24.     newAnimState.name = rootAnimatorState.state.name;
    25.  
    26.     //get the original blendtree from the NPC_Movement state
    27.     BlendTree originRootBlendtree = (BlendTree)rootAnimatorState.state.motion;
    28.  
    29.     //our new blendtree, with all properties copied from the origin
    30.     BlendTree newRootBlendtree = new BlendTree();
    31.     newRootBlendtree.name = originRootBlendtree.name;
    32.     newRootBlendtree.blendType = originRootBlendtree.blendType;
    33.     newRootBlendtree.blendParameter = originRootBlendtree.blendParameter;
    34.     newRootBlendtree.blendParameterY = originRootBlendtree.blendParameterY;
    35.  
    36.     //now we iterate through all the childrens of the blend tree, which is basically the middle line of the blendtree picture from the preview
    37.     foreach (ChildMotion firstLevelChilds in originRootBlendtree.children)
    38.     {
    39.         //calling it "firstlevel" , because "zerolevel" is the NPC_Movement itself
    40.         BlendTree firstLevelBlendTree = (BlendTree)firstLevelChilds.motion;
    41.  
    42.         //copy the basic properties from the current origin blendtree
    43.         BlendTree newFirstLevelBlendTree = new BlendTree();
    44.         newFirstLevelBlendTree.blendType = firstLevelBlendTree.blendType;
    45.         newFirstLevelBlendTree.name = firstLevelBlendTree.name;
    46.         newFirstLevelBlendTree.blendParameter = firstLevelBlendTree.blendParameter;
    47.  
    48.         //now iterate through all the children the current child blendtree
    49.         AnimationType animType;
    50.         foreach (ChildMotion secondLevelChild in firstLevelBlendTree.children)
    51.         {
    52.             //now we will make use our pre-setup AnimationType and pre generated animatino clips, stored in the dictionary
    53.             //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
    54.             if (!Enum.TryParse<AnimationType>(secondLevelChild.motion.name, out animType))
    55.             {
    56.                 throw new InvalidEnumArgumentException();
    57.             }
    58.             //pickup the animation clip from the dictionary
    59.             AnimationClip newSecondLevelMotion = animationClipsDictionary[animType];
    60.  
    61.             //add the new animation clip to the current child blendtree
    62.             newFirstLevelBlendTree.AddChild(newSecondLevelMotion, secondLevelChild.threshold);
    63.         }
    64.  
    65.         //now add the whole child blendtree to the root blendtree at position as like the origin
    66.         newRootBlendtree.AddChild(newFirstLevelBlendTree, new Vector2(firstLevelChilds.position.x, firstLevelChilds.position.y));
    67.     }
    68.  
    69.     //assign the new generated blendtree as the motion of the state
    70.     newAnimState.motion = newRootBlendtree;
    71.  
    72.     //add the fresh state to the new controller
    73.     newStateMachine.AddState(newAnimState, rootAnimatorState.position);
    74.  
    75.     //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)
    76.     newStateMachine.defaultState = newAnimState;
    77.  
    78.     //finally assign the newly generated controller to the current animator
    79.     spriteAnimator.runtimeAnimatorController = newAnimatorController;
    80. }
    81.  

    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.
    Just drag&drop the texture spritesheet you want for this new gameobject (and for sure you have configured in the GraphicBundleContainer

    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 :D

    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 :D

    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
     

    Attached Files:

    Last edited: Nov 29, 2019
  2. ed_s

    ed_s

    Unity Technologies

    Joined:
    Apr 17, 2015
    Posts:
    67
    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.
     
  3. MisterSkitz

    MisterSkitz

    Joined:
    Sep 2, 2015
    Posts:
    815
    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!
     
  4. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    Thanks to the Unity guys and mods for helping me getting this pinned.
    I'm super glad to have such a cool team around :)

    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 :)
     
    Last edited: Nov 12, 2019
    rustum, Ted_Wikman and MisterSkitz like this.
  5. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    ** Article / Thread Update **
    Please find changes for Point 6.
     
  6. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    ** NEW **
    "How to swap sprites in an animator / animator controller / animation clip on runtime ?"


    Please find a new guidline @ Point 8 :)
     
    Last edited: Nov 29, 2019
    MisterSkitz likes this.
  7. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    ** 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.
     
    MisterSkitz and RemDust like this.
  8. spryx

    spryx

    Joined:
    Jul 23, 2013
    Posts:
    368
    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
     
  9. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    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 :D)

    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 :)

    Feel free to ask if I still couldn't explain it clearly enough :)

    KR
    blu3
     
  10. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    ** 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 :)
    (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 :)

    KR
    blu3
     
  11. APSchmidt

    APSchmidt

    Joined:
    Aug 8, 2016
    Posts:
    3,335
    @blu3drag0n Hi! and thank you for sharing your knowledge! :)
     
    Last edited: Dec 7, 2019
  12. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    65
    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^_^
     
  13. APSchmidt

    APSchmidt

    Joined:
    Aug 8, 2016
    Posts:
    3,335
    I guess I can do that. Do you know the answer to my question?
     
  14. Sicinsions

    Sicinsions

    Joined:
    Dec 18, 2019
    Posts:
    11
    A cool guide, many thanks for devoting time and effort to create it!
     
  15. Undertaker-Infinity

    Undertaker-Infinity

    Joined:
    May 2, 2014
    Posts:
    69
    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.
     
  16. APSchmidt

    APSchmidt

    Joined:
    Aug 8, 2016
    Posts:
    3,335
    What do you mean exactly? Could you give examples?
     
  17. Undertaker-Infinity

    Undertaker-Infinity

    Joined:
    May 2, 2014
    Posts:
    69
    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.
     
  18. APSchmidt

    APSchmidt

    Joined:
    Aug 8, 2016
    Posts:
    3,335
    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. :)
     
  19. Tom-Atom

    Tom-Atom

    Joined:
    Jun 29, 2014
    Posts:
    77
    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).

    TileProperties.png

    For now, you can change position, which is offset from tile center, rotation and scale, bacause transform is not locked. For example like this:
    TileProperties_transform_changed.png
    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.
    Tile_Unlock_Color.png

    Return back to tilemap and select tile again. Now, Lock Color should be set to false and you can change tint for tile:
    Tile_Change_Color.png
     
    APSchmidt likes this.
  20. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    1,525
    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.
     
  21. Rocky_Unity

    Rocky_Unity

    Joined:
    Oct 13, 2017
    Posts:
    3
    Edit:

    There are great tools on getting the most out of your tilemap! It's just that nobody seems to have made tutorials about using these (maybe I can do that at some point?)

    You can create custom tiles with whatever data you want, and you can also populate a grid full of information based on those tiles that you can manipulate. There are some catches to allow for both performance, yet still be individually manipulated to be aware of and that's where the challenge comes in.

    Regardless, the important tools lie here:
    https://github.com/Unity-Technologies/2d-extras/blob/master/Documentation~/TableOfContents.md
    Particularly, "Gridinformation"

    and demos of these are here:
    https://github.com/Unity-Technologies/2d-techdemos
     
    Last edited: Feb 10, 2020
  22. Undertaker-Infinity

    Undertaker-Infinity

    Joined:
    May 2, 2014
    Posts:
    69
    Yeah, I used shadows as an example, but I don't want lights, because then my fully 2D game would need sprites moving in Z so their shadow moves accordingly and then I can't control their look as precisely as I do with a sprite... but what if I wanted to mask something else that is not a shadow? explosion damage decals, for example.
    Tilemap cannot write to sprite masks. It should, just as sprite renderer can!
     
  23. DungDajHjep

    DungDajHjep

    Joined:
    Mar 25, 2015
    Posts:
    46
  24. THE_MEDICI

    THE_MEDICI

    Joined:
    Apr 8, 2018
    Posts:
    51
    I went ahead and recreated it. It's overkill for many kinds of projects. I have several parallel collections: one for tile classes with gameplay data/logic (health, etc), another for pathfinding, and another for my own physics. None of it derives from a Unity type. I didn't want any mega-objects and I wanted precise control over the collections and types for performance, serialization and source-transparency reasons.

    This parallel system uses entities with components, where the most common entity is a GameTile (its barebones gameplay data/logic) which has a component for the Visuals. This Visual component class encapsulates calls into the Unity API. On tile instantiation, the Visual component is called to draw however the tile is represented. When destroyed, the Visual erases itself. The nice thing about this approach is that I can have a tile which is represented by any number of UnityEngine.Tilemaps.TileBase-derived tiles in any spatial arrangement, or even as a Prefab with SpriteRenderer or ParticleSystem. When I want a new way to represent something on my map, I instantiate a tile and I don't need to think about how it's represented (Tilemap Tile, GameObject, whatever). If I need a new specialized representation, I derive a new type of Visual.

    I'm not quite sure how I would get it working with TileBrushes for a designer, though. I don't have that requirement because the world is procedurally generated. I only use the Editor for testing my visuals. No level design.
     
    Last edited: Feb 20, 2020 at 11:52 AM
  25. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    1,525
    I guess your method can work for a procedural generated maps, but when you want to just design actual tilemap level with brushes using tiles picked from pallet, it makes no sense to attach custom data to anywhere other than the tile itself. I know I can attach a custom game object to the tile, but that is going to spawn massive amounts of gameobjects which the tilemap is developed to avoid in the first place. The tile class, in my humble opinion needs to have some kind of generic class or scriptable object that we can attach to store our own custom data.
     
unityunity