Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

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

    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 ?
    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 swap sprites for an animated object in the Animator / Animator Controller / Animation Clip, including blend trees - ON RUNTIME ?

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


    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 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
    Just an Animator Controller, nothing special.


    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

    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 12, 2019 at 2:16 PM
  2. ed_s

    ed_s

    Unity Technologies

    Joined:
    Apr 17, 2015
    Posts:
    59
    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:
    623
    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:
    44
    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 at 9:17 AM
    Ted_Wikman and MisterSkitz like this.
  5. blu3drag0n

    blu3drag0n

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

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    44
    ** NEW **

    Please find a new guidline @ Point 8 :)
    "How to swap sprites in an animator / animator controller / animation clip on runtime ?"
     
    Last edited: Nov 12, 2019 at 12:53 PM
    MisterSkitz likes this.