Search Unity

Bug Jumbled up coordinates in tile matching game

Discussion in 'Scripting' started by nebuchednezar, Mar 10, 2023.

  1. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    Hello i'm making a tile matching game which is very similar to the mobile game Toon Blast. The rules are simple when you click on two or more tiles with the same color, you destroy those tiles and new ones fall from top. I achieved to a large extent but there's one annoying glitch which i can't manage to fix so far. The script recognizes the adjacent tiles according to their coordinates. It's a 5x6 grid(6 rows, 5 coloums) Coordinates are sorted from bottom to top in rows, left to right on coloumns like in ths screenshot. The coordinates are perfectly correct when the tiles are first instantiated however as more tiles are getting blasted and new ones fall from top. The coordinates start to mix up and the script don't recognize those tiles as adjacent even though they look adjacent in game therefor and i can't blast those tiles which breaks the core game mechanic. And after some testing i noticed that only the row coordinates get mixed up, coloumn coordinates are correct. This is the script that handles tile generation and assigning coordinates:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using System;
    5.  
    6. public class TileManager : MonoBehaviour
    7. {
    8.     public Sprite[] tileSymbolSprites, tileBackgroundSprites;
    9.     public List<TileController> tiles = new List<TileController>();
    10.     public Transform[] spawnPoints;
    11.     public GameObject tilePrefab;
    12.     public static TileManager instance;
    13.     public int rows;
    14.     private WaitForSeconds pacing = new WaitForSeconds(0.1f);
    15.  
    16.  
    17.     private void Awake()
    18.     {
    19.         if (instance != null)
    20.         {
    21.             Debug.LogWarning("More than one Tile Manager");
    22.         }
    23.         else
    24.         {
    25.             instance = this;
    26.         }
    27.     }
    28.  
    29.     public void StartGame()
    30.     {
    31.         StartCoroutine(GenerateTiles());
    32.     }
    33.  
    34.     internal void TileClicked(TileController tileController)
    35.     {
    36.         List<TileController> interestingNeighbours = new List<TileController>();
    37.         List<TileController> furtherExamination = new List<TileController>();
    38.         foreach (TileController tile in tiles)
    39.         {
    40.             if (tile.validNeighbours.Contains(tileController))
    41.             {
    42.                 interestingNeighbours.Add(tile);
    43.             }
    44.             else
    45.             {
    46.                 furtherExamination.Add(tile);
    47.             }
    48.         }
    49.         while (furtherExamination.Count > 0)
    50.         {
    51.             TileController checkingTile = furtherExamination[0];
    52.             furtherExamination.Remove(checkingTile);
    53.             bool addToInteresting = false;
    54.             foreach (TileController tile in interestingNeighbours)
    55.             {
    56.                 if (checkingTile.validNeighbours.Contains(tile))
    57.                 {
    58.                     addToInteresting = true;
    59.                     break;
    60.                 }
    61.  
    62.             }
    63.             if (addToInteresting) interestingNeighbours.Add(checkingTile);
    64.         }
    65.      
    66.         if(interestingNeighbours.Count > 0)
    67.         {
    68.             StartCoroutine(CheckChain(tileController, interestingNeighbours));
    69.         }
    70.     }
    71.  
    72.     private IEnumerator CheckChain(TileController tileController, List<TileController> directNeighbours)
    73.     {
    74.         GenerateTileAtColumn(tileController.tile.coordinates);
    75.         tiles.Remove(tileController);
    76.         Destroy(tileController.gameObject);
    77.         foreach (TileController tile in directNeighbours)
    78.         {
    79.                 GenerateTileAtColumn(tile.tile.coordinates);
    80.                 tiles.Remove(tile);
    81.                 if (tile != null) Destroy(tile.gameObject);
    82.                 //  yield return pacing;
    83.         }
    84.         GameplayUIController.instance.LowerRemainingMoves();
    85.         yield return null;
    86.     }
    87.  
    88.  
    89.  
    90.     private IEnumerator ReassignCoordinates()
    91.     {
    92.         yield return new WaitForEndOfFrame();
    93.         foreach (TileController tile in tiles)
    94.         {
    95.             int parentIndex = 0;
    96.             for (int i = 0; i < spawnPoints.Length; i++)
    97.             {
    98.                 if (spawnPoints[i] == tile.transform.parent)
    99.                 {
    100.                     parentIndex = i;
    101.                     break;
    102.                 }
    103.             }
    104.             tile.tile.coordinates = new Vector2Int(parentIndex, tile.transform.GetSiblingIndex());
    105.             tile.SetName(tile.tile.coordinates);
    106.         }
    107.     }
    108.  
    109.     private void GenerateTileAtColumn(Vector2Int coordinates)
    110.     {
    111.         GameObject tile = Instantiate(tilePrefab, spawnPoints[coordinates.x]);
    112.         int typeRandomized = UnityEngine.Random.Range(0, 4);
    113.         Tile tileData = new Tile(coordinates, (TileType)typeRandomized);
    114.         tile.GetComponent<TileController>().Initialize(tileData);
    115.         tiles.Add(tile.GetComponent<TileController>());
    116.         StartCoroutine(ReassignCoordinates());
    117.     }
    118.  
    119.     private IEnumerator GenerateTiles()
    120.     {
    121.         for (int i = 0; i < rows; i++)
    122.         {
    123.             for (int j = 0; j < spawnPoints.Length; j++)
    124.             {
    125.                 GameObject tile = Instantiate(tilePrefab, spawnPoints[j]);
    126.                 Vector2Int coordinates = new Vector2Int(j, i);
    127.                 int typeRandomized = UnityEngine.Random.Range(0, 4);
    128.                 Tile tileData = new Tile(coordinates, (TileType)typeRandomized);
    129.                 tile.GetComponent<TileController>().Initialize(tileData);
    130.                 tiles.Add(tile.GetComponent<TileController>());
    131.                 yield return pacing;
    132.             }
    133.         }
    134.         yield return null;
    135.     }
    136.  
    137.     public List<TileController> GetDirectNeighbours(TileController t)
    138.     {
    139.         List<TileController> neighbours = new List<TileController>();
    140.         for (int i = -1; i <= 1; i++)
    141.         {
    142.             for (int j = -1; j <= 1; j++)
    143.             {
    144.                 if (i == 0 && j == 0) continue;
    145.                 if (Mathf.Abs(i) == 1 && Mathf.Abs(j) == 1) continue;
    146.                 int checkX = t.tile.coordinates.x + j;
    147.                 int checkY = t.tile.coordinates.y + i;
    148.                 if (checkY <= 5 &&
    149.     checkX >= 0 &&
    150.     checkY >= 0 &&
    151.     checkX <= 4 &&
    152.     FindTileByCoordinates(new Vector2Int(checkX, checkY)).tile.type == t.tile.type
    153.     )
    154.                 {
    155.                     neighbours.Add(FindTileByCoordinates(new Vector2Int(checkX, checkY)));
    156.                 }
    157.             }
    158.         }
    159.         return neighbours;
    160.     }
    161.  
    162.     private TileController FindTileByCoordinates(Vector2Int coordinates)
    163.     {
    164.         foreach (TileController tile in tiles)
    165.         {
    166.             if (tile.tile.coordinates == coordinates)
    167.             {
    168.                 return tile;
    169.             }
    170.         }
    171.         return null;
    172.     }
    173. }
    174.  
    175.  
    I'm working on this for days and couldn't find a fix. I'd appreciate any support that you can give me regarding the issue. Thanks in advance
     

    Attached Files:

  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    The general process of debugging involves simplifying the data set to the smallest number of tiles that show off the problem, then instrumenting the code to see how it is not doing what you want.

    Here is how you can get started on your exciting new debugging adventure:

    You must find a way to get the information you need in order to reason about what the problem is.

    Once you understand what the problem is, you may begin to reason about a solution to the problem.

    What is often happening in these cases is one of the following:

    - the code you think is executing is not actually executing at all
    - the code is executing far EARLIER or LATER than you think
    - the code is executing far LESS OFTEN than you think
    - the code is executing far MORE OFTEN than you think
    - the code is executing on another GameObject than you think it is
    - you're getting an error or warning and you haven't noticed it in the console window

    To help gain more insight into your problem, I recommend liberally sprinkling
    Debug.Log()
    statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run? what order does it run in?
    - what are the values of the variables involved? Are they initialized? Are the values reasonable?
    - are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

    Knowing this information will help you reason about the behavior you are seeing.

    You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as
    Debug.Log("Problem!",this);


    If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

    You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

    You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

    You could also just display various important quantities in UI Text elements to watch them change as you play the game.

    If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: https://forum.unity.com/threads/how-to-capturing-device-logs-on-ios.529920/ or this answer for Android: https://forum.unity.com/threads/how-to-capturing-device-logs-on-android.528680/

    If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

    Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494

    When in doubt, print it out!(tm)

    Note: the
    print()
    function is an alias for Debug.Log() provided by the MonoBehaviour class.
     
  3. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    Thx for the reply. Allright first thing i'm doing here is is to narrow down the lines that have a potential to cause this issue. I'm already added a debug code in the script that shows the x,y coordinates of the tile i clicked. That's how i figured that the y axis gets mixed up when the new tiles are instantiated. Before i start to share my troubleshootihg process with you, i want to tell you that, this is not my script, i got this from a video tutorial i watched, i'm a beginner user of C# and this is a learning experience for me. So pls judge me accordingly. Now there's a method in this script named "ReassignCoordinates"

    Code (CSharp):
    1. private IEnumerator ReassignCoordinates()
    2.     {
    3.         yield return new WaitForEndOfFrame();
    4.         foreach (TileController tile in tiles)
    5.         {
    6.             int parentIndex = 0;
    7.             for (int i = 0; i < spawnPoints.Length; i++)
    8.             {
    9.                 if (spawnPoints[i] == tile.transform.parent)
    10.                 {
    11.                     parentIndex = i;
    12.                     break;
    13.                 }
    14.             }
    15.             tile.tile.coordinates = new Vector2Int(parentIndex, tile.transform.GetSiblingIndex());
    16.             tile.SetName(tile.tile.coordinates);
    17.         }
    18.     }
    This method assigns new coordinates to the newly instantiated tiles. This is an IEnumerator type method bcs we want to iterate the spawnPoints array in a certain order and in a certain period of time. Also at the top of the method there is yield return statement which makes the method run at the end of the frame. So i think what's happening here is that the timing of where the tile falls in an emptied spot and the time scripts assign new coordinates are different. To understand the situation better pls take a look at the ss. The replaced lemon tile gets the y coordinate of 5 but it is supposed to get 4. This show that ReassignCoordinates method is later than the time that the afore mentiones tile falls in place. So the real problem here is the synchronization to my understanding. Can you give any advice on how to fix this?
     

    Attached Files:

  4. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    The speed which the tiles fall are random so i don't think i'll be able to synchronize it with the script. The only way that i can think of here is creating trigger colliders which'll pass each tile the correct coordinates
     
  5. LethalGenes

    LethalGenes

    Joined:
    Jan 31, 2023
    Posts:
    69
    If the tile size is not uniform to a known scale then inaccuracy is inherited.

    for example

    certain scales are known to be reliable 4x 8x and 16x are more stable. This means 32+16 is more likely to be the whole integer result than it is to have a slight inaccuracy. All scales will break down at higher numbers and inherit a small inaccuracy when operation is performed. Some break down at lower numbers. It really depends whether you are following an order of scale or not.

    when I say scale here I do not mean vector scale, I mean pixel scale. If your unit per pixel is 1, then then your pixels scale to your vector. If your vector inherits an inaccuracy you use a controlled scale, or set the values using a = the result operator. This require you to know the result and perhaps apply it by list reference.

    An alternative is to round the numbers.
     
  6. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    Pixel size is not a determining factor here. Coordinates aren't assigned by triggering colliders in the current script. All tiles are instantiated from a prefab. All of them are same size, all sprites have 1 unit per pixel too. Thx for the reply but i don't think this is it. The problem is that the timing of the new tiles filling empty slots and the script assigning new coordinates to the tiles are not synchronized so the coordinates get mixed up sometimes
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    The main issue I see with the entire setup of the code above (and I understand that you got it from a tutorial... I am merely observing the potential issues!) is the TileController, where there is this
    .coordinates
    value.

    This approach opens up an entire class of complex bugs that simply don't exist traditionally in grid games: It becomes possible for a tile located at one position to actually contain coordinates indicating it is at another position.

    Similarly it becomes possible for a tile to incorectly claim its neighbor is actually some other tile.

    This approach means that 100% of ALL transactions throughout the code must respect and bookkeep these numbers perfectly, and if there is ever a bug, then things get really spooky-mysterious with tiles claiming they are somewhere they are not.

    The only benefit this confers is the ability to store all your tiles in a 1-dimensional List<T>, in this case tiles.

    Ideally you have a 2D array:

    Code (csharp):
    1. MyTileClass[,] tiles;
    You would initialize this to hold the desired size:

    Code (csharp):
    1. tiles = new MyTileClass[ across, down];
    and then fill out each cell at level generation (or load) time.

    Then you have a small class of helpers like "list my neighbors" or "check my neighbors" or "move me from position A to B" and nothing about that data is ever stored in the tile. The address of a tile becomes the point where it is in the grid.

    For an example of this process, check out my SpawningInAGrid scene:

    https://github.com/kurtdekker/makeg...ocGenStuff/SpawningInAGrid/SpawningInAGrid.cs

    Full setup in that repo. Line 155 is where it checks the computed new position to see if there's a tile already there and prevents player movement. That check can trivially be repurposed to do anything, such as matching or swapping or whatever the game calls for.
     
    nebuchednezar likes this.
  8. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    This is the TileController script i share this so maybe the cause of the issue would be more clear:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TileController : MonoBehaviour
    6. {
    7.     public SpriteRenderer symbolSpriteRenderer, backgroundSpriteRenderer;
    8.     public Tile tile;
    9.     public List<TileController> validNeighbours = new List<TileController>();
    10.  
    11.     public void Initialize(Tile tile)
    12.     {
    13.         this.tile = tile;
    14.         SetName(tile.coordinates);
    15.         symbolSpriteRenderer.sprite = TileManager.instance.tileSymbolSprites[(int)tile.type];
    16.         backgroundSpriteRenderer.sprite = TileManager.instance.tileBackgroundSprites[(int)tile.type];
    17.         StartCoroutine(CheckNeighborsDelayed());
    18.     }
    19.  
    20.     private IEnumerator CheckNeighborsDelayed()
    21.     {
    22.         yield return new WaitForSeconds(1);
    23.         validNeighbours = TileManager.instance.GetDirectNeighbours(this);
    24.     }
    25.  
    26.     public void SetName(Vector2Int tileCoordinates)
    27.     {
    28.         name = string.Format("Tile: {0}, {1}", tile.coordinates.x.ToString(), tile.coordinates.y.ToString());
    29.         StartCoroutine(CheckNeighborsDelayed());
    30.     }
    31.  
    32.     private void OnMouseDown()
    33.     {
    34.         TileManager.instance.TileClicked(this);
    35.         Debug.Log("Clicked on tile" + tile.coordinates.ToString());
    36.     }
    37.  
    38.     private void OnDestroy()
    39.     {
    40.         if (tile.coordinates.y <= 5)
    41.         {
    42.             GameplayUIController.instance.OnTileDestroyed(tile.type);
    43.             MusicManager.instance.PlayTileSound(tile.type);
    44.         }
    45.     }
    46.  
    47.     private void OnCollisionEnter(Collision collision)
    48.     {
    49.         tile.coordinates = new Vector2Int(tile.coordinates.x, transform.GetSiblingIndex());
    50.         if (transform.GetSiblingIndex() > 5) Destroy(gameObject);
    51.         SetName(tile.coordinates);
    52.     }
    53. }
    54.  
     
  9. nebuchednezar

    nebuchednezar

    Joined:
    Feb 6, 2021
    Posts:
    31
    So you're saying that the ideal way to make a match tile game is to create a grid with a 2D Array and store the tile coordinates there instead of the tiles right? I watched some videos on youtube where they are doing it with the method you are suggesting. I'll give it a try but i feel like i'll have to build the project from scratch maybe that would be the best anyway
     
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    Close! Take it one step further... what I am suggesting is that the coordinates are never "stored" per se: they are the x,y of the particular cell in the 2D array.

    With this approach the only remaining step is to ensure the GameObject itself gets moved to the right place in the scene to match where it is in the 2D array.

    This is why grid games are so simple to start with: with correct logic you could never have two tiles in the same cell, assuming they always check potential cells before moving to them.

    The coordinates would never be "stored" more than transiently as you process moving, etc.

    It might be easiest to at least study a complete setup (such as the one I linked above or one of those tutorials) until you understand what is going on, and then decide how to refactor your existing stuff.

    Doing an "each tile tracks its position" situation CAN be done, don't get me wrong, but it relies on every step and transaction being perfect at all times, and any error can become extremely mysterious to track down, which is why the "grid as authority" method is generally simpler.
     
    nebuchednezar likes this.