How do I create grid-based movement restrictions without physics?

I’m new to Unity and I’m trying to create a Pac-Man clone as a starting point to understand things better. I want to be able to have things move and interact with the world on a grid-based system just like the original game and I’m trying to do with without using box colliders and rigid bodies for the places that Pac-Man and the ghosts cannot go.

I previously did a roguelike tutorial for Pico-8, and basically it was really easy to draw a map using the various tiles in the sprite sheet, with some of them being designated “isPassable” and others not, so the game just knew, automatically, that you couldn’t walk on those. In Unity, I’m only finding this possible by generating physics collisions, and I’m just wondering if there’s some easier way to implement tiles that have properties, whatever they might be.

I did one tutorial for Pac-Man that used pysics and it created all sorts of weird movement situations that I would describe as messy, whereas the original game or even modern versions of things like it obviously have very clean and exact movements, if that makes sense. In one case, I turned down a corner too early and Pac-Man started hilarious spinning and tumbling because of the circle collider that was on him. In another, a square collider prevented anything but perfect movements instead of the couple-pixel leeway and “leaning into turns” that you can do on some versions, for example.

I’m considering taking this further to try randomly generated levels and the like, and it seems, at my current level of understanding, far more difficult to also have to do all this extra physics stuff, perhaps manually, once the levels are created in such a manner. That, and obviously old games didn’t use physics, so I’m trying to figure out what the heck would be the equivalent in Unity, or is it just not possible?

As a side note, most of the tutorials I’ve found have to do with more traditional grid movements, like old school RPGs, moving one tile at a time, rather than the continuous motion that something like Pac-Man has. I’m guessing that is something that could be tweaked in whatever movement script deals with the motion, but if there is some pitfall I’m inevitably going to run into with this line of thinking, I’d appreciate any forewarning there. Thanks!

@quadnine_damage

Well my Pac-Man clone is still on hold, but here are some things I figured out while doing it… maybe these will help you.

You can simply create a 2D array where you store walkable tiles and non-walkable tiles. These can be simple as some int values, not actual tilemap tiles.

You move your sprite and in parallel to that, keep book of its tile position, so you can query your grid for next allowed position. Pac-Man does it so, that you are basically just moving from center of one tile to the center of the next tile.

You also mentioned the “leaning into turns”, by which I guess you mean pre-turns and post-turns? That too is doable with this method, simply check you are close enough to target tile center, and allow taking next tile in user selected walkable direction as target tile and move towards its center. And then repeat.

Movement - Pac-Man is just the same as tile by tile RPG movement with smooth transition from tile to tile, only difference is, that there is no delay between moves, you simply glide from tile to tile.

Check out the Pac-Man dossier if you already haven’t it has many insights, not only relevant to Pac-Man.

1 Like

Could inherit from TileBase and add a simple Boolean for a collider.

Can you point to any practical example of the array that you are suggesting? I guess my main concern is how would I go from drawing a level by hand to having this array, or would that array have to be completely manual as well? (This is where it gets weird for me with thinking about something like randomly generating a level and then still needing this array as a separate entity.)

How would I handle navigation? Like, how do I tell it that a tile is nearby and therefore able to be considered for movement? I think I might be misunderstanding a bit how this gets implemented in a very general sense.

I think I have that dossier opened in one of the hundred tabs I have going for all this right now, I’ll give it a read. Thanks!

Could you explain this in a little more detail?

Sure thing. Not at Unity right now, but have a look at my pathfinding post:
https://discussions.unity.com/t/774085/3

It is implemented without using built-in physics.

I will update with a tile inheritance example.

1 Like

@quadnine_damage

“Can you point to any practical example of the array that you are suggesting?”

I think you should do reading outside of Unity docs, can’t remember any specific article or video, search for tile based movement, old flash, c++ or C# tutorials, as this is not Unity specific.

“I guess my main concern is how would I go from drawing a level by hand to having this array, or would that array have to be completely manual as well?”

Define “by hand”? Are you using tiles to draw your level? If so, you could draw your map in PS, and then turn it it into map data. One pixel black is a wall, one pixel of white is walkable. Then again, I don’t know how you think you will draw your level, but you must yourself consider how that is convertible to meaningful data you can use.

“something like randomly generating a level and then still needing this array as a separate entity”

In practice, you will need that data or more like you will generate that data, if you generate your level procedurally.

“How would I handle navigation”

Just like I described, but do some reading about tile movement first. If you are at 0,0 and want to move up, you check cell 0, 0+1 and check if that array element is wall, then you can’t move, if not, you can move to 0,1.

“I think I have that dossier opened in one of the hundred tabs”

I recommend again you read that, it explains most of the things I mentioned.

Despite of what I said above, here is an example of map data:

public static int[,] map =
{
    {1,1,1,1,1,1},
    {1,0,0,0,0,1},
    {1,0,1,1,0,1},
    {1,0,0,0,0,1},
    {1,1,1,1,1,1}
};

So it is a room with block in its center area. Remember, it is also upside down, as first row in code it the “bottom” row by default.

Then you could do the checks like this (simplified, you’ll have to prevent out of bounds queries):

bool CanMoveTo(Vector2Int pos)
{
    // do array boundary check here
    return map[pos.y, pos.x] != 1;
}

So if that cell in array is not 1 (wall) you get true, otherwise false.

1 Like

As mentioned above you should have a byte array that represents a grid that describes your terrain. Each value in the array represents a block type ( 0 = air, 1 = rock, 2 = water, etc). This will be your blueprint for the level, it can handle collisions and also represent graphics(tiling).

When you try to move you first test if the position is walkable by looking it up in the array, if it is you allow the movement to go through, if it isn’t then there’s a collision and the movement code doesn’t execute.

1 Like

Here is an example of tile inheritance. All of my tiles share some common fields that are part of a base class:

using UnityEngine.Tilemaps;

public abstract class AdvancedTileBase : TileBase
{
    public bool hasCollider = false;
    public bool colliderDoesNotCastShadows = false;
}

Any subsequent inheritance will get these fields. The important bit is hasCollider. This is a simple boolean value that is used for soft “physics”.

using General;
using UnityEngine;

[CreateAssetMenu(fileName = ".asset",menuName = Globals.CustomTilesBaseMenu + "Single Advanced Tile",order = 0)]
public class AdvancedTile : AdvancedTileBase {
}

To clarify, whenever I intend to do any grid-based movement, I use MapDataAccess to check tiles within all my tilemap layers through a DataAccess script:

using System.Linq;
using Elements;
using Extensions;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace Systems
{
    public class MapDataAccess : MonoBehaviour
    {
        public static MapDataAccess Service;
        public Vector2Int mapMaxBounds = Vector2Int.zero;

        private Tilemap[] _mapLayers;
      
        void Awake()
        {
            if (Service == null)
                Service = this;
        }

        public void Initialize()
        {
            _mapLayers = GetComponentsInChildren<Tilemap>();

            foreach (var layer in _mapLayers)
            {
                //Compress bounds to only shown tiles
                layer.CompressBounds();

                //Find largest layer: layer.size.x and layer.size.y and update map size if needed
                if (layer.size.x > mapMaxBounds.x)
                    mapMaxBounds.x = layer.size.x;

                if (layer.size.y > mapMaxBounds.y)
                    mapMaxBounds.y = layer.size.y;

            }
        }

        public bool IsCellWithinMaximumBounds(int x, int y)
        {
            {
                return x >= 0 && x < mapMaxBounds.x &&
                       y >= 0 && y < mapMaxBounds.y;
            }
        }

        public bool IsCellBlockedAndShadowCaster(int x, int y)
        {
            //Check TileMap first
            for (var index = 0; index < _mapLayers.Length; index++)
            {
                var layer = _mapLayers[index];
                AdvancedTile layerTile = layer.GetTile<AdvancedTile>(new Vector3Int(x, y, 0));

                if (!layerTile)
                    continue;

                if (layerTile.hasCollider && !layerTile.colliderDoesNotCastShadows)
                {
                    return true;
                }
            }

            //Check anything else
            var tiles = GetNonTilemapObjects(x, y);
            for (var index = 0; index < tiles.Length; index++)
            {
                NonTilemapObject tile = tiles[index];
                if (tile.hasCollider && !tile.colliderDoesNotCastShadows)
                {
                    return true;
                }
            }

            return false;
        }

        public bool IsCellBlocked(int x, int y)
        {
            //Check TileMap first
            for (var index = 0; index < _mapLayers.Length; index++)
            {
                var layer = _mapLayers[index];
                AdvancedTile layerTile = layer.GetTile<AdvancedTile>(new Vector3Int(x, y, 0));

                if (!layerTile)
                    continue;

                if (layerTile.hasCollider)
                {
                    return true;
                }
            }

            //Check anything else
            var tiles = GetNonTilemapObjects(x, y);
            for (var index = 0; index < tiles.Length; index++)
            {
                NonTilemapObject tile = tiles[index];
                if (tile.hasCollider)
                {
                    return true;
                }
            }

            return false;
        }

        public T GetFirstNonTilemapObjectAt<T>(int x, int y)
        {
            //No need to check tilemap as doors don't "live" there
            var tiles = GetNonTilemapObjects(x, y);
            for (var index = 0; index < tiles.Length; index++)
            {
                NonTilemapObject tile = tiles[index];
                var nonMapObjectComponent = tile.gameObject.GetComponent<T>();

                if (nonMapObjectComponent!=null)
                {
                    return nonMapObjectComponent;
                }
            }

            return default;
        }

        #region Method Overloads

        public bool IsCellBlocked(Vector2Int cellPosition)
        {
            return IsCellBlocked(cellPosition.x, cellPosition.y);
        }

        public bool IsCellWithinMaximumBounds(Address address)
        {
            return IsCellWithinMaximumBounds(address.X, address.Y);
        }

        #endregion

        #region Helper Methods

        private NonTilemapObject[] GetNonTilemapObjects(int x, int y)
        {
            Vector2Int vectoredAddress = new Vector2Int(x, y);
            return GetComponentsInChildren<NonTilemapObject>().Where(c => c.gameObject.transform.TilemapPosition() == vectoredAddress).ToArray();
        }

        #endregion
    }
}

This along with the Pathfinding script, allows me to check the “collider” that is on the tile.

1 Like

For “by hand,” I mean the idea of taking a set of sprites and arranging them in the scene view in order to build a level like Lego bricks. At that point, in the previous tutorials and all the examples I find, people end up manually drawing 2D box colliders around all of the walls, leaving the paths in let’s say a “default” state where characters can travel freely. In the roguelike I was building before, the tiles themselves had a flag that designated their type, so there was no need for physics, the code was just able to tell a flag set to 0 or 1 and go from there.

What you are describing sounds like, effectively, the exact same thing. I guess another way of asking is - is there some sort of built in function in Unity that will allow me to set flags like this on tiles? I’ve already been looking through the documentation for Tilemaps and the like, and I can’t find any way to do this.

If I’m understanding it right, it looks like spryx is manually doing this in the top of the code in their last post, so as I understand, by making those public bool statements, they are effectively just going to be check boxes in the inspector, which is super clean and simple and exactly what I’m after. But the question is, where would someone find that information if not for me asking here? It just seems more elusive than I would have expected, I guess.

When I was asking about movement regarding the array, I guess what I meant was how would the code know that the four tiles around the current position relate to the array. Like, I get that I can build that exact array you gave as an example, and that I can make a maps that matches that layout, but I’m wondering if there’s a way to tie those two things directly together, so if I make changes in one it would automatically change the other. Again, maybe spryx’s code example does this, but I’m not 100% sure.

Thanks again for this help. I’m still reading through your code examples above and trying to parse everything, but it’s making more sense than earlier today, that’s for sure.

Well the code knows your position (x,y), this translates to your position in the array[x,y], the four tiles around it are array[x-1,y], array[x,y+1],array[x+1,y],array[x,y-1].

you can write a “level maker” that paints tiles and at the same time fills the array, and then saves it to disk.

I sorta realized I didn’t really answer the question in a meaningful way. Movement (i’m assuming character movement?) is difficult with the tilemap system if you aren’t using the built-in physics systems (aka: relying on colliders for hit detection). I’m also not talking about animated tiles that don’t move around, this is easy enough to accomplish with the 2d extras package. Fair warning: If your game isn’t too complex, i’d recommend using the built-in physics. Interacting with the tilemaps with a “roll your own” physics solution isn’t the most straightforward process. Being new to Unity is only going to compound the issue.

There are two primary problems to overcome:

  1. Due to the way in which the tilemap system is currently designed, it is difficult to move a single tile around tilemaps. In effect, you would need to move the tile, then erase/replace the tile at the previous position. This makes character movement difficult as it is choppy at best and there are other issues at play (namely a gamobject tied to a tile is destroyed if the tile is replaced).
  2. Individual tiles aren’t actually gameobjects, so adding any sort of behavior to them is difficult, and you need a prefab or some gameobject instance tied to them in order to add behavior per instance.

One solution is to decouple dynamic objects from the tilemap system. Adding prefabs to with a tilebrush is easy enough, but this does not associate them to any particular tile. @eses and @brigas are correct. You need to maintain some sort of reference between your tiles and non-tilemap objects so they can interact.

In the screenshot posted below, You can see I have a player and 2 enemies: a red and blue cube. These are derived classes from TileBase that I use to represent non-tilemap objects (anything that needs to move :p). I use the 2d position of the gameobject that is spawned from the “tile” as a link between tilemap objects and non-tilemap objects. Please ignore the console error… Unity Collaborate seems to be having issues this morning.
Screenshot: Non-TileMap and TileMap Objects

5892842--628394--TDT-Sample.PNG

Doors, enemies, and the player are not actually tilemap objects. Instead, they are a special type of tile:
NonTilemapObjectTileBase.cs

using System;
using UnityEngine;
using UnityEngine.Tilemaps;

[Serializable]
public abstract class NonTilemapObjectTileBase : AdvancedTile
{
    public Sprite TileSprite;
    public GameObject TileAssociatedPrefab;

    public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
    {
        tileData.sprite = TileSprite;
    }
}

NonTilemapObjectSpawnerTile.cs

using General;
using UnityEngine;

// ReSharper disable once CheckNamespace
[CreateAssetMenu(fileName = "NTO_.asset",menuName = Globals.CustomTilesBaseMenu + "Non-Tilemap Object Spawner")]
public class NonTilemapObjectSpawnerTile : NonTilemapObjectTileBase
{
}

When these tiles are created, the prefab is instantiated in place of the tile:
KarceroVirtualMap::VisualizeMap

public override void VisualizeMap(MapVisualTranslator mapVisualInfo)
        {
#if UNITY_EDITOR
            //Call base visualizer if called with null component
            if (mapVisualInfo == null)
            {
                base.VisualizeMap(null);
                return;
            }
#endif
            //Stop if no visuals
            if (mapVisualInfo.tileDefinitions.Count == 0)
                throw new Exception("No visual definitions for map");

            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Height; y++)
                {
                    VirtualCell cell = GetCell(x, y);

                    bool hasCompanionCell = false;
                    VirtualCell companionCell = null;

                    //If a door is found, attach a floor companion cell, otherwise there is no floor beneath a door
                    if (cell.CellType == CellTypeMatrix["DOOR"])
                    {
                        hasCompanionCell = true;
                        companionCell = new VirtualCell(CellTypeMatrix["FLOOR_CORRIDOR"]);
                    }

                    VisualTileDefinition first = mapVisualInfo.tileDefinitions.FirstOrDefault(g => g.name == (CellTypeMatrix.First(v => v.Value == cell.CellType).Key));
                    VisualTileDefinition second = hasCompanionCell ? mapVisualInfo.tileDefinitions.FirstOrDefault(g => g.name == (CellTypeMatrix.First(v => v.Value == companionCell.CellType).Key)) : null;

                    TileBase cellTranslationTile = first?.translationTile;
                    Tilemap instanceLocation = first?.translationTile.GetAssociatedTilemap();

                    TileBase companionCellTranslationTile = hasCompanionCell ? second?.translationTile : null;
                    Tilemap companionInstanceLocation = hasCompanionCell ? second?.translationTile.GetAssociatedTilemap() : null;

                    //Instantiate tile if found here. Remember that Karcero is stupid and swaps x and y coords
                    if (cellTranslationTile != null)
                    {
                        cellTranslationTile.InstantiateMapObject(instanceLocation, new Vector2Int(y, x));
                    }

                    //Instantiate tile companion if found here. Remember that Karcero is stupid and swaps x and y coords
                    if (companionCellTranslationTile != null)
                    {
                        companionCellTranslationTile.InstantiateMapObject(companionInstanceLocation, new Vector2Int(y, x));
                    }
                }
            }
        }

There is a lot of code to unpack here. Karcero is a 3rd party library I use to generate random dungeons. The visual translator adapts this for use with the tilemap system. The important bit is the call to GetAssociatedTilemap() and InstantiateMapObject(). Pseudocode is as follows.

Get Associated Tilemap:

  1. Look for the tilemap associated with the tile within all known tilemap layers
  2. If not in known layers, attempt to find the layer tile belongs to.
  3. Return layer name, or error if we can’t find the layer the tile belogs to.

InstantiateMapObject:

  1. Determine if the “tile” belongs on the tilemap system or not
  2. If not a tilemap tile, instantiate the gameobject associated to it
  3. Set gameobject parent to the layer the object belongs to.
  4. Attach a NonTileMapObject script so we can keep track of the instance.
  5. If tile does belong on the tilemap, just instantiate it.

TileBaseExtensions.cs

using System;
using System.Collections.Generic;
using Elements;
using General;
using UnityEngine;
using UnityEngine.Tilemaps;
using Object = UnityEngine.Object;

namespace Extensions
{
    public static class TileBaseExtensions
    {
        private static Dictionary<string, Tilemap> _tileMapCache;
        public static Tilemap GetAssociatedTilemap(this TileBase baseTile)
        {
            //Init list if empty
            if (_tileMapCache == null)
                _tileMapCache = new Dictionary<string, Tilemap>();

            //Determine the correct tilemap the object belongs to
            string tileNativeMap = TileMatrix.GetTileOwnerLayer(baseTile.name);

            //Determine if we have already found the tilemap this belongs to
            // ReSharper disable once AssignNullToNotNullAttribute
            bool mapIsCached = _tileMapCache.TryGetValue(tileNativeMap, out Tilemap associatedTilemap);

            if (!mapIsCached)
            {
                //Find map tile belongs to and "highlight" it
                associatedTilemap = GameObject.Find(tileNativeMap)?.GetComponent<Tilemap>();

                //Add to cache if not null
                if (associatedTilemap != null)
                    _tileMapCache.Add(tileNativeMap, associatedTilemap);
                else
                {
                    throw new ApplicationException("Could not find an owner layer for " + baseTile.name);
                }
            }
            else
            {
                //Determine if the reference to the tilemap is still good
                if (associatedTilemap == null)
                {
                    //ref is bad, remove from dictionary
                    _tileMapCache.Remove(tileNativeMap);

                    //Call this method again to find the ref and add to cache
                    return GetAssociatedTilemap(baseTile);
                }
            }

            return associatedTilemap;
        }

        public static void InstantiateMapObject(this TileBase baseTile, Tilemap instanceTilemap,
            Vector2Int instancePosition)
        {
            InstantiateTileOrMapObject(baseTile, instanceTilemap, instancePosition);
        }

        private static void InstantiateTileOrMapObject(TileBase baseTile, Tilemap instanceTilemap,
            Vector2Int instancePosition)
        {
            NonTilemapObjectTileBase nonMapTile = baseTile as NonTilemapObjectTileBase;
            if (nonMapTile)
            {
                //Instantiate GO
                GameObject nonTilemapObject = Object.Instantiate(nonMapTile.TileAssociatedPrefab,
                    nonMapTile.GetAssociatedTilemap().gameObject.transform);

                //Move GO
                nonTilemapObject.gameObject.DirectlyMoveNonTilemapPosition(instancePosition);

                //Attach non tilemap object script
                NonTilemapObject objectProperties =
                    (NonTilemapObject)nonTilemapObject.AddComponent(typeof(NonTilemapObject));

                //Get tile association to set properties
                AdvancedTile advTileVersion = (AdvancedTile)baseTile;

                //Set properties
                objectProperties.colliderDoesNotCastShadows = advTileVersion.colliderDoesNotCastShadows;
                objectProperties.hasCollider = advTileVersion.hasCollider;
            }
            else
            {
                //Instantiate TileBase if found
                instanceTilemap.SetTile(new Vector3Int(instancePosition.x, instancePosition.y, 0), baseTile);
            }
        }
    }
}

A script with the same fields as the tiles is attached to any non-tilemap object. This allows me to keep track of their “colliders”:
NonTilemapObject.cs

using UnityEngine;

namespace Elements
{
    public class NonTilemapObject : MonoBehaviour
    {
        public bool hasCollider;
        public bool colliderDoesNotCastShadows;
    }
}

Finally, after all that mess we get to movement. I am including portions of the player behavior that process movement when it is the player’s turn. I will link my previous post that references pathfinding and MapDataAccess.

PlayerController::processInput, Update

private void ProcessInput(Vector2Int mDelta)
        {
            Vector2Int playerTilemapPosition = transform.TilemapPosition();
            Vector2Int futureLocation = playerTilemapPosition + mDelta;


            //Finish Tweening if we need to move
            if (DOTween.PlayingTweens() != null)
                DOTween.CompleteAll();

            //if (MapDataAccess.Service.IsClosedDoorAt(futureLocation.x, futureLocation.y))
            //{
            //    //Try to open
            //    MapDataAccess.Service.GetFirstNonTileMapObjectAt<Door>(futureLocation.x, futureLocation.y).Open();

            //    //Collider was removed as door was opened - Do vis update to show what is behind the door
            //    FieldOfView.System.ForceVisUpdate(playerTileMapPosition);
            //    return;
            //}

            if (!MapDataAccess.Service.IsCellBlocked(futureLocation))
            {
                playerTilemapPosition = futureLocation;
                gameObject.DirectlyMoveNonTilemapPosition(playerTilemapPosition);

                FieldOfView.System.ForceVisUpdate(playerTilemapPosition);
            }
            else
            {
                //Check for anything with a default action
                IReceivesDefaultActionFromActor defaultActionableObject =
                    MapDataAccess.Service.GetFirstNonTilemapObjectAt<IReceivesDefaultActionFromActor>(futureLocation.x,
                        futureLocation.y);
                //If found, call default action
                defaultActionableObject?.ReceiveDefaultAction(this);

            }

            //Player moved, finish turn
            CompleteTask();
        }


        // Update is called once per frame
        void Update()
        {
            //Don't do anything is it is not our turn
            if (!_isPlayerTurn)
            {
                return;
            }

            if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
            {
                ProcessInput(new Vector2Int(0, 1));
                return;
            }

            if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
            {
                ProcessInput(new Vector2Int(0, -1));
                return;
            }

            if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
            {
                ProcessInput(new Vector2Int(1, 0));
                return;
            }

            if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
            {
                ProcessInput(new Vector2Int(-1, 0));
            }
        }

It is worth noting here that I have to keep track of the position of the player relative to the tilemap it belongs to. This is done so that MapDataAccess can use the transform position to keep track of Non-Tilemap objects. My previous post has a copy of this code.

At long last, I apologize for the length of this post and how beginner unfriendly it is. To make an incredibly long story short if you can, use built-in 2d physics. I chose not to for my project and you can probably tell it increases the complexity quite a bit. If you want to use the built-in systems, the tilemap colliders intreact rather nicely. The tilemap system is also very performant for what it is.

Thanks so much for all that information!

I just finished going through long YouTube series making Pac-Man and it’s pretty great, but the movement system this guy used was manually placing nodes at the corners, checking if the direction was valid, and then allowing or denying movement. It technically worked, but the idea of making more than one level, let alone random dungeon generation makes this, I think, the worst option. So let me ask a couple different questions.

I was originally playing around with Tilemaps and Rule Tiles, and I was pretty excited when I managed to recreate the original Pac-Man maze using one set of rules. I figure combining this with the Karcero stuff you mentioned above could, theoretically, make a variety of dungeons. Let’s pretend they are perfect. So the issue that I saw, which it seems you have touched on up there, is that you can’t add scripts to tiles, which to me invalidates the whole idea of using the Tilemap in the first place. Is that a fair summary?

So let’s talk about physics. in a very plain sense, adding physics clearly works pretty well, but how do I avoid the weird issues that I was describing above? One thing that I noticed the collider shapes will fit the sprite rather than a designated 8x8 (or whatever size) tile. So corner pieces would have 45 degree edges and the like. Is there a way to avoid that?

How do I tell the characters that the tile in front of them isn’t a valid space for decision making? I get that they will physically be stopped, but how would they know they can’t go that way and need to make a turn? (This is where I was thinking originally if there were flags set on each tile for canMove/cannotMove it would be pretty straightforward.)

And I guess I’m wondering how random generation would work when adding physics or any scripts. Like, I know the Tilemap has a prefab brush, but that seems to me more of a manual thing for one item at a time.

Sorry I’m a bit all over the place. I was really all set to make me own version, pretty confident that I understood everything I learned, but the moment I tried to do something slightly different, I just hit like 10 brick walls and I’m not sure exactly where to go next.

Have you tried to change the physics shape of the corner piece in sprite editor?

Can you see if Physics2D.OverlapCircle with layermask works? I’m thinking create a tilemap for walls with a layer set to something like “Walls”, and use Physics2D.OverlapCircle to test is the target destination is of the specific layer. If no hit then the target tile is canMove in your words.

For random generation, I only tried to generate something like a basic dungeon with either a floor tile or a wall tile. I used the algorithm called random walker to place all the floor tiles using Tilemap.SetTile, then surround them with wall tiles, which are rule tile created by hand using the 2d extra package. The wall tile only need a layer to tell player whether it is canMove or not, so no script needs to be attached on them.

For the wall tilemap, I used tilemap colider and composite collider, and use polygons as the geometry type, because outline doesn’t work for me :frowning:

@quadnine_damage

Karcero is pretty nice, but it has some terrible bugs. It is also fairly easy to integrate into Unity, i’m not the first one to do so. The author no longer updates it and for some stupid reason swapped the x and y coordinates when it generates maps. I have all sorts of “fixes” in place around it.

The RuleTile would be a good use for this as Karcero doesn’t know or care about physics. All the collision logic can be handled when the tiles are created.

You are correct in that Tiles are not GameObjects and as such, defining behavior for them is difficult. It is possible to instantiate a GameObject that is tied to the tile instance. There are some caveats to this of course:
The GameObject will be destroyed if TileMap.SetTile is called at the tile position
Limited usefulness of behavior (e.g. Physics will be applied to the gameobject, not the tile).

I believe what you want is already possible. You can define physics shapes per sprite. For normal walls, this should take up the entire sprite area. For 45-degree walls, you can manually edit the polygon collider.

In terms of movement, generally you want to look at the future location when moving a character/actor For this purpose, I might opt to do a 2D RayCast to determine if I can safely move to the future location. You might need to determine a good spot to raycast into, or do several if your future location contains tiny colliders. This should work with all the built-in tilemap functionality. Your actors should be normal sprites (not part of the tilemap system) if they need to do movement.

So, the raycast I was talking about above generally should be done in all directions around the actor. (eg. grid positions above, right of, below, left of) From this you will be able to determine which directions around the actor are blocked and then make movement decisions.

Depends on what you are using for random generation. Most of these systems should follow a single responsibility pattern and simply generate random data. The physics portion could really be handled entirely by the tilemap system (i.e. rule tiles).

No worries. That is part of the fun of game dev. It is an unlimited source of puzzles to solve. I’ve found this community to be super helpful for the most part as long as you are willing to provide good information. For your first few games don’t try to do something super out of the box. Almost everything and anything is possible, but it is always a trade off between features and complexity.

Not sure if it has been mentioned, but one thing i learned from an old udemy course was to create another tilemap where you only paint in blocking tiles, then in your character movement you read whether or not there’s a tile in the next space before you let the character move (assuming you are using grid based movement and not freeform movement).

The tilemap system gives you limied information by default (without the 2d extras gridinformation package), but checking if a space on the grid has a tile, or the tile name, are both possible. Using the name is one trick i used to prototype trpg movement on different terrain types. I stored the sprites with the terrain type in the name, like Grass_ and then used a text parser to delete the _ and everything after. I could then check this against an enum to get a movement cost with basically no hassle.

One other method is to store all your tiles in a dictionary at runtime. This allows you to store any sort of information you want, and then you can read back that information at will. You can cross reference the grid location against the dictionary reference. Here’s a pretty cool old article on the method, though it might be overkill for this particular project: Unity Tilemaps and Storing Individual Tile Data | by Allen Hendricks | Medium