Grid style movement

Hello, I am making a game based on a board game. Basically I am having some trouble about the movement system for it. The movement system will be like a square grid style system

Right now I had an idea about using ray casting for it. What I mean is casting a ray to the front, the back and both sides of the character and detect if the block the player wants to walk to is available or not if it is available the player would be able to click on the block and move to there.
Here’s a top view picture about the ray cast idea, and a picture of 1 block. maybe it will help you understand what I am thinking a bit more.

Red dot = player (red arrow = the direction player is facing)
Green lines = ray cast
Blue block = unavailable block
Yellow invisible block = another block on top to detect player’s ray

The problem is that it would be harder to make a range
system (how far player can walk, I want to be able to change how far player can walk also) for it because I think the ray would stop at the first block and it wouldn’t detect any further so the range would stay the same (1 block)

This is what I want it to be like

Green line = raycast

Now, I want to know how would I fix the range problem on my ray casting idea. If you have any suggestions, any other ideas please suggest it below. Thanks in advance

(If there is any mistakes about the scripting stuff, I apologize. I’m not very experienced at scripting)

4 Likes

The raycasting idea could work, but IMO it and other types of spatial queries to look for parts is a bit janky. It can 100% work reliably, but I just don’t feel like it’s an especially elegant way of doing it, and has a lot of assumptions about how the world is set up that isn’t visible by reading the code.

What I usually do when making grid-based games is to have a 2d array of tiles, which I call a Map. That way you can quickly and easily get a list of neighboring tile for any given tile, which enables you to write algorithms like flood fill, depth first search, even A* for path finding. I also write functions to convert between different coordinate systems so that 1 tile always = 1 unit, e.g. the tile to the right is always map[x+1][y] even if each tile is 4 studs on a side.

**

Here's a Map2D class I use in lots of projects:
local Map2D = {}
local Map2D_mt = {__index = Map2D}

function Map2D.new()
    local map = setmetatable({}, Map2D_mt)
    return map
end

function Map2D:Get(p: Vector3): any
    if self[p.X] then
        return self[p.Y]
    end
end

function Map2D:Set(p: Vector3, value: any)
    self[p.X] = self[p.X] or {}
    self[p.X][p.Y] = value
end

return Map2D

… although you might want to make it so it uses the Z coordinate instead of the Y coordinate for the 2nd index.


I usually have a "BlockWorld" module for various 3D grid helper functions and managing Maps, although for your case "TileWorld" is probably more appropriate:

local Map2D = require(game.ServerStorage.Map2D)

local floor, v3 = math.floor, Vector3.new

local TILE_SIZE = 4

local TileWorld = {
TILE_SIZE = TILE_SIZE
}

local tileMap = Map2D.new()

local function round(n: number, to: number): number
to = to or 1
return floor(n/to + 0.5) * to
end

local function roundVector3(v: Vector3, to: number): Vector3
return v3(round(v.X, to), round(v.Y, to), round(v.Z, to))
end

function TileWorld.WorldToTileSpace(worldPos: Vector3): Vector3
–Rounded to nearest whole-number coordinates, because non-whole tile coordinates generally aren’t valid
– i.e. there’s no tile at (1.5, 2.25).
return roundVector3(worldPos / TILE_SIZE)
end

function TileWorld.TileToWorldSpace(tilePos: Vector3): Vector3
–Not rounded because TILE_SIZE might not be a whole number.
–Besides, if tilePos has non-whole coordinates then probably it’s on purpose to
– get e.g. the world-space position of tile edges
return tilePos * TILE_SIZE
end

function TileWorld.GetTile(tilePos: Vector3): Model
return tileMap:Get(tilePos)
end

function TileWorld.SetTileType(tilePos: Vector3, tileType: number): Model
local oldTile = TileWorld.GetTile(tilePos)
local oldTileType = oldTile and oldTile.TileType.Value

if oldTile then
    if tileType == oldTileType then
        return oldTile
    else
        oldTile:Destroy()
    end
end

local tile = TileTypes[tileType]:Clone()
tile:SetPrimaryPartCFrame( TileWorld.TileToWorldSpace(tilePos) )
tileMap:Set(tilePos, tile)
return tile

end

function TileWorld.Adjecent(pos: Vector3): Vector3
return {
pos + v3(-1, 0, 0),
pos + v3(0, 0, 1),
pos + v3(1, 0, 0),
pos + v3(0, 0, -1)
}
end

return TileWorld

… I usually work with a set of pre-built tiles of different types, where every tile of the same type has identical models. If you don’t use that approach you might want to change SetTileType to SetTile and just pass a model as the second parameter instead of a number. If you have pre-built maps then you’d want to do that at the start of the game, i.e. just loop over every tile, convert it’s position to tile space coordinates, and call TileWorld.SetTile(tilePos, tileModel) to initialize the tile map.

The TileTypes module just looks like this:

local TileTypes = {}

for _, tileModel in ipairs(script:GetChildren()) do
    local tileType: number = tileModel.TileType.Value
    TileTypes[tileModel.Name] = tileType
    TileTypes[tileType] = tileModel
end

return TileTypes

That way you can get the model from a tile type number like this: TileTypes[1], or the tile type number from the name like this: TileTypes.Grass. Combined you can get the model from the name like this: TileTypes[TileTypes.Grass]. If I weren’t so lazy I’d probably set up some custom Enums, but this approach works fine too just don’t make typos :stuck_out_tongue:


Anyway, with the TileWorld module it’s pretty easy to get all tiles in a range by just looping over X and Y (or Z) coordinates, but if you also want to take into account going around obstacles, you’ll need to use depth- first search. I have implementations for a few handy, generic algorithms like DFS and flood fill that make it really easy to do this, so ask away if you want info on that. It’s on my other computer tho so I won’t be able to respond right away.


11 Likes