Hello There Developers,
I got a little bored so I decided to make some A* Pathfinding, If you wanna see it in full action go watch the video below!
Creation Process
I wont be diving very deeply into each system but very primitive or vanilla operations were done to create this system, so Its nice to leave a like on this post!
**HIS AREA IS SOLEY TO SHOW ITS FUNCTIONALITY**
Grid System
Initializer
function makeGrid(size) : {}
local grid = {}
for x = 1, size do
grid[x] = {}
end
for x = 1, size do
for y = 1, size do
grid[x][y] = {x = x, y = y, data={role = "Empty"}}
end
end
return grid
end
function GridModule:Create(Size:number) : GridClass
self = setmetatable({}, {__index = GridModule})
self.Grid = makeGrid(Size)
self._width = 1
self._centre = Vector3.zero
self._size = Size
return self
end
This is where all the magic happens! Briefly this just makes a square grid, specifically square for simplicity,
Basic Operations and Functions
function GridModule:Visualize(Width:number, Centre:Vector3) : Model
if self.VisualGrid and typeof(self.VisualGrid) == "Instance" then self.VisualGrid:Destroy() end
Centre = typeof(Centre) == "Vector3" and Centre or Vector3.zero
Width = typeof(Width) == "number" and Width or 1
self._width = Width
self._centre = Centre
local grid = self.Grid
local model = Instance.new("Model")
model.Name = "Grid"
self.VisualGrid = model
model.Parent = workspace
local GridSize : number = self._size
local totalGridWidth = GridSize * Width
local startOffset = -(totalGridWidth/2) + (Width/2)
for x = 1, self._size do
for y = 1, self._size do
if not self.VisualGrid then print("Stopping grid build process..") break end
local part = Instance.new("Part")
part.Name = `{x}_{y}`
part.Anchored = true
part.Size = Vector3.new(Width, 0.25, Width)
part.Material = Enum.Material.SmoothPlastic
local xPos = startOffset + (x-1) * Width
local zPos = startOffset + (y-1) * Width
part.Position = Vector3.new(xPos, 0, zPos) + Centre
part.Parent = model
if (x + y) % 2 == 0 then
part.Color = PatternColor1 -- Light color
else
part.Color = PatternColor2 -- Dark color
end
end
end
self.VisualGrid = model
return model
end
function GridModule:GetCell(Position: Vector2): (Cell, Part)
local x = Position.X
local y = Position.Y
if not self.Grid[x] then return end
local cell = self.Grid[x][y] or nil
if not cell then return end
local part = self.VisualGrid:FindFirstChild(`{x}_{y}`)
return cell, part
end
function GridModule:SetCellData(Position:Vector2, Data:{})
local cell, part = self:GetCell(Position)
if not cell then warn("Invalid cell") return end
self.Grid[cell.x][cell.y].data = Data
end
function GridModule:HighlightCell(Position: Vector2, Color: Color3 | BrickColor, Role: string): Cell
local cellData, part = self:GetCell(Position)
if not cellData then return end
self.Grid[Position.X][Position.Y].data.role = Role or cellData.role
if not part then return cellData, nil end
part.Color = (typeof(Color) == "BrickColor") and Color.Color or Color
task.wait(0.05)
return cellData, part
end
function GridModule:Clean()
self.Grid = makeGrid(self._size)
self:Visualize(self._width, self._centre)
end
Most of the functions you see here are to visualize the data we’ll produce from the algorithm, Each were made in such a way that its easy to understand but accurate, Here are the types if your eager to see:
type Cell = {
x : number,
y: number,
data : {any}
}
type GridType = {
x:
{y:
{
x:number,
y:number,
data:{any}
}
}
}
type GridClass = {
Grid : {},
VisualGrid : Model,
Visualize : (GridClass, Width:number, Centre:Vector3) -> (Model),
GetCell : (GridClass, Position:Vector2) -> (Cell?),
SetCell : (GridClass, Position:Vector2, data :{any}) -> (Cell?),
HighlightCell : (GridClass, Position:Vector2, Color:Color3 | BrickColor, Role:string) -> (),
GetNeighbouringCells : (GridClass, Position:Vector2, Radius:number) -> ({Cell?}),
DistanceToCell : (GridClass?, Position:Vector2, ToPosition:Vector2) -> (Distance),
GetGridSize : (GridClass, GridType) -> (number),
Clean : (GridClass) -> (),
}
Experimental Functions
function GridModule:GetNeighbouringCells(Position: Vector2, Radius: number): {Cell}
local list = {}
local x = Position.X
local y = Position.Y
Radius = math.max(math.floor(Radius), 1)
for i = x - Radius, x + Radius do
for j = y - Radius, y + Radius do
-- Skip the current cell
if i == x and j == y then continue end
if not self.Grid[i] then continue end
local cell = self.Grid[i][j]
if not cell then continue end
table.insert(list, cell)
end
end
return list
end
function GridModule:IsCellObstacle(Position:Vector2)
local cell = self:GetCell(Position)
if not cell then return true end
return cell.data.role == "Wall"
end
function GridModule:IsCellEmpty(Position:Vector2)
if not Position then return true end
if typeof(Position) ~= "Vector2" then return true end
local cell, part = self:GetCell(Position)
return cell == nil
end
function GridModule.DistanceToCell(Position:Vector2, ToPosition:Vector2) : number
local cell, part = GridModule:GetCell(ToPosition)
if not cell then warn("Invalid cell") return end
local distance = (Position - ToPosition).Magnitude
return distance
end
These are the functions used mostly within the A* Pathfinding algorithm, created with simplicity and efficiency.
A✦ Algorithm
Definition
For a more detailed explanation this Article by Mr. Swift should definitely shoot out all your questions! This algorithm is simple if you really break it down into its finite sections and clear meaning. (remember to refactor!)
According to Wikipedia (23/02/2025) : “A-star is an informed search algorithm, or a best-first search, meaning that it is formulated in terms of weighted graphs: starting from a specific starting node of a graph, it aims to find a path to the given goal node having the smallest cost (least distance travelled, shortest time, etc.). It does this by maintaining a tree of paths originating at the start node and extending those paths one edge at a time until the goal node is reached.”
Implementation
function APath:FindPath(Start: Vector2, Goal: Vector2): ({any}, boolean)
local _0 = tick()
self.Nodes = {}
local Path = {}
local PathFound = false
local StartNode = self:CreateNode(Start, 0, 0, (Goal - Start).Magnitude)
local GoalNode = self:CreateNode(Goal, 0, 0, 0)
local OpenSet = {[Start] = StartNode}
local ClosedSet = {}
local iterations = 0
while next(OpenSet) do
iterations += 1
if iterations % 100 == 0 then task.wait() end
local Current, LowestF = nil, math.huge
for _, Node in pairs(OpenSet) do
if Node.F < LowestF then
Current = Node
LowestF = Node.F
end
end
-- Check if we've reached the goal position
if Current.Position == Goal then
PathFound = true
GoalNode.Parent = Current.Parent
Current = GoalNode
break
end
OpenSet[Current.Position] = nil
ClosedSet[Current.Position] = Current
for _, NeighborPos in ipairs(self:GetNeighbors(Current)) do
if ClosedSet[NeighborPos] then continue end
if self.Env:IsCellEmpty(NeighborPos) or self.Env:IsCellObstacle(NeighborPos) then continue end
local tentativeG = Current.G + 1
local Neighbor = OpenSet[NeighborPos]
if not Neighbor then
Neighbor = self:CreateNode(
NeighborPos,
0,
tentativeG,
(Goal - NeighborPos).Magnitude
)
OpenSet[NeighborPos] = Neighbor
Neighbor.Parent = Current
elseif tentativeG < Neighbor.G then
Neighbor.Parent = Current
Neighbor.G = tentativeG
else
continue
end
Neighbor.F = Neighbor.G + Neighbor.H
end
if iterations > 1000 then
warn("Pathfinding took too long - path may be impossible")
break
end
end
-- Path reconstruction from goal to start
if PathFound then
local Current = GoalNode
while Current do
table.insert(Path, 1, Current.Position)
Current = Current.Parent
end
end
print(`Process Completion Time : {(tick() - _0) * 1000}ms`)
return Path, PathFound
end
This script isn’t user friendly (hard to understand) and is complex to write out completely, so I’d recommend reading the article I provided.
Review
Thank you for reading! If you can, would you please rate this post? All choices are not forced and are not negatively taken, rather its used for Improvement and Engagement.
- Yes!
- Meh…
- Could Be Better.
- No.
0 voters