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
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.