Match3 style Game Tiles Overlap when moving to fast

,

Im attempting to make a match 3 puzzle style game similiar to puzzles and dragons
the issue is when i move to quickly it can sometimes force one tile into anothers spot
grid manager and scriptable object based grid

using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using TMPro;
using UnityEngine;
using UnityEngine.UIElements;

public class TestGridManager : MonoBehaviour
{
  
    public GridData grid;
    private GameObject selectedTile;
    private GameObject TransparentTile;
    private Vector2Int lastGridPosition;
    private Vector2Int  currentGridPosition;
   

    // Start is called before the first frame update
    void Start()
    {
        grid.Initialize();
        FillGrid();

        Debug.Log("test2");
    }

    void FillGrid()
    {
       
        for (int y = 0; y < grid.height; y++)
        {
            for (int x = 0; x < grid.width; x++)
            {
                Vector3 parentPosition = transform.position;
                grid.SetTileType(x, y, Random.Range(0, grid.tilePrefabs.Length));
                Vector3 localPosition = new Vector3(x * grid.tileSize, y * grid.tileSize, 0);
                Vector3 worldPosition = parentPosition + localPosition;
                GameObject tile = Instantiate(grid.tilePrefabs[grid.GetTileType(x,y)], worldPosition, Quaternion.identity, transform);
              
            }
        }
    }
     void Update()
    {
        HandleInput();
    }
    void HandleInput()
    {
        //grab tile,set nearest grid positon,
        //leave transparent tile at nearest grid position
        OnTileClick();
        //set nearest grid position,constrain to grid,swap tile logic, leave transparent tile at nearest grid position
        OnTileDrag();
        //remove after image,snap visual to nearest gridposition, handle matches
        OnTileRelease();
        //refill board
        //refillboard
    }
    void OnTileClick()
    {
        // if theres nothing in hand onclick we try to grab a tile
        if(selectedTile == null)
        {
            TryGrabTile();
        }
   
       
    }

    void TryGrabTile()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 mouseClickPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Collider2D hitCollider = Physics2D.OverlapPoint(mouseClickPosition);

            if (hitCollider != null)
            {
                selectedTile = hitCollider.gameObject;
                SetTileTransparency(selectedTile,0.5f);
                SetCurrentGridPosition(selectedTile.transform.position);
                lastGridPosition = currentGridPosition;
                FollowMouse(selectedTile);
                Vector3 tes = GetWorldPosition(currentGridPosition);
                Debug.Log($"worldPosition is{tes}");
            }
        }
    }
    void OnTileDrag()
    {
        if (selectedTile != null)
        {
            FollowMouse(selectedTile);
            SetCurrentGridPosition(selectedTile.transform.position);
           
            if (IsAdjacent(lastGridPosition, currentGridPosition))
            {
                // Check if the target grid position is not occupied
                if (!grid.IsTileOccupied(currentGridPosition.x, currentGridPosition.y))
                {
                    GameObject tileToSwap = GetTileAtPosition(currentGridPosition);
                    if (tileToSwap != null && tileToSwap != selectedTile)
                    {
                        MoveTile(tileToSwap);
                    }
                }
            }
        }
    }
    void SetTileTransparency(GameObject tile,float amount)
    {
        TransparentTile= tile;
        SpriteRenderer renderer = TransparentTile.GetComponent<SpriteRenderer>();
        if (renderer != null)
        {
            Color color = renderer.color;
            color.a = amount; // Set alpha to make it semi-transparent
            renderer.color = color;
        }
    }
    void SetCurrentGridPosition(Vector3 worldPosition)
    {
        if (selectedTile != null)
        {
            Vector3 localPosition = transform.InverseTransformPoint(worldPosition);
            int x = Mathf.RoundToInt(localPosition.x / grid.tileSize);
            int y = Mathf.RoundToInt(localPosition.y / grid.tileSize);
            Vector2Int newClosestGridPosition = new Vector2Int(x, y);
            Vector3 test = GetWorldPosition(lastGridPosition);
            if (newClosestGridPosition != currentGridPosition)
            {
                lastGridPosition = currentGridPosition;
                currentGridPosition = newClosestGridPosition;
                Debug.Log($"The last grid position was {test}, the nearest is: {currentGridPosition}");
            }
        }
    }
    bool IsAdjacent(Vector2Int pos1, Vector2Int pos2)
    {
        return (Mathf.Abs(pos1.x - pos2.x) == 1 && pos1.y == pos2.y) || (Mathf.Abs(pos1.y - pos2.y) == 1 && pos1.x == pos2.x);
    }
    GameObject GetTileAtPosition(Vector2Int gridPosition)
    {
        Vector3 worldPosition = GetWorldPosition(gridPosition);
        Collider2D hitCollider = Physics2D.OverlapPoint(worldPosition);
        return hitCollider != null ? hitCollider.gameObject : null;
    }
    void OnTileRelease()
    {
        if (Input.GetMouseButtonUp(0) && selectedTile !=null)
        {
            SnapToGrid(selectedTile, currentGridPosition);
            SetTileTransparency(selectedTile, 1);
            selectedTile = null;
            
        }
    }

   

    void MoveTile(GameObject Tile)
    {
        float step = 20 * Time.deltaTime;
        Vector3 targetGridPostion = GetWorldPosition(lastGridPosition);
        // move the tile to the last grid position animation wise
        Tile.transform.position = targetGridPostion;
        // update its location in the grid
        // snap it

    }
    Vector3 GetWorldPosition(Vector2Int gridPosition)
    {
        return transform.TransformPoint(new Vector3(gridPosition.x * grid.tileSize, gridPosition.y * grid.tileSize, 0));
    }
    void SnapToGrid(GameObject tile,Vector2Int SnapPosition)
    {
        //Vector2Int gridPosition = GetGridPosition(tile.transform.position);
        //convert the grids position to a relative position in the world and move the tile there
        Vector3 snappedPosition = GetWorldPosition(SnapPosition);
        tile.transform.position = snappedPosition;

    }
   
    void FollowMouse(GameObject obj)
    {
        Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        mousePosition.z = obj.transform.position.z; // Maintain the original z position

        // Calculate the grid boundaries based on the parent's position and scale
        float minX = transform.position.x;
        float maxX = transform.position.x + (grid.width-1)*grid.tileSize* transform.localScale.x;
        float minY = transform.position.y;
        float maxY = transform.position.y +(grid.height -1) * grid.tileSize * transform.localScale.y;

        // Clamp the mouse position to the grid boundaries
        mousePosition.x = Mathf.Clamp(mousePosition.x, minX, maxX);
        mousePosition.y = Mathf.Clamp(mousePosition.y, minY, maxY);

        obj.transform.position = mousePosition;
    }


}
using UnityEngine;



[CreateAssetMenu(fileName = "New Grid", menuName = "Grid/Grid Data", order = 1)]
public class GridData : ScriptableObject
{
    public int width;
    public int height;
    public float tileSize;
    public GameObject[] tilePrefabs;
    private int[,] gridData;

    public void Initialize()
    {
    
        gridData = new int[height, width];
    }
    public bool IsTileOccupied(int x, int y)
    {
        if (IsValidPosition(x, y))
        {
            return gridData[y, x] != 0;
        }
        return true;
    }
    public int GetTileType(int x, int y)
    {
        if (IsValidPosition(x, y))
        {
            return gridData[y, x];
        }
        return -1; // Invalid type
    }

    public void SetTileType(int x, int y, int type)
    {
        if (IsValidPosition(x, y))
        {
            gridData[y, x] = type;
        }
    }

    public bool IsValidPosition(int x, int y)
    {
        return x >= 0 && x < width && y >= 0 && y < height;
    }
}

I don’t see any coroutines or time-delay type stuff above so it’s unlikely to be time-related.

You probably just have a logic bug.

You also do not want to be using Unity collision stuff to find things in a grid-based game. That only serves to needlessly complicate the problem space without giving any actual benefit. Just keep your own grid and be done with it.

More reading:

Tile-based / grid-based 2D games: match3, tetris, chips challenge, rogue, etc:

For any tile-based game such as Match3 or Tetris or a grid-based Roguelike, do all the logical comparisons in your own data storage mechanism for the tiles, such as a 2D array of tiles.

Otherwise you needlessly bind your game logic into Unity objects and the Unity API, making it about 10x more complicated than it needs to be.

If you have no idea how to work with 2D arrays, hurry to some basic C# tutorials for the language portions of it, then look at any good tutorial that uses a 2D array

Here is my Match3 demo using this technique of storing data in a grid. Full source linked in game comments.

It stores all of its data in a 2D array:

PieceController[,] Board;

This allows for easy simple checking in code, not relying on anything like physics.

You should strive to use that pattern for all logic, then only use Unity to present to the user what is happening in the game logic.

Here’s some debugging notes:

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

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

I have a few questions about your implementation.

  1. When a tile is moved, do you ever update the gridData? It looks like SetTileType is only ever called during initialization.
  2. In OnTileDrag it looks like you are checking to see if the adjacent space has a tile so that you can swap that tile to the original position. But you are checking if that location is not occupied. Wouldn’t you want to see if there is a tile in the target location before calling MoveTile?