How could I programmatically convert tiles to a new tile type that inherits from Tile?

Some context: I am currently generating a map in Editor mode by iterating through all of my scenes and copying the tiles from their tilemap into one super-map. But, I realized that - when it comes to displaying how much of the map the player has seen upon reloading - I will need to either give the supermap tiles knowledge of which scene they came from, or I will need to load every scene that the player has visited whenever they load up the game and capture their tilemap into a lesser supermap. To me, it seems that the logical choice is to keep my single-generation supermap, but give the tiles knowledge of what scene they come from; this will allow me to get per-tile information and update accordingly.

I was able to successfully create tiles with this knowledge using the code below. However, when I opened up my Tilemap, I saw that under Info > Tiles, instead of my two tile types I saw a bunch of None (TileBase) entries; one for each tile in my map, presumably. It seems to me that this is not the proper way to convert existing tiles to my new tile type with the added knowledge. How should I go about it instead?

    public static void Add(this Tilemap tilemap, Tilemap source, Room thisRoom)
    {
        // Iterate through all positions within the bounds of the source tilemap
        BoundsInt bounds = source.cellBounds;
        foreach (Vector3Int position in bounds.allPositionsWithin)
        {
            TileBase tile = source.GetTile(position);
            if (tile != null)
            {
                MapTile mapTile = new MapTile((Tile)tile, thisRoom);

                // Set the tile in the destination tilemap

                //Convert from relative position to world position
                Vector2 worldPos = source.CellToWorld(position);
                //Convert from world position to local position of the new map
                tilemap.SetTile(tilemap.WorldToCell(worldPos), mapTile);
            }
        }
    }
    public MapTile(Tile tile, Room parentRoom)
    {
        sprite = tile.sprite;
        transform = tile.transform;
        flags = tile.flags;
        color = tile.color;
        colliderType = tile.colliderType;
        gameObject = tile.gameObject;
        name = tile.name;
        gameObject = tile.gameObject;
        this.parentRoom = parentRoom.gameObject.scene.name;
    }

This is really just a save data thing. This kind of information can be stored as pure data in the playerā€™s save game. Then your super map builds itā€™s representation off of that.

Well sure, I am saving that information. The problem is when it comes to generating the map to show the player: I can either start from the full super map, iterate through all the tiles, determine from the save state if they are from a seen room, and display them (or not) accordingly -

Or, I can re-create the map every time the player loads the game, loading up every scene theyā€™ve visited in order to capture its tilemap, then add every visited tilemap to the super-map. This seems far less efficient.

I suppose another alternative would be to, instead of combine my tilemaps into one super-map, add them as a series of gameobjects under the supermap parent grid with an associated scene name and choose only to show those which have been visited. Thatā€™s not the worst idea I guess, as long as having all those gameobjects wouldnā€™t be problematic. I think I remember reading a while back that we donā€™t want too many child objects in a single scene or somethingā€¦ Or maybe too deep of a child tree? I donā€™t recall.

There might also be other solutions I havenā€™t thought of? I donā€™t really think saving the supermap itself is a viable solution, but it would technically probably workā€¦

Hm. Still havenā€™t had any luck figuring this out. It looks like maybe I need to create a (Map) Tile Pallet and go back over my maps completely with the new Tile Pallet? Surely there must be some more convenient way to just update the tile type.

Well if MapTile is a scriptable object then you are instancing it incorrectly. Scriptable objects should be created with ScriptableObject.CreateInstance<T>() only. You probably want to write them to disk as well, as non-asset scriptable objects have the tendency to disappear.

Though that could possibly be a lot of scriptable objects, so you probably only want to make one when you donā€™t have an existing tile that meets that requirements already generated.

Huh, didnā€™t realize TileBase was a scriptable object. Okay so seems like the idea is to check to see if such an instance exists, and if not, create the instance and write it to disk. Hmā€¦ looking like writing to disk doesnā€™t have a Resources built in functionality, so I guess itā€™ll be a matter of defining a filepath for it which I guess makes sense since you can have multiple resources file. Was missing the CreateInstance() there, Iā€™ll give it a go and see what I can figure out

At runtime you canā€™t create assets (by which, I mean in a build). So if youā€™re pre-generating this, then you would make assets out of these scriptable objects. If this is happening at runtime, then you will need to work with in-memory instances, and just be sure to Destroy those instances when no longer needed.

Yeah, Iā€™m doing this in the Editor; just trying to convert my existing maps to use a new tile type with more self-awareness. I guess since this is a scriptable object, the different maps would each need their own tile type/SO specifically to denote that it came from their map. What Iā€™m looking into now is saving the SO asset in code before I go off testing it

Any handling of assets in the editor is generally done via the AssetDatabase class: Unity - Scripting API: AssetDatabase.CreateAsset

Got it! I was going down a similar, but different, rabbit hole haha this looks like it might solve my problem! Thanks!

Hm. Itā€™s pretty slow, presumably from trying to load from the asset database for every tileā€¦ Iā€™m not quite sure how to resolve that; I was thinking of creating a list of created/found MapTiles, but will need to figure out a way of comparing a tile to the maptile directly. I donā€™t know if C# has operator overloads but even so I think I would want to use the Contains() function, maybe with some assertionā€¦

But, I have another problem. Upon trying to generate the tiles, Iā€™m getting these errors:

Cannot open file ā€˜Assets/Tilemaps/MapTiles/Altar Room_PlatformPlacer.asset.metaā€™ for write.

Failed to write .meta file ā€˜Assets/Tilemaps/MapTiles/Altar Room_PlatformPlacer.asset.metaā€™

Presumably the second is due to the first, and the first is because the asset is still open or something?
Also, my created SOs donā€™t look like Tile objects with an added field, which is what I expected - in fact, it says their script is None (Mono Script), which seemsā€¦ wrong.

Also, all my tiles are turning various shades of pink. I believe I read that means that they donā€™t have a texture? But, my created assets do have textures, as well as assigned colors.

Hereā€™s the current codeā€¦

    public static MapTile ConvertToMapTile(this TileBase tile, Room room)
    {
        MapTile m = AssetDatabase.LoadAssetAtPath<MapTile>("Assets/Tilemaps/MapTiles/" + room.gameObject.scene.name + "_" + tile.name);

        if (m == null)
        {
            m = ScriptableObject.CreateInstance<MapTile>();
            m.EstablishMapTile((Tile)tile, room);

            AssetDatabase.CreateAsset(m, "Assets/Tilemaps/MapTiles/" + room.gameObject.scene.name + "_" + tile.name + ".asset");
        }
        return m;
    }

    public void EstablishMapTile(Tile tile, Room parentRoom)
    {
        sprite = tile.sprite;
        transform = tile.transform;
        flags = tile.flags;
        color = tile.color;
        colliderType = tile.colliderType;
        gameObject = tile.gameObject;
        name = tile.name;
        this.parentRoom = parentRoom.gameObject.scene.name;
    }

It seems that setting the transform to an individual tileā€™s transform is probably not correct. I basically just want to create a tile that inherits everything from the Tile SO thatā€™s passed in, but has one extra field of data, which I fill in.

Is your MapTile script in itā€™s own file? ScriptabeObject and MonoBehaviours need to have their own script files, and only one per file, with the class name matching the script asset name.

No it is not. Let me give that a go.

Well, at least the SOs are showing the script now, but itā€™s the strangest thing. I have a ā€œslopeā€ tile which, when I select the tile, shows that it has the SO properly included in the Tile property on the Grid; but when I select my other sprite (just a square right now), it shows the tile property as (Tile) without the scriptable object, and all the units are various shades of pink. I reset the tilemap, tooā€¦ itā€™s not like thereā€™s different code between them, either.

I got those errors again, too, but only for that tile type. So it sort of makes sense that that would be part of the issue. But why for that tile type and not the other? I only have one of the other tile currentlyā€¦

I think those errors are just because youā€™re trying to load an asset without checking if it actually exists or not.

Well, itā€™s creating that asset, but not using it for any of the related tiles. However, itā€™s also creating the other asset (and not throwing the error for that asset), and actually populating a tile with that asset - whereas the first asset is not getting populated into any tiles (presumably related to the error) and its tiles are all pink squares with a generic (Tile) in the Tile field, rather than the created SO - even though the SO is being created, despite the errors that imply that it is not.

Would need the entire callstacks for those errors. Otherwise I can only guess whatā€™s causing them.

Fair enough. I resolved the issue, though - I am not 100% sure what the cause was, but what I ended up doing was adding the created tiles to a list and retrieving from the list if the tileā€™s room and sprite matched up (which was kind of a pain to create a workaround for; for some reason you can downcast a tilebase to a tile, but not to a derivative of tile). I did this just to generate the map faster but the result is that the error is no longer getting thrown and the sprites are being properly populated. Could be that it was trying to load the asset too many times in succession, and I suspected an issue with async errors but looking through the code it doesnā€™t seem like there are any async functions encapsulating this function so I donā€™t know what happened there. Anyway, thanks a ton for your help!

1 Like