Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Do Tilemap colors work at all?

Discussion in '2D' started by Problem-Machine, Jul 7, 2021.

  1. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Just spent a very frustrating day putting together a custom brush and brush editor for a special effect. The special effect relies on packing specific information into the tile's color channel to be sent to a custom shader, but trying to get custom tile colors working is maddening. With my custom brush editor, it draws the preview as white regardless of intended source color and it furthermore overwrites whatever color I've already set to that tile, even when the preview gets cleared. If I disable the editor so I don't have the preview problem, it seems to draw every tile as white initially, then if I draw back over it again sets it to the correct color -- but if I undo any of these draws it sets all the tiles back to white! This really feels like this seemingly extremely basic feature is largely unimplemented internally, while still having its interface exposed to users. I wasn't sure whether to list this as a bug or a request for help, because I can't even tell if I missed some non-obvious function I had to have in place to ensure it doesn't do this!

    Is what I'm trying to do even possible? Am I just wasting my time in trying to do anything useful whatsoever with this tile system?

    Also, unrelated to this current issue, I have noticed that Tilemap's floodfill method neither allows setting of tile transforms nor tile colors, and also doesn't return any info about which tiles it fills, making it all but useless for any custom tilebrush -- yet still seemingly requiring custom implementation.
     
  2. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    One gotcha could be tiles w/o the correct TileFlags or that somehow reverse what you intend to set externally within GetTileData. Have you double-checked this?
     
  3. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    I've tried setting and unsetting the lock flags in every instance where colors are set in a few different ways. The whole tile flags thing doesn't seem very well-documented so it's been a bit trial and error, but I can get it to set the color -- it's getting it to stay set, and getting it to work in the previews, that seem to be impossible. The whole set of SetEditorPreview* methods don't seem to have anything related to tile locking and SetEditorPreviewColor seems to just do... absolutely nothing.
     
  4. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    Looking over my custom brushes where I set colors, I notice I never use SetEditorPreview* methods. I can't remember why this is, but it might be for the problems you've encountered here.

    As a workaround, if you make use of
    Code (CSharp):
    1. Undo.RecordObject(tilemap, "Set Color");
    You may find that editor previews aren't necessary. Undo the changes if it's wrong.
     
  5. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    The OP:
    I realize my suggestion might not work for you if this is the case. I'm not sure why this would be the case. This persists if you remove the SetEditorPreview* calls, correct? If so, that makes me suspect a scripting error, either with the brush or the tile.
     
  6. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    Simple test case - does this work? Works for me.
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using UnityEditor;
    5. using UnityEditor.Tilemaps;
    6. using UnityEngine;
    7. using UnityEngine.Tilemaps;
    8.  
    9. [CustomGridBrush(true, false, false, "SimpleColorBrush")]
    10. public sealed class SimpleColorBrush : GridBrushBase
    11. {
    12.     public Color color;
    13.  
    14.     /// <summary>
    15.     /// Tints tiles into a given position within the selected layers.
    16.     /// The TintBrush overrides this to set the color of the Tile to tint it.
    17.     /// </summary>
    18.     /// <param name="gridLayout">Grid used for layout.</param>
    19.     /// <param name="brushTarget">Target of the paint operation. By default the currently selected GameObject.</param>
    20.     /// <param name="position">The coordinates of the cell to paint data to.</param>
    21.     public override void Paint(GridLayout grid, GameObject brushTarget, Vector3Int position)
    22.     {
    23.         // Do not allow editing palettes
    24.         if (brushTarget.layer == 31)
    25.             return;
    26.  
    27.         Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    28.         if (tilemap != null)
    29.         {
    30.             Undo.RecordObject(tilemap, "Color Paint");
    31.             // does NOT consider flags, always colors
    32.             tilemap.SetColor(position, color);
    33.         }
    34.     }
    35.  
    36.     public override void FloodFill(GridLayout gridLayout, GameObject brushTarget, Vector3Int seed)
    37.     {
    38.         Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    39.         if (tilemap != null)
    40.         {
    41.             // *** NOTE :  only works if targets have all the same tint ***
    42.             // *** That's b/c if perlin-noise'd, color differs per cell ***
    43.  
    44.             TileBase targetTile = tilemap.GetTile(seed);
    45.             if (targetTile == null) return;
    46.             Undo.RecordObject(tilemap, "Color Bucket");
    47.  
    48.             Stack<Vector3Int> cells = new Stack<Vector3Int>();
    49.             HashSet<Vector3Int> visited = new HashSet<Vector3Int>();
    50.             Color targetColor = tilemap.GetColor(seed);
    51.  
    52.             cells.Push(seed);
    53.  
    54.             while (cells.Count > 0)
    55.             {
    56.                 Vector3Int c = cells.Pop();
    57.                 if (visited.Add(c) && tilemap.GetTile(c) == targetTile && tilemap.GetColor(c) == targetColor)
    58.                 {
    59.                     // does NOT consider flags, always colors
    60.                     tilemap.SetColor(c, color);
    61.                     cells.Push(new Vector3Int(c.x - 1, c.y, 0));
    62.                     cells.Push(new Vector3Int(c.x + 1, c.y, 0));
    63.                     cells.Push(new Vector3Int(c.x, c.y - 1, 0));
    64.                     cells.Push(new Vector3Int(c.x, c.y + 1, 0));
    65.                 }
    66.             }
    67.         }
    68.     }
    69.  
    70.     public override void BoxFill(GridLayout gridLayout, GameObject brushTarget, BoundsInt position)
    71.     {
    72.         Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    73.         if (tilemap != null)
    74.         {
    75.             Undo.RecordObject(tilemap, "Color Box Fill");
    76.             foreach (var c in position.allPositionsWithin)
    77.             {
    78.                 TileBase tile = tilemap.GetTile(c);
    79.                 if (tile != null)
    80.                 {
    81.                     // does NOT consider flags, always colors
    82.                     tilemap.SetColor(c, color);
    83.                 }
    84.             }
    85.         }
    86.     }
    87.  
    88.     public override void BoxErase(GridLayout gridLayout, GameObject brushTarget, BoundsInt position)
    89.     {
    90.         Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    91.         if (tilemap != null)
    92.         {
    93.             Undo.RecordObject(tilemap, "Color Box Erase");
    94.             Color white = Color.white;
    95.             foreach (var c in position.allPositionsWithin)
    96.             {
    97.                 TileBase tile = tilemap.GetTile(c);
    98.                 if (tile != null)
    99.                     tilemap.SetColor(c, white);
    100.             }
    101.         }
    102.     }
    103.  
    104.     /// <summary>
    105.     /// Resets the color of the tiles in a given position within the selected layers to White.
    106.     /// The TintBrush overrides this to set the color of the Tile to White.
    107.     /// </summary>
    108.     /// <param name="gridLayout">Grid used for layout.</param>
    109.     /// <param name="brushTarget">Target of the erase operation. By default the currently selected GameObject.</param>
    110.     /// <param name="position">The coordinates of the cell to erase data from.</param>
    111.     public override void Erase(GridLayout grid, GameObject brushTarget, Vector3Int position)
    112.     {
    113.         // Do not allow editing palettes
    114.         if (brushTarget.layer == 31)
    115.             return;
    116.  
    117.         Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    118.         if (tilemap != null)
    119.         {
    120.             Undo.RecordObject(tilemap, "Color Erase");
    121.             tilemap.SetColor(position, Color.white);
    122.         }
    123.     }
    124. }
    125.  
    126. /// <summary>
    127. /// The Brush Editor for a Tint Brush.
    128. /// </summary>
    129. [CustomEditor(typeof(SimpleColorBrush))]
    130. public sealed class SimpleColorBrushEditor : GridBrushEditorBase
    131. {
    132.     /// <summary>Returns all valid targets that the brush can edit.</summary>
    133.     /// <remarks>Valid targets for the TintBrush are any GameObjects with a Tilemap component.</remarks>
    134.     public override GameObject[] validTargets => GameObject.FindObjectsOfType<Tilemap>().Select(x => x.gameObject).ToArray();
    135.  
    136.     public override void OnInspectorGUI()
    137.     {
    138.         base.OnInspectorGUI();
    139.     }
    140. }
    141. #endif
     
    Last edited: Jul 7, 2021
  7. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Alright the layer check against layer 31 ended up causing some issues because that's the same layer I've been using for the background elements I was trying to test on, but that works. When I modified it to draw a tile at the same time like the grid brush does, it generated the same problem I was having before, where it initially placed tiles as white and then only recolored them if I kept painting over them. After a bit more work I finally put it together that apparently whenever you draw a tile it resets the color and matrix of that cell, so the order of operations matters a lot. I might have done in this way in the first place, except that there are options to set multiple tiles but not multiple colors or transforms, so I just ended up setting those while I was iterating through the bounds to build a list of locations to change.

    It works now for placing a tile and setting it to the correct color, but having previews be completely broken is really quite unfortunate since much of why I started developing a brush for this in the first place was for getting better real-time feedback on a special effect shader that uses it. Still, this is good enough for the time being, thanks for your help.

    Also, I noticed in the process that while having the undo record in the brush works, it seems to take quite a while to perform the undo (perhaps it records every mouse update even if it doesn't change the object) -- I ended up going back to GridBrush's approach which records undos in the editor and it's much faster.
     
  8. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    If changing several properties of a cell at once, you might consider (in newer Unity versions)
    tilemap.SetTile(TileChangeData tcd);
    This lets you change it all at once.
     
  9. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Oh wow thanks, that's very helpful. I've been version-locked at 2020.3 so I had no idea. It looks like that was added in 2021.2 and is still in beta, but I'll keep an eye on it.
     
  10. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    Hi, I am not certain about where you are at with this, but if you need more help, do post it here!

    You could try making use of the
    Tilemap.tilemapTileChanged
    (https://docs.unity3d.com/2020.3/Doc...ence/Tilemaps.Tilemap-tilemapTileChanged.html) callback to help you identify which Tiles have been changed with the Floodfill method.

    The SetEditorPreview* methods rely on having called SetEditorPreviewTile first. These do not change the values of the underlying Tile called with SetTile, so you will not see any changes with the original Tile if you are trying to set the EditorPreviewColor only.

    You could remove the layer check for 2020.3. This was originally added for versions prior to 2020.1 for handling the Tile Palette.
     
  11. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    This may be the intended behavior, but testing out a version that calls SetEditorPreviewTile and then SetEditorPreviewColor still doesn't seem to work, always drawing a white tile and, moreover, overwriting the intended underlying color.

    The issue with undo erasing all color information seems to occur completely independently of any of my code. I tested it using the standard GridBrush and the above SimpleColorBrush, modified slightly to remove the layer check and unlock colors before painting (without which it wouldn't work). If I draw with GridBrush, then color with SimpleColorBrush, then draw more with GridBrush and undo that most recent draw it also sets all previously colored tiles back to white.
     
  12. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    It seems like the changes for the SimpleColorBrush may not be recorded correctly? Would it be possible to share the code for your SimpleColorBrush and how you want it to work?

    The GridBrush's behaviour would be to set the color of its Painted Tiles based on its picked selection, which by default would be Color.white. This would be likely what the Undo has reverted to?
     
  13. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    The code is largely the same as above, but this is the slightly modified version I used for this test:
    Code (CSharp):
    1.     #if UNITY_EDITOR
    2.     using System.Collections.Generic;
    3.     using System.Linq;
    4.     using UnityEditor;
    5.     using UnityEditor.Tilemaps;
    6.     using UnityEngine;
    7.     using UnityEngine.Tilemaps;
    8.    
    9.     [CustomGridBrush(true, false, false, "SimpleColorBrush")]
    10.     public sealed class SimpleColorBrush : GridBrushBase
    11.     {
    12.         public Color color;
    13.    
    14.         /// <summary>
    15.         /// Tints tiles into a given position within the selected layers.
    16.         /// The TintBrush overrides this to set the color of the Tile to tint it.
    17.         /// </summary>
    18.         /// <param name="gridLayout">Grid used for layout.</param>
    19.         /// <param name="brushTarget">Target of the paint operation. By default the currently selected GameObject.</param>
    20.         /// <param name="position">The coordinates of the cell to paint data to.</param>
    21.         public override void Paint(GridLayout grid, GameObject brushTarget, Vector3Int position)
    22.         {
    23.    
    24.             Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    25.             if (tilemap != null)
    26.             {
    27.                 Undo.RecordObject(tilemap, "Color Paint");
    28.                 // does NOT consider flags, always colors
    29.                 tilemap.RemoveTileFlags(position, TileFlags.LockColor);
    30.                 tilemap.SetColor(position, color);
    31.                 tilemap.AddTileFlags(position, TileFlags.LockColor);
    32.             }
    33.         }
    34.    
    35.         public override void FloodFill(GridLayout gridLayout, GameObject brushTarget, Vector3Int seed)
    36.         {
    37.             Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    38.             if (tilemap != null)
    39.             {
    40.                 // *** NOTE :  only works if targets have all the same tint ***
    41.                 // *** That's b/c if perlin-noise'd, color differs per cell ***
    42.    
    43.                 TileBase targetTile = tilemap.GetTile(seed);
    44.                 if (targetTile == null) return;
    45.                 Undo.RecordObject(tilemap, "Color Bucket");
    46.    
    47.                 Stack<Vector3Int> cells = new Stack<Vector3Int>();
    48.                 HashSet<Vector3Int> visited = new HashSet<Vector3Int>();
    49.                 Color targetColor = tilemap.GetColor(seed);
    50.    
    51.                 cells.Push(seed);
    52.    
    53.                 while (cells.Count > 0)
    54.                 {
    55.                     Vector3Int c = cells.Pop();
    56.                     if (visited.Add(c) && tilemap.GetTile(c) == targetTile && tilemap.GetColor(c) == targetColor)
    57.                     {
    58.                         // does NOT consider flags, always colors
    59.                         tilemap.SetColor(c, color);
    60.                         cells.Push(new Vector3Int(c.x - 1, c.y, 0));
    61.                         cells.Push(new Vector3Int(c.x + 1, c.y, 0));
    62.                         cells.Push(new Vector3Int(c.x, c.y - 1, 0));
    63.                         cells.Push(new Vector3Int(c.x, c.y + 1, 0));
    64.                     }
    65.                 }
    66.             }
    67.         }
    68.    
    69.         public override void BoxFill(GridLayout gridLayout, GameObject brushTarget, BoundsInt position)
    70.         {
    71.             Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    72.             if (tilemap != null)
    73.             {
    74.                 Undo.RecordObject(tilemap, "Color Box Fill");
    75.                 foreach (var c in position.allPositionsWithin)
    76.                 {
    77.                     TileBase tile = tilemap.GetTile(c);
    78.                     if (tile != null)
    79.                     {
    80.                         // does NOT consider flags, always colors
    81.                         tilemap.SetColor(c, color);
    82.                     }
    83.                 }
    84.             }
    85.         }
    86.    
    87.         public override void BoxErase(GridLayout gridLayout, GameObject brushTarget, BoundsInt position)
    88.         {
    89.             Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    90.             if (tilemap != null)
    91.             {
    92.                 Undo.RecordObject(tilemap, "Color Box Erase");
    93.                 Color white = Color.white;
    94.                 foreach (var c in position.allPositionsWithin)
    95.                 {
    96.                     TileBase tile = tilemap.GetTile(c);
    97.                     if (tile != null)
    98.                         tilemap.SetColor(c, white);
    99.                 }
    100.             }
    101.         }
    102.    
    103.         /// <summary>
    104.         /// Resets the color of the tiles in a given position within the selected layers to White.
    105.         /// The TintBrush overrides this to set the color of the Tile to White.
    106.         /// </summary>
    107.         /// <param name="gridLayout">Grid used for layout.</param>
    108.         /// <param name="brushTarget">Target of the erase operation. By default the currently selected GameObject.</param>
    109.         /// <param name="position">The coordinates of the cell to erase data from.</param>
    110.         public override void Erase(GridLayout grid, GameObject brushTarget, Vector3Int position)
    111.         {  
    112.             Tilemap tilemap = brushTarget.GetComponent<Tilemap>();
    113.             if (tilemap != null)
    114.             {
    115.                 Undo.RecordObject(tilemap, "Color Erase");
    116.                 tilemap.SetColor(position, Color.white);
    117.             }
    118.         }
    119.     }
    120.    
    121.     /// <summary>
    122.     /// The Brush Editor for a Tint Brush.
    123.     /// </summary>
    124.     [CustomEditor(typeof(SimpleColorBrush))]
    125.     public sealed class SimpleColorBrushEditor : GridBrushEditorBase
    126.     {
    127.         /// <summary>Returns all valid targets that the brush can edit.</summary>
    128.         /// <remarks>Valid targets for the TintBrush are any GameObjects with a Tilemap component.</remarks>
    129.         public override GameObject[] validTargets => GameObject.FindObjectsOfType<Tilemap>().Select(x => x.gameObject).ToArray();
    130.    
    131.         public override void OnInspectorGUI()
    132.         {
    133.             base.OnInspectorGUI();
    134.         }
    135.     }
    136.     #endif
    137.  
    I just tested in a different scene to be absolutely sure there was no other code active, since I do have tile change listeners elsewhere that could hypothetically be interfering, but got the same results. To reproduce (in 2020.3) just create a tilemap, draw on it with the default GridBrush, use the SimpleColorBrush to paint color onto some of those tiles, draw elsewhere to completely different tiles with the GridBrush and undo the last Gridbrush draw. The colors will revert along with the undo. Similarly, you can see the preview bug by passing the cursor over the colored tiles: They will stay white after the cursor passes by.
     
  14. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    It looks like this line is causing the issues you are experiencing. You could remove it and try it out. Adding back TileFlags.LockColor would revert all changes to the Tile's color back to the values from the Tile Asset.

     
  15. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Commenting out this line does not resolve either of the mentioned bugs. Also, I'm still not clear on what the intended logic of these 'lock' flags is: Is it documented anywhere in any detail beyond what's at https://docs.unity3d.com/ScriptReference/Tilemaps.TileFlags.html ? "Lock" could logically mean either making user/brush changes take priority over tilemap defaults or the exact opposite. The linked documentation says "TileBase locks any color set by brushes or the user." -- wouldn't that suggest that I should set it after changing the color, as in the above brush? Right now I don't even have a strong conception of what it's supposed to be doing, and I just end up shuffling around lock and unlock statements until it works.

    Regardless, as said, commenting out line 31 doesn't change the above noted behavior.
     
  16. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    What do you get instead when painting with the brush after you have commented out the line? Do ensure that the changes are reflected on the Tilemap by using the Select Tool and validating it with the Inspector window.

    There is a little bit more detail about it at the manual page (https://docs.unity3d.com/2021.2/Documentation/Manual/Tilemap-ScriptableTiles-OtherClasses.html), but what it does is to lock the color values to that provided by the Tile asset. Eg. If your Tile returns the color red in Tile.GetTileData, this will be the color used to tint the Tile after any changes (Editor Preview, Undo etc) rather than the value set using Tilemap.SetColor.
     
  17. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Commenting out the line changes the behavior in no way I can observe. Checking it with the inspector confirms that the tiles do erroneously have their color reverted to white.
    I made a quick recording demonstrating this behavior. The description notes the steps taken in the video, since the tile palette and cursor aren't visible.
     
  18. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068
    Thanks for the video! You are right, using the default brush and using the undo will clear out the changes done the TileFlags.LockColor, which will then revert the color changes. This is something that we will check out.

    To workaround this, it looks like you will need to change the TileFlags for your Tile Assets directly. For the standard Tile Asset, you can do so by:
    • Selecting the "settings" toggle for a Tile asset upload_2021-7-15_9-51-14.png
    • Switch to Debug
    • Change the Flags to None upload_2021-7-15_9-51-49.png
    If they are custom Tiles, you can set the TileFlags to None in Tile.GetTileData.
     

    Attached Files:

  19. Problem-Machine

    Problem-Machine

    Joined:
    Nov 12, 2013
    Posts:
    12
    Perfect, that resolves both issues. Thanks!
     
  20. ge01f

    ge01f

    Joined:
    Nov 28, 2014
    Posts:
    121
    Just to add onto this thread, for any that didnt get satisfaction from above, this worked for me:

    Code (CSharp):
    1.             tilemap.SetTile(pos, TileSet.Tiles[tileIndex]);
    2.             tilemap.SetTileFlags(pos, TileFlags.None);
    3.             tilemap.SetColor(pos, color);
    4.             tilemap.SetTileFlags(pos, TileFlags.LockColor);
    5.  
    I had to 1) Set the Tile. 2) Unset any flags. 3) Set the color 4) Lock the color again. Or everything would go white.

    It worked if I used very base colors like only Color.blue, but setting dynamic colors set everything back to white again. Totally insane, but this worked.
     
    thegreatzebadiah likes this.
  21. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,762
    That sounds like you are making your colors by using
    Color()
    with values of 0 to 255

    Review the differences between
    Color()
    and
    Color32()