2D Object movement problem need help

Hi, I started using Unity couple days ago and I am trying to re-create an old game for learning the program. I’ve got everything covered except the boulders movement. There is a video below about the level that I’m trying to re-create. Please help me about the boulders movement logic. Thanks.

If I had to guess the logic, it would be this:

If the cell underneath is empty, fall down.

Otherwise if a cell on the left or right is empty, along with the cell underneath that cell, then roll to that side and down.

1 Like

thanks for helping but how can I code this in C#

Well first of all you need to establish a grid system that you can use to place objects in the world, so start with that.

I would suggest building a GridManager class that knows how big the grid is, the size of a cell, and has functions that can give back information about which objects are in which cells.

This is not an easy task for a beginner, but I’ll post some code here to give you an idea of what I mean.

1 Like

This may be a lot of information to take in, but to make a grid based game with any level of organization, you’ll need to create classes like this to help you keep track of the grid.

If you need me to explain anything in more detail, or if this isn’t helpful to you due to the complexity, please let me know. Some people learn best with a full example, others get overwhelmed.

using UnityEngine;

public class GridManager : MonoBehaviour {

    // global (or "static") access to this class via "GridManager.Instance"
    public static GridManager Instance { get; private set; }

    // defining the data each grid cell will hold
    public class Cell {
        public Vector2 gridPosition; // grid coordinate
        public Vector2 worldPosition; // actual world position
        public GameObject obj; // object currently in the cell

        public bool IsOccupied {
            get {
                return obj != null;
            }
        }

        // constructor
        public Cell(Vector2 gridPosition, Vector2 worldPosition, GameObject obj) {
            this.gridPosition = gridPosition;
            this.worldPosition = worldPosition;
            this.obj = obj;
        }
    }

    // data to configure in the inspector
    public Vector2 cellSize = Vector2.one; // default to 1 by 1 cells
    public int gridWidth = 10;
    public int gridHeight = 10;

    // the core 2D array of grid cells
    private Cell[,] grid;

    // called once when this object loads
    private void Awake() {
        // make sure there's only one GridManager
        if(Instance == null) {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }

    private void Start() {
        // build the grid
        BuildGrid();
    }

    // show the grid in the scene
    private void OnDrawGizmos() {
        BuildGrid();
        DrawCells();
    }

    private void BuildGrid() {
        // create a 2D array using the grid size
        grid = new Cell[gridWidth, gridHeight];

        // make each horizontal cell
        for(int x = 0; x < gridWidth; x++) {
            // for each horizontal cell, make the vertical cells
            for(int y = 0; y < gridHeight; y++) {
                Vector2 gridPosition = new Vector2(x, y);

                // position in the world is the grid position * the cell size
                Vector2 worldPosition = Vector2.Scale(gridPosition, cellSize);

                // create an empty cell with position data (not an actual world object)
                grid[x, y] = new Cell(gridPosition, worldPosition, null);
            }
        }
    }

    private void DrawCells() {
        foreach(Cell cell in grid) {
            Gizmos.DrawWireCube(cell.worldPosition, cellSize);
        }
    }

    // ------- Here are some examples of functions you may have in a GridManager

    // checks if X and Y are within the grid size
    private bool GridPositionValid(int x, int y) {
        return grid.GetLength(0) > x && grid.GetLength(1) > y;
    }

    // gets an object from a given grid location
    public GameObject GetObject(int x, int y) {
        // if the position is valid
        if(GridPositionValid(x, y)) {
            // return the object in that grid position (could be null)
            return grid[x, y].obj;
        } else {
            Debug.LogErrorFormat("GetObject was given position: {0}, {1}, which is outside the grid.", x, y);
            return null;
        }
    }

    // checks if a given grid position is occupied
    public bool IsGridPositionOccupied(int x, int y) {
        if(GridPositionValid(x, y)) {
            return grid[x, y].IsOccupied;
        } else {
            // throw an error because the position is not valid
            Debug.LogErrorFormat("isOccupied was given position: {0}, {1}, which is outside the grid.", x, y);
            return false;
        }
    }

    // returns the world position of a cell at the given coordinates
    public Vector3 GetWorldPosition(int x, int y) {
        if(GridPositionValid(x, y)) {
            return grid[x, y].worldPosition;
        } else {
            Debug.LogErrorFormat("GetWorldPosition was given position: {0},{1} which is outside the grid.", x, y);
            return Vector2.zero;
        }
    }

    // round the position to the nearest grid position
    public Vector3 GetGridPosition(Vector3 position) {

        // using some math i found here: http://stackoverflow.com/questions/29557459/round-to-nearest-multiple-of-a-number
        int x = Mathf.FloorToInt(((position.x + cellSize.x * 0.5f) / cellSize.x) * cellSize.x);
        int y = Mathf.FloorToInt(((position.y + cellSize.y * 0.5f) / cellSize.y) * cellSize.y);

        if(GridPositionValid(x, y)) {
            return new Vector2(x, y);
        } else {
            Debug.LogErrorFormat("GetGridPosition was given position: {0}, which is outside the grid.", position);
            return Vector2.zero;
        }
    }
}
1 Like

Here’s an example of how you can use that class to create an object that locks to the grid when you drag it (you’ll get errors if you drag it outside the grid). There are definitely better ways to set this up. You could create a function called “SnapToGrid” or something, that does all this in one step given the mouse position.

using UnityEngine;

[RequireComponent(typeof(Collider2D))]
public class GridObject : MonoBehaviour {

    private void OnMouseDrag() {
        Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector2 gridPos = GridManager.Instance.GetGridPosition(mousePos);
        transform.position = GridManager.Instance.GetWorldPosition((int)gridPos.x, (int)gridPos.y);
    }
}

Hello again.Thanks for helping me. I tried to understand this script but you were right, it wasn’t an easy task for a beginner. So I watched couple tutorials and I made a grid system like this

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

public class LevelManager : MonoBehaviour {
    [SerializeField]
    private GameObject[] tile;

    public Dictionary<Point, TileManager> Tiles { get; set; }
    private Point bspawn;
    [SerializeField]
    private GameObject boulder;

    public float TileSize
    {
        get { return tile[0].GetComponent<SpriteRenderer>().sprite.bounds.size.x; }
    }
    // Use this for initialization
    void Start () {
        CreateLevel();
    }
   
    // Update is called once per frame
    void Update () {
       
    }
    private void CreateLevel()
    {
        Tiles = new Dictionary<Point, TileManager>();

        string[] mapData = ReadLevelText();

        int mapXSize = mapData[0].ToCharArray().Length;
        int mapYSize = mapData.Length;

        Vector3 maxTile = Vector3.zero;

        Vector3 worldStart = Camera.main.ScreenToWorldPoint(new Vector3(0, Screen.height));
        for (int y = 0; y < mapYSize; y++)
        {
            char[] newTiles = mapData[y].ToCharArray();

            for (int x = 0; x < mapXSize; x++)
            {
              PlaceTile(newTiles[x].ToString(),x,y, worldStart);
            }
        }

        maxTile = Tiles[new Point(mapXSize - 1, mapYSize - 1)].transform.position;

        Spawn();
    }

    private void PlaceTile(string tileType, int x, int y, Vector3 worldStart)
    {
        int tileIndex = int.Parse(tileType);

        TileManager newTile = Instantiate(tile[tileIndex]).GetComponent<TileManager>();
       
        newTile.Setup(new Point(x, y), new Vector3(worldStart.x + (TileSize * x), worldStart.y - (TileSize * y), 0));

        Tiles.Add(new Point(x, y), newTile);

    }
    private string[] ReadLevelText()
    {
        TextAsset bindData = Resources.Load("Level") as TextAsset;

        string data = bindData.text.Replace(Environment.NewLine, string.Empty);

        return data.Split('.');
    }
    private void Spawn()
    {
        bspawn = new Point(3,2);
        Instantiate(boulder, Tiles[bspawn].transform.position, Quaternion.identity);
    }
}

I also have a “public struct Point” script and a “Tile Script”. It works good, I can place the objects but I still can’t figure it out how to move the boulders like in the game itself. Can I do this with my script or what should I change?

From a programming standpoint, you don’t want the LevelManager to be responsible for how every single object behaves within the grid. It should handle the layout and hold all the data about the level, but ideally you would let each object define its own behavior, using the LevelManager as a tool to query about surrounding tiles and their contents.

What you have is a great start, but you’re missing a bit of key functionality that will drive object behaviors after spawning.

You’ll need to create a way for objects to move from tile to tile or be interacted with, and the Level Manager should update accordingly. This could be done for example by the object querying the LevelManager for if the next tile over is blocked, and if it’s not then the object will tell the LevelManager to move it to that tile. Or if an object gets destroyed, it should notify the LevelManager so it knows that that the tile is now empty, etc.

Perhaps all objects on the grid inherit from a base class of “GridObject” which handles basic movement on the grid and things common to all things on the grid by interfacing with the LevelManager. You could then create a Boulder class which inherits from GridObject, which has logic specific to boulders.

If I were to code this, I would give each tile an event that fires when their contents are updated (see UnityEvent - (UnityEngine.Events) ). Then I would have each boulder listen to that event for the relevant tiles around it (left, right, and down) and run a movement check using the logic from my first reply if those tiles become empty. Those event listeners would need to be updated any time the boulder is moved, as the relevant tiles have changed.

1 Like