A* Pathfinding Showcase

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!


:hammer_and_pick: 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! :happy3:

**HIS AREA IS SOLEY TO SHOW ITS FUNCTIONALITY**

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

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


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

Was this post Entertaining and Useful?
  • Yes!
  • Meh…
  • Could Be Better.
  • No.
0 voters
1 Like

I was watching a breakdown of the code in Animal Crossing when I was reminded of this topic. What’s super interesting to me is that this design can be used with additional logic to procedurally generate world seed maps faster than before. Obviously, it’s a lot simpler than the breakthroughs that @DevUltra has been pushing for procedural generation, but this showcase has more broad applications than I previously considered. Dev can definitely speak more on the topic than I can, but keep up the good work! Excited to see what you do with your creation.

Here’s the video referenced above.

1 Like