Tile Based Cardinal Movement System

  1. What do you want to achieve? An 8 directional cardinal movement system.

  2. What is the issue? I want the character to move as shown in blue, but moves as shown in black instead.

  3. What solutions have you tried so far? Looked through the forums, couldn’t find anything on the topic. I somewhat have it but it’s far from perfect

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local mouse = player:GetMouse()
local currentMovement

mouse.Button1Down:Connect(function()
	local target = mouse.Target

	if target and target:HasTag("Floor") then

		local targetPosition = target.Position + Vector3.new(0, target.Size.Y / 2, 0)
		local intermediatePosition = Vector3.new(targetPosition.X, character.PrimaryPart.Position.Y, targetPosition.Z)

		if currentMovement then
			currentMovement:Disconnect()
			currentMovement = nil
		end

		humanoid:MoveTo(intermediatePosition)
		currentMovement = humanoid.MoveToFinished:Connect(function(reached)
			if reached and humanoid then
				humanoid:MoveTo(targetPosition)
				currentMovement:Disconnect()
				currentMovement = humanoid.MoveToFinished:Connect(function()
					currentMovement = nil
				end)
			end
		end)
	end
end)

humanoid.MoveToFinished:Connect(function()
	currentMovement = nil
end)

If it’s one diagonal it works fine, just not over long diagonal distances.

On the same topic, would it be more reasonable to clone a part rather than manually placing all of them? The game I am working on will have hills / slopes.

Here is what I’m trying to accomplish:

it looks like your trying to make a pathfinding algorithm. if that’s the case then do some research on the a* algorithm specifically using the Euclidean distance heuristic for those diagonals.

1 Like

Works well, although I have an issue of it not being able to find the path if it’s on the opposite side of an obstacle.

Here is what I came up with:

local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local CollectionService = game:GetService("CollectionService")

local player = Players.LocalPlayer
local mouse = player:GetMouse()
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")

local PART_SIZE = 4
local MAX_ITERATIONS = 2000
local DEBOUNCE_TIME = 0.5
local lastClickTime = 0
local currentMove = nil -- Keep track of the current move

local function getEuclideanDistance(nodeA, nodeB)
	local dx = (nodeB.X - nodeA.X) * PART_SIZE
	local dy = (nodeB.Y - nodeA.Y) * PART_SIZE
	return math.sqrt(dx * dx + dy * dy)
end

local function getNeighbors(node, grid)
	local neighbors = {}
	local directions = {
		Vector2.new(-1, 0), Vector2.new(1, 0),
		Vector2.new(0, -1), Vector2.new(0, 1),
		Vector2.new(-1, -1), Vector2.new(1, 1),
		Vector2.new(-1, 1), Vector2.new(1, -1)
	}
	for _, direction in ipairs(directions) do
		local neighborPos = node + direction
		if grid[neighborPos.X] and grid[neighborPos.X][neighborPos.Y] and grid[neighborPos.X][neighborPos.Y] == 0 then
			table.insert(neighbors, neighborPos)
		end
	end
	return neighbors
end

local function aStar(start, goal, grid)
	local openSet = {[start] = true}
	local cameFrom = {}
	local gScore = {[start] = 0}
	local fScore = {[start] = getEuclideanDistance(start, goal)}
	local iterations = 0

	while next(openSet) do
		if iterations >= MAX_ITERATIONS then
			warn("A* algorithm exceeded maximum iterations. Terminating.")
			return nil
		end
		iterations = iterations + 1

		local current
		for node in pairs(openSet) do
			if not current or fScore[node] < fScore[current] then
				current = node
			end
		end

		if current == goal then
			local path = {}
			while current do
				table.insert(path, 1, current)
				current = cameFrom[current]
			end
			return path
		end

		openSet[current] = nil
		for _, neighbor in ipairs(getNeighbors(current, grid)) do
			local tentativeGScore = gScore[current] + getEuclideanDistance(current, neighbor)
			if not gScore[neighbor] or tentativeGScore < gScore[neighbor] then
				cameFrom[neighbor] = current
				gScore[neighbor] = tentativeGScore
				fScore[neighbor] = gScore[neighbor] + getEuclideanDistance(neighbor, goal)
				openSet[neighbor] = true
			end
		end

		if iterations % 100 == 0 then
			task.wait()
		end
	end

	return nil
end

local function createGrid()
	local grid = {}
	local workspace = game:GetService("Workspace")
	local parts = workspace:FindFirstChild("MapParts"):GetChildren()

	for _, part in ipairs(parts) do
		local gridX = math.floor(part.Position.X / PART_SIZE)
		local gridY = math.floor(part.Position.Z / PART_SIZE)

		if not grid[gridX] then
			grid[gridX] = {}
		end

		if not CollectionService:HasTag(part, "Obstacle") then
			grid[gridX][gridY] = 0 -- Walkable
		else
			grid[gridX][gridY] = 1 -- Not walkable
		end
	end

	return grid
end

local function visualizePath(path)
	for _, node in ipairs(path) do
		local centerX = (node.X * PART_SIZE) + (PART_SIZE / 2)
		local centerZ = (node.Y * PART_SIZE) + (PART_SIZE / 2)
		local pathPart = Instance.new("Part")
		pathPart.Anchored = true
		pathPart.CanCollide = false
		pathPart.Size = Vector3.new(1, 0.2, 1)
		pathPart.Position = Vector3.new(centerX, character.PrimaryPart.Position.Y, centerZ)
		pathPart.Color = Color3.fromRGB(128,0,128)
		pathPart.Parent = workspace
		Debris:AddItem(pathPart, 3)
	end
end

local function moveCharacter(path)
	if currentMove then
		currentMove:Disconnect() -- Cancel the current movement
	end

	currentMove = humanoid.MoveToFinished:Connect(function()
		if #path > 0 then
			local node = table.remove(path, 1)
			local centerX = (node.X * PART_SIZE) + (PART_SIZE / 2)
			local centerZ = (node.Y * PART_SIZE) + (PART_SIZE / 2)
			humanoid:MoveTo(Vector3.new(centerX, character.PrimaryPart.Position.Y, centerZ))
		else
			currentMove:Disconnect()
			currentMove = nil
		end
	end)

	-- Start the movement
	local firstNode = table.remove(path, 1)
	local firstCenterX = (firstNode.X * PART_SIZE) + (PART_SIZE / 2)
	local firstCenterZ = (firstNode.Y * PART_SIZE) + (PART_SIZE / 2)
	humanoid:MoveTo(Vector3.new(firstCenterX, character.PrimaryPart.Position.Y, firstCenterZ))
end

mouse.Button1Down:Connect(function()
	local currentTime = tick()
	if currentTime - lastClickTime < DEBOUNCE_TIME then
		return
	end
	lastClickTime = currentTime

	local targetPos = mouse.Hit.p
	local gridStartPos = Vector2.new(math.floor(character.PrimaryPart.Position.X / PART_SIZE), math.floor(character.PrimaryPart.Position.Z / PART_SIZE))
	local gridGoalPos = Vector2.new(math.floor(targetPos.X / PART_SIZE), math.floor(targetPos.Z / PART_SIZE))
	local grid = createGrid()

	local path = aStar(gridStartPos, gridGoalPos, grid)
	if path then
		visualizePath(path)
		moveCharacter(path)
	end
end)

I put a max iteration check so the game doesn’t freeze if it can’t path, however I am unable to go around obstacles (tiled red) unless it’s decently close, even if I raise the iteration count.