Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule Override Tile #67

Open
Krummelz opened this issue Jan 8, 2019 · 34 comments
Open

Rule Override Tile #67

Krummelz opened this issue Jan 8, 2019 · 34 comments

Comments

@Krummelz
Copy link

Krummelz commented Jan 8, 2019

Let's say I have a set of rules to paint walls with, in a top-down game. Now, I want to add windows to the walls at specific points. The windows would be one extra tile, and should take the place of a straight wall segment, where I paint it. I cannot add a window tile to the existing rules, because the rules for a straight segment, and a straight segment with a window, would be the same. So you would always get only one or the other.

I tried using the rule override tiles, overriding a straight segment with a windowed segment. When I now paint this on the same layer, to replace a straight wall segment, I expect a window to appear, and the wall tiles from the base rule should connect as they normally would, except the existing tiles don't connect to the new overridden tile. The rules don't work across both the base rules, and the overridden rules. It seems this is only useful for is creating another set of rules with the same tiles. The end result is that you have to use your override tiles somewhere else on your tile map.

Is there some way of doing this? There's no sample project that makes use of the Rule Override Tile, so I can't really tell, and from my experiments, it doesn't do what the name implies.

@edwardrowe
Copy link

edwardrowe commented Jan 8, 2019

I haven't used the RuleOverrideTile yet, but RuleTiles in general only look for copies of the same tile. It sounds like you are using two tiles that implement the same ruleset. Let's call them tile A and B. A has walls when Up and Down neighbors are matched, B has a window when they are matched.

Tilemap looks like this:
A A A
A B A
A A A

The thing is, as far as B is concerned, it doesn't have neighbors, because there are no tiles of B around it. So you'll get the Sprite for no matching neighbors on B, and "edges" from the neighboring A tiles.

Does that make sense? Rule Override Tile is designed to reuse rulesets on different tiles, not for alternate sprites within a ruleset.

For our project, we added what we call "Siblings" to the rule tile. It's a list of Tiles it considers as a match. A and B would be siblings, and you'd get the desired effect.

@Krummelz
Copy link
Author

Krummelz commented Jan 8, 2019

You're right, that is what I'm doing, and what you say makes sense. Your solution sounds like what I need.. Would you be willing to share the code?

@edwardrowe
Copy link

edwardrowe commented Jan 8, 2019 via email

@edwardrowe
Copy link

Here's a quick modification to RuleTile to do what I'm saying. You should be cautious about diverging from this repository too much, as it can make it hard to update. And this is actually pretty tricky to make work with Override tiles as it's a bit unclear how they should behave, so it only works for the base rule tile right now.

This required changes to both the RuleTile and RuleTileEditor scripts.

RuleTileWithSiblings.zip

@ChuanXin-Unity
Copy link
Collaborator

I have added a quick and hopefully correct example of Rule Override Tile and Custom Rule that may help with this at 2d-techdemos:
Unity-Technologies/2d-techdemos@e10431a

I will check out RuleTileWithSiblings as well!

@johnsoncodehk
Copy link
Contributor

@Krummelz For your case, this is an example of inheriting the RuleTile implementation:

[CreateAssetMenu]
public class MyTile : RuleTile {

    public bool isWall;

    public override bool RuleMatch(int neighbor, TileBase other) {

        if (neighbor == RuleTile.TilingRule.Neighbor.This)
        if (other is MyTile)
        if (this.isWall && (other as MyTile).isWall)
            return true;

        return base.RuleMatch(neighbor, other);
    }
}

@johnsoncodehk
Copy link
Contributor

@ChuanXin-Unity Thank you for this example, this is look good!

I found that comparing the methods of RuleTile and RuleOverrideTile is a bit complicated. I tried to add the m_OverrideSelf field to RuleOverrideTile to handle different connection situations.

I am not sure if this is better because it may become more difficult to understand. Can you look at it?

PR: #70

@ElnuDev
Copy link

ElnuDev commented Apr 19, 2020

Here's my script! It works like a charm. Based on @johnsoncodehk's script:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

[CreateAssetMenu(fileName = "New Sibling Rule Tile", menuName = "Tiles/Sibling Rule Tile")]
public class SiblingRuleTile : RuleTile
{
    public List<TileBase> siblings;

    public override bool RuleMatch(int neighbor, TileBase other)
    {
        if (siblings.Contains(other))
        {
            return true;
        }

        return base.RuleMatch(neighbor, other);
    }
}

I hope this gets added into 2d-extras, it is a much needed feature!

@gamercoon
Copy link

I ran into exactly the same problem. Your example scripts have helped me a lot! I found that @ElnuDev 's implementation is close, but had a few bugs when you're using both positive and negative neighbor rules. Here's my code, based on yours, fixing this behavior:

using System.Collections.Generic;
using UnityEngine.Tilemaps;

namespace UnityEngine
{
    [CreateAssetMenu(fileName = "New Sibling Rule Tile", menuName = "Tiles/Sibling Rule Tile")]
    public class SiblingRuleTile : UnityEngine.RuleTile
    {
        public List<TileBase> siblings;

        public override bool RuleMatch(int neighbor, TileBase other)
        {
            switch(neighbor)
            {
                case UnityEngine.RuleTile.TilingRule.Neighbor.This:
                    return (siblings.Contains(other) 
                        || base.RuleMatch(neighbor, other));
                case UnityEngine.RuleTile.TilingRule.Neighbor.NotThis:
                    return (!siblings.Contains(other)
                        && base.RuleMatch(neighbor, other));
            }
            return base.RuleMatch(neighbor, other);
        }
    }
}

@longtran2904
Copy link

Hey, I know this post is old but I had some questions (I used the example script in the wiki):

  1. How to change the icon in the inspector from 3 to some sprite like the default arrow or X
  2. When I drew, the tile update was slow and noticeable. How to make it faster and update instantly?

@ChuanXin-Unity
Copy link
Collaborator

ChuanXin-Unity commented Aug 25, 2020

  1. You would need to write a custom editor for your custom rule tile and override the following method below:

     /// <summary>
     /// Draws a neighbor matching rule
     /// </summary>
     /// <param name="rect">Rect to draw on</param>
     /// <param name="position">The relative position of the arrow from the center</param>
     /// <param name="neighbor">The index to the neighbor matching criteria</param>
     public virtual void RuleOnGUI(Rect rect, Vector3Int position, int neighbor)
     {
         switch (neighbor)
         {
             case RuleTile.TilingRule.Neighbor.This:
                 GUI.DrawTexture(rect, arrows[GetArrowIndex(position)]);
                 break;
             case RuleTile.TilingRule.Neighbor.NotThis:
                 GUI.DrawTexture(rect, arrows[9]);
                 break;
             default:
                 var style = new GUIStyle();
                 style.alignment = TextAnchor.MiddleCenter;
                 style.fontSize = 10;
                 GUI.Label(rect, neighbor.ToString(), style);
                 break;
         }
     }
    

What you currently see is the default option in the switch, where a label with selected rule is printed. You can change it to an icon you have created by adding a new case with GUI.DrawTexture.

  1. I am not certain what causes the slowness for your update. If possible, could you use the Unity profiler, record your tile update in editor mode and post the profiler data log here? This will help to determine what is causing the slow down for you.

@longtran2904
Copy link

@ChuanXin-Unity If I draw the custom tile everything is normal but if I draw the custom tile and then draw a normal tile (sibling tile to the custom one) it won't update until I hover my cursor to its position.
Here the file:
siblingTIle.zip

@johnsoncodehk
Copy link
Contributor

@longtran2904 RuleTile refresh can only be trigger by RuleTiles (RuleTile, OverrideRuleTile, CustomRuleTile...), "trigger by any tile" need Tilemap to add support.
At now, you can only replace "NonRuleTile" with RuleTile to avoid this problem.

@longtran2904
Copy link

@johnsoncodehk Most of the time, I would use RuleTile but in some special cases that the rules didn't apply, I had to use the normal tile (which messed up the rule tile so I created a custom sibling tile for it). So I had a custom rule tile and a normal tile. Are there any other ways?

@johnsoncodehk
Copy link
Contributor

johnsoncodehk commented Aug 26, 2020

@longtran2904 If you want to use normal tile, you need to inherit the normal tile class and override RefreshTile() to update RuleTile. Compared to this method, it is much simpler to switch to RuleTile.
You can talk about special cases to see if I can help.

@longtran2904
Copy link

@johnsoncodehk Does this problem only appear visually and when I in play mode or instantiate the tilemap from a prefab it will be disappeared? If it's true then I can live with it.

@johnsoncodehk
Copy link
Contributor

johnsoncodehk commented Aug 26, 2020

All tiles will be refreshed once when entering playback mode. Therefore, if you do not change the Tilemap at runtime, this problem will not occur.
The same problem occurs if the normal tile is placed at runtime.

@longtran2904
Copy link

@johnsoncodehk Thank you, I think I will just leave it there.

@johnsoncodehk
Copy link
Contributor

johnsoncodehk commented Aug 26, 2020

@ChuanXin-Unity Maybe we can consider refresh affected tiles by Tilemap, or create RuleTilemap for this problem?
I think tiles provide all affected positions, and refresh by Tilemap internal is better, but Tilemap is not open source, I cannot work for that.
And the efficiency problem mentioned by #163 can also be solved.

@longtran2904
Copy link

@johnsoncodehk Hey, when my game starts, it spawns some tilemaps (which have rule tiles) and then copies it into a shared tilemap so sometimes the tiles don't get updated again. How to call RefreshTile() for all the tiles through code?

@johnsoncodehk
Copy link
Contributor

@longtran2904 You can use Tilemap.RefreshAllTiles, But if the tilemap size is relatively large, you need to consider performance.

@longtran2904
Copy link

@johnsoncodehk Thank you! It worked. My game is composed of different rooms (which have different tilemaps) and I copy it at the start of the level only so it will be fine (max: 50,000 tiles).

@longtran2904
Copy link

longtran2904 commented Aug 27, 2020

@johnsoncodehk How the bool RuleMatch(int neighbor, TileBase tile) work? How the parameter neighbor gets passed? I want a rule which has a sibling tile in it and do I need to make another case for the switch (Neighbor.Sibling) or Neighbor.This is ok?

@johnsoncodehk
Copy link
Contributor

johnsoncodehk commented Aug 27, 2020

@longtran2904 Neighbor.Sibling and Neighbor.This is also work. If you use Neighbor.This, this is a very similar script: ExampleSiblingRuleTile.cs
And sorry, I can't use my English to say how RuleMatch() work...

@longtran2904
Copy link

longtran2904 commented Aug 27, 2020

@longtran2904 If the neighbor is a sibling then will the int neighbor equal to 1 or 3? If it is 3 then how the case Neighbor.This work (This == 1)? (I used the example script sibling 1 in the 2d-extras wiki)

@johnsoncodehk
Copy link
Contributor

@longtran2904 Neighbor.Sibling Is 3, Neighbor.This is 1 and progress in base.RuleMatch(neighbor, tile)

@longtran2904
Copy link

longtran2904 commented Aug 27, 2020

@johnsoncodehk I knew that, but when the RuleMatch gets called if the tile gets checked is a sibling then the int neighbor parameter is 1 or 3?

@johnsoncodehk
Copy link
Contributor

@longtran2904 The role of RuleMatch is not to judge the neighbor type. It is to check whether the rules in Tiling Rules match in order.
For example:
image
RuleTile will first determine whether A matches, so RuleMatch will be executed 4 times:

RuleMatch(This, GetOtherTile(0, 1));
RuleMatch(NotThis, GetOtherTile(-1, 0));
RuleMatch(NotThis, GetOtherTile(1, 0));
RuleMatch(NotThis, GetOtherTile(0, -1));

If all return true, the sprite of A will be displayed.
Otherwise, continue to determine whether B matches, so RuleMatch will be executed 5 times:

RuleMatch(This, GetOtherTile(0, 1));
RuleMatch(NotThis, GetOtherTile(1, 1));
RuleMatch(NotThis, GetOtherTile(-1, 0));
RuleMatch(This, GetOtherTile(1, 0));
RuleMatch(NotThis, GetOtherTile(0, -1));

If all return true, the sprite of B will be displayed.
Otherwise, the default sprite will be displayed.

@longtran2904
Copy link

Oh, I understand now. Thanks for replying!

@ChuanXin-Unity
Copy link
Collaborator

@ChuanXin-Unity Maybe we can consider refresh affected tiles by Tilemap, or create RuleTilemap for this problem?
I think tiles provide all affected positions, and refresh by Tilemap internal is better, but Tilemap is not open source, I cannot work for that.
And the efficiency problem mentioned by #163 can also be solved.

For this issue, perhaps the Tilemap could have a default refresh radius parameter that users can specify if TileBase.RefreshTile has not been overridden instead of just the current cell. This is only useful if there is a mix of different types of Tiles with different Refresh ranges.

Alternatively, the base RefreshTile of the normal Tile needs to have its RefreshTile range increased to include all of the neighbours, same as the Rule Tile used.

@johnsoncodehk
Copy link
Contributor

@ChuanXin-Unity If it is implemented by normal tiles, AnimatedTile/PipelineTile/TerrainTile... and so on all need to be changed, which is not conducive to the user to create custom normal tiles, and the user may not know that in order to support the use with RuleTile, you need to override RefreshTile to add related logic .

Therefore, I recommend implementing it by Tilemap. The advantage of setting with radius is that it is easy to understand. It is feasible to convert the neighbor positions of RuleTile to radius and provide it to Tilemap, but performance may be a problem.
This is an extreme example that will cause the Tilemap refresh radius to be 10(100 box), but ideally it should only be refreshed by 1 box.

螢幕截圖 2020-08-28 下午3 25 22

@ChuanXin-Unity
Copy link
Collaborator

Yes, performance will be an issue if the radius is set unnecessarily. Having special cases such as the one as you set above also makes it unfeasible. The issue then would be how to track individual cases like this for refreshing, since the placement of a normal Tile can could trigger updates for Rule Tiles anywhere with extended neighbours.

@johnsoncodehk
Copy link
Contributor

johnsoncodehk commented Aug 28, 2020

@ChuanXin-Unity Fortunately RuleTile has already dealt with this problem and the way it works is:

  1. RuleTileA and RuleTileB each provide their own neighbor positions(Set_A, Set_B) (code)
  2. When TileMap uses RuleTileA and RuleTileB, merge Set_A and Set_B into the new neighbor positions set (Set_C). (code)
  3. When executing RefreshTile, base on refresh position, get the "other tile that may be affected" with the reverse position of Set_C. (code)
  4. Check the neighbor positions of the "other tile that may be affected", if the other tile is indeed affected, execute refresh other tile. (code)

Since the calculation method is based on Tilemap, it is easy to migrate to Tilemap.

@ChuanXin-Unity
Copy link
Collaborator

@johnsoncodehk

Thanks for pointing this out! It could be possible that the Tilemap has its own callback method for refreshing Tiles without the RefreshTile override that implements this for all types of Tiles (or some other way if needed) instead of the default radius. We would definitely check this out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants