Search Unity

Feedback Frustrations with extending Tile (Tilemap)

Discussion in '2D' started by CoraBlue, Jan 17, 2023.

  1. CoraBlue

    CoraBlue

    Joined:
    Nov 16, 2017
    Posts:
    60
    Over the last year I've done a lot of experimenting with Unity's tile system. I keep coming across one particular headache over and over again. Extending the tile class in any way, shape, or form. Maybe someone can help me or we can get something changed.

    Take a very simple example. Say I want a surface type, so I know what sound to play when a tile is stepped on, or perhaps I want to find specific tiles programmatically to add doodads.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Tilemaps;
    3.  
    4. public class SurfaceTile : Tile
    5. {
    6.     [SerializeField] public Controller2DSurface.SurfaceTypes surfaceType;
    7. }
    You've already got your normal tile assets. Not every tile needs to be a SurfaceTile right? So we just need to apply this script to a few tile assets and we're done. Easy, right? No. Because as far as I can tell at no point has Unity considered this use case. You have to enter Debug Mode and change the script on the ScriptableObject.



    This functions. You'll get your custom tile behaviors working, but since you can't multi-edit while changing scripts, be prepared to change every single one of your hundreds of tile assets manually. You are now also locked out of Unity's hidden custom editor features which is practically a deal breaker. No rule tiles. Not even sprite previews in the Project view. And the Tile script is protected and can't be reselected from Unity's explorer, so you will never be able to revert anything.



    Someone please tell me I'm wrong about this and there's a simpler way. Creating custom tile behavior is too useful. Tilemap Extras is nice, but Unity will never be able to anticipate every single thing a dev might want to do with tiles and stepping off the beaten path is this difficult isn't really great for creativity.

    And I'm not the only one with the headache. I've seen countless assets on the store have to rewrite the entire RuleTile editor GUI to add support for custom behavior like this. It's actually insane. What's the deal here?
     
  2. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    249
    Unity tiles can't have persistent fields except in the asset itself (the tile in the Project folder). The TIlemap only serializes the asset reference, color, transform etc. So if you add a field and the place the tile, changing the field in an editor (if you could, which you can't, and the lack of serializing such things is why the custom inflexible inspector) would change the field only in the Asset.

    However if what you're looking for is tiles with persistent data you could check out (free) Tilemap Toolkit on the asset store. The current version does not work with rule tiles, but the new version coming out soon should. I could use a beta tester for that aspect of it since I don't use rule tiles very often. A custom selection / brush inspector allows use of tags in your tile scripts which control what appears in the inspector.

    Check it out and see if it might work for you. It's not right for everyone.

    DM me if you have any questions (Not in this thread please).

    If you can see my signature line (doesn't appear on mobile IIRC) there's a blog link that talks about some of the issues you raise.
     
  3. CoraBlue

    CoraBlue

    Joined:
    Nov 16, 2017
    Posts:
    60
    I am not sure why you think I want persistent per tile data. That's not relevant here. This implementation works fine for custom fields in the tile asset. It isn't that it's impossible. It's that it's a headache. Here.

    Code (CSharp):
    1. SurfaceTile currentPlayerTile = myTilemap.GetTile<SurfaceTile>(somePlayerWorldToCellPosition);
    2. Debug.Log(currentPlayerTile.surfaceType);
    It works. It's just that it makes life extremely inconvenient because it locks you out of certain Tile Palette features. To reiterate I hope there is either a better way or Unity can make some changes, like a ITile interface, or a ScriptableTile.
     
  4. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    249
    My mistake. If it's acceptable that the custom fields in the tile asset is shared by every instance of the tile on a tilemap then you're ok. It sounded like you wanted individual data instances for each placed tile for control purposes.

    "never mind" :)
     
  5. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    @CoraBlue

    Hi, I would like to understand your use-case.

    You have created Tilemaps with a few Tile Assets (say with the Tile type) and would like to change some of these Tile Assets to a new extended type (SurfaceTile type). To do this, you are currently changing these Tile Assets by changing the base script for these assets using the debug inspector. This is not ideal because:
    • Lack of multi-select for your various Tile Assets
    • Unable to choose protected types like Tile
    • Need to switch between Debug and Normal Inspector views.
    Do let me know if my understanding is correct!

    Also, are there any expectations when these assets are changed, eg. empty defaults for properties, maintain similar serialized data ?
     
  6. CoraBlue

    CoraBlue

    Joined:
    Nov 16, 2017
    Posts:
    60
    Thank you so much for responding. I will try and be more clear so I can outline exactly what the frustration is.

    Yes, I would like to create a custom tile type that plays well in Unity's Tilemap ecosystem. I would like to add custom methods that can be accessed with a typecast, or properties for other custom behavior (an example might be friction, luminance for example).

    The issue is that Unity doesn't like this. The RuleTile for example, doesn't use Tilebase. It accepts Sprites and creates the Tilebase objects under the hood. This means it is impossible to create RuleTiles using custom tile scripts that inherit from Tilebase. It also means Unity's UI code breaks down. Animated Tiles for example, your own Tilemap extension, has no preview in the Project view. It is much easier to navigate a sea of tiles when you can see a preview of the tile sprite instead of the generic ScriptableObject icon.

    To be clear, there is a way around this. I have created custom tilemap manager scripts before that run alongside your code and append data to existing tiles. Or you can override the ScriptableObject in Debug mode and live without conveniences. My point is I don't think it should be this hard to extend an official Unity system.

    Here are the things that I would like to see:

    • Correct sprite previews in the project view for any object that inherits from Tilebase.
    • An intended way to change the script a tile is using in the inspector.
    • This would extend to Ruletiles, AnimatedTiles, etc, allowing me to choose per object what type the tile will instantiate as.
    • We do maintain any serialized data from inherited types, say, if we inherit from Tile rather than Tilebase.
     
    Last edited: Jan 19, 2023
  7. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    @CoraBlue

    Hi, thank you for your feedback! Just a few things to clarify:


    • We will see what we can do about this in a generic way. The Sprite preview would have to be retrieved from GetTileData, which may need special handling depending on any custom scripts.

    • We could create a tool that could help you in changing the type of a Tile Asset, which would be easier than changing the script of each Tile individually.

    Could you explain more about this? The RuleTile does derive from TileBase and implements the methods in TileBase for handling the Rules and generating the output. Could you how else you would like to extend the RuleTile for this case?
     
  8. CoraBlue

    CoraBlue

    Joined:
    Nov 16, 2017
    Posts:
    60
    That would be helpful. Multiobject editing in the inspector would be the preferred way. The option in the Preferences of the Tile Palette that's just 'default tile script' is less helpful because it applies to everything (I would also add that the custom code that allows custom tile scripts to show up there is really obscure).

    Sure. I apologize if I've been unclear. You've been super patient so I appreciate that.

    The scriptable object that is RuleTile only exposes Sprites to you in the Inspector. That is, you drag sprite objects into the rule. At runtime/editor validation, Ruletile, without the users ability to override, creates generic Tile objects using these sprites. If you want a RuleTile that creates DamageTile:Tile, or an AnimatedTile that creates CustomAnimated:Tile for example, that is currently impossible.

    The real issue is the effort that went into the friendly interface that Unity has created for these scripts. A user can theoretically create custom editor GUI for their own versions of RuleTiles or Animated Tiles, but this fractures codebases and can be inaccessible to less experienced Unity developers.

    Let me know if you have any other questions!
     
  9. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,514
    Yeah, extending RuleTileEditor is a big-time pain - and really most editor scripts. I'm not sure if there's a way around that.

    For my projects, which sounds somewhat similar to your situation ("custom tilemap manager"), I have a parallel data store for each cell with which I can lookup info like sounds or damaged tiles to swap to.

    To outline a different approach, what if TileBase (and all derived tiles like RuleTile) had a field TileAccessory, which is just an abstract ScriptableObject stub which the user can extend. This would make it easier to optionally associate, at editor-time, a tile with some extra data, like your example friction / luminence / footstep sound, etc. Since this is just a field, all the editor classes like RuleTileEditor could show this field so users don't need to change the editor class to add a bit of data. Instead, they can extend TileAccessory with their own class and create a instance of it.

    Code (CSharp):
    1. public class TileBase : ScriptableObject
    2. {
    3.    // all the normal stuff, plus...
    4.    [SerializeField] TileAccessory tileAccessory;
    5.    public TileAccessory GetTileAccessory() => tileAccessory;
    6. }
    7.  
    8. public abstract class TileAccessory : ScriptableObject
    9. {
    10.     // nothing here, up to user to populate child class
    11. }
    An implementation adds whatever data you want for a type you control:
    Code (CSharp):
    1. public class MyTileAccessory : TileAccessory
    2. {
    3.      public Sound footstepSound;
    4.      public float friction;
    5.      public float luminence;
    6. }
    And then usage can look like this:
    Code (CSharp):
    1. // some user-specific use case
    2. void OnPlayerStepOnNewTile(Vector3Int playerCell)
    3. {
    4.      var tile = tilemap.GetTile(playerCell);
    5.  
    6.      // casting to resolve custom data particular to user
    7.      if(tile.GetTileAccessory() is MyTileAccessory mta)
    8.      {
    9.            var worldPos = CellToWorld(playerCell);
    10.            mta.footstepSound.Play(worldPos);
    11.      }
    12. }
    The benefit of this approach is that it would extend the versatility of tiles without breaking anything existing, and it would not require special user-made editor extensions.

    In contrast, the drawback is that you now have two scriptable objects - GrassTile and GrassTileAccessory - that you need to associate, but perhaps Unity could do something smart so the two objects appear as one within the Project, kind of like how TextMeshPro handles its fonts. Unity could even go so far as to automatically create an instance of your Default Tile Accessory class (specified in Edit > Preferences) for each new tile, and make it *appear* like you're modifying a single object within the editor.

    Something like a damageable tile could be done like this:
    Code (CSharp):
    1. public interface IDamageableTile // user-owned interface
    2. {
    3.     TileBase DamagedTile { get; }
    4. }
    5.  
    6. // implement interface on the accessory
    7. public class MyTileAccessory : ScriptableObject, IDamageableTile
    8. {
    9.      public Sound footstepSound;
    10.      public float friction;
    11.      public float luminence;
    12.      public TileBase damagedTile;
    13.  
    14.      public TileBase DamagedTile => damagedTile;
    15. }
    16.  
    17. // usage method...
    18. void OnDamage(Vector3Int cell)
    19. {
    20.      var tile = tilemap.GetTile(cell);
    21.  
    22.      // casting to resolve custom data particular to user
    23.      if(tile.GetTileAccessory() is IDamageableTile dt)
    24.      {
    25.            tilemap.SetTile(cell, dt.DamagedTile); // do swap        
    26.      }
    27. }
    28.  
    That may get less-experienced users further by removing the need to extend/modify the editor scripts to just associate a little data with each tile. Not a perfect solution though.
     
  10. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    249
    One of the problems with that approach is that the Tilemap doesn't currently serialize any fields of the Tile, just the tile reference and some info about color, transform and position for each type of tile (oversimplified but that's basically it). You can see this by creating a simple scene with a tilemap and just a few painted tiles then examining the scene file.

    The way I deal with it is to have the tile clone itself in StartUp. This creates a scriptable object instance rather than just a reference to a project folder asset. That clone gets saved in the scene. Works great and you can have whatever fields you like.
     
  11. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,514
    Yeah, friction/luminence/sound are appropriate for a *kind* of tile (grass vs dirt) but not as per-cell data, which is another reason a parallel data store (w/ an instance per cell) is often required for more advanced use cases. OR the cloning approach, as you suggest, which honestly I'm not a fan of b/c I'd rather own the data structure myself (and know its exact characteristics) rather than lookup into the tilemap.
     
  12. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    249
    The cloning approach allows you to keep all the serialized instance data of the tiles right in the scene file, and access it directly without accessing the Tilemap methods at all (unless you want to).

    Further, the data is specifically bound to that particular scene, so you don't have to keep track of what information is for a particular scene. That aspect of it is (IMO) superior to an external data store. Load the scene and the data is just there.

    To me, the data is going to take up memory wherever you put it.
     
  13. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    Thanks for clarifying this. We will try to look into making RuleTileEditor capable of showing custom data better or at least easier to extend to do so. I would assume that the basic property fields for the custom data would work by default, otherwise the Editor itself would have been extended.

    I added a script TileAssetConverter.cs which would allow you to drag and drop multiple Tile Assets and convert their Types to other Tiles. Only properties with the same type and name would be maintained right now.

    Thanks everyone for the input! Let me know this is what you are looking for, or if you have more suggestions for improvement!
     
    CoraBlue likes this.
  14. CoraBlue

    CoraBlue

    Joined:
    Nov 16, 2017
    Posts:
    60


    This is a really nice gesture and hopefully a sign of improvements to come. I'm able to convert things back to the default Tile type and I'm also able to batch convert several tiles at once. TypeCache.GetTypesDerivedFrom<T> is handy, I'll have to remember that.

    I did run into some kind of caching issue with the array after awhile that broke the tool, but I wasn't able to reproduce it and restarting the editor fixed it. If I learn more about it I'll let you know.

    Obviously this doesn't fix the issues with Ruletiles etc, but for everyone using the ScriptableObject switch hack you've saved us hours of time and tons of frustration. I can sense how crazy it must be over there, so the fact you took time to do this is incredibly appreciated.