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