惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
量子位
小众软件
小众软件
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
T
Threat Research - Cisco Blogs
Latest news
Latest news
Spread Privacy
Spread Privacy
C
Cyber Attacks, Cyber Crime and Cyber Security
NISL@THU
NISL@THU
T
Tor Project blog
Hacker News: Ask HN
Hacker News: Ask HN
V2EX - 技术
V2EX - 技术
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
K
Kaspersky official blog
Cyberwarzone
Cyberwarzone
博客园 - 叶小钗
博客园 - 聂微东
Last Week in AI
Last Week in AI
爱范儿
爱范儿
腾讯CDC
博客园 - Franky
美团技术团队
J
Java Code Geeks
O
OpenAI News
L
Lohrmann on Cybersecurity
Simon Willison's Weblog
Simon Willison's Weblog
有赞技术团队
有赞技术团队
T
Threatpost
G
GRAHAM CLULEY
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
宝玉的分享
宝玉的分享
I
Intezer
N
News and Events Feed by Topic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Blog of Author Tim Ferriss
S
Security @ Cisco Blogs
Forbes - Security
Forbes - Security
N
News | PayPal Newsroom
Stack Overflow Blog
Stack Overflow Blog
Scott Helme
Scott Helme
H
Hacker News: Front Page
Cloudbric
Cloudbric

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How I implemented A* pathfinding in Unity C# (with DFS maze generation)
Abdul Fahad · 2026-06-28 · via DEV Community

Posted by Abdul Fahad Shahbaz — Unity game developer based in Lahore, Pakistan. Portfolio: fahadshahbaz.fun
In my latest Unity project — an AI-driven 3D maze game — I needed two things to work together: a maze generator that creates a unique, fully-connected layout every run, and an enemy AI that could navigate those mazes intelligently in real time.

The challenge? The maze is procedural. That means I can't bake a navmesh ahead of time or rely on Unity's built-in navigation system. Every time the player starts a run, a brand new maze is generated — and the enemy has to find its way through it from scratch.

I solved this with two algorithms working in tandem:

Depth-First Search (DFS) to generate the maze
A* pathfinding to navigate it

In this post I'll explain both implementations with working C# code taken directly from the project. By the end you'll have a self-contained AStarPathfinder class you can drop into any Unity maze project.

My portfolio (see the full project): https://fahadshahbaz.fun/works/AI-Maze-Game

The problem with Unity's built-in NavMesh

Unity's NavMesh is excellent for static environments. But procedural mazes create geometry at runtime — walls are instantiated, corridors are carved, the whole layout is different every time. By the time the maze is ready, it's too late to bake a navmesh without adding significant frame time.

I needed a pathfinder that:

Works on a grid, not a mesh
Knows which cells have walls between them (not just which cells exist)
Converts grid coordinates back to world-space Vector3 positions the enemy can walk to

A custom A* implementation turned out to be the cleanest fit.

Part 1: Generating the maze with DFS

Before we can pathfind through a maze, we need a maze. I used iterative Depth-First Search (also called "recursive backtracker") because it produces long, winding corridors that feel natural for a 3D game — not a grid of perfect squares.

The algorithm works like this:

Start at a random cell, mark it visited
Pick a random unvisited neighbour
Remove the wall between the current cell and that neighbour
Move to the neighbour and repeat
If no unvisited neighbours exist, backtrack to the previous cell
Repeat until all cells are visited

Every cell in the grid is a MazeCell — a simple data structure that tracks which of its four walls (left, right, front, back) are still standing.

csharp

public class MazeCell
{
    private bool rightWall = true;
    private bool leftWall  = true;
    private bool frontWall = true;
    private bool backWall  = true;

    public bool HasRightWall() => rightWall;
    public bool HasLeftWall()  => leftWall;
    public bool HasFrontWall() => frontWall;
    public bool HasBackWall()  => backWall;

    public void RemoveRightWall() => rightWall = false;
    public void RemoveLeftWall()  => leftWall  = false;
    public void RemoveFrontWall() => frontWall = false;
    public void RemoveBackWall()  => backWall  = false;
}

The generator keeps a Stack to track the path for backtracking. When it carves a passage between cell A and cell B, it removes A's wall facing B and B's wall facing A — so both cells agree on the opening.

csharp// Example: carving a passage to the RIGHT
currentCell.RemoveRightWall();
grid[next.x, next.y].RemoveLeftWall();

The result is a MazeCell[,] grid where every cell knows exactly which of its walls are still standing. This is the data structure the A* pathfinder reads directly.

Why DFS over Prim's or Kruskal's? DFS produces mazes with a single long main path and fewer dead ends — which feels more fun to race through. Prim's produces mazes with many short dead ends, which is better for puzzle games. For a time-pressure race game, DFS was the right call.

Part 2: A* pathfinding through a wall-aware grid

Here is the full AStarPathfinder class from the project.

csharp

using System.Collections.Generic;
using UnityEngine;

public class AStarPathfinder
{
    private MazeCell[,] grid;
    private int width;
    private int depth;
    private float cellSizeX;
    private float cellSizeZ;
    private Vector3 originOffset;

    public AStarPathfinder(
        MazeCell[,] grid,
        int width,
        int depth,
        float cellSizeX,
        float cellSizeZ,
        Vector3 originOffset = default)
    {
        this.grid         = grid;
        this.width        = width;
        this.depth        = depth;
        this.cellSizeX    = cellSizeX;
        this.cellSizeZ    = cellSizeZ;
        this.originOffset = originOffset;
    }

I pass in cellSizeX, cellSizeZ, and originOffset so the pathfinder can convert grid coordinates back to Unity world space at the end. The maze cells live in a logical grid (integer indices), but the enemy AI needs real Vector3 positions to move to.

The Node class

csharp

class Node
    {
        public int x, z;
        public float g, h;
        public float f => g + h;
        public Node parent;

        public Node(int x, int z)
        {
            this.x = x;
            this.z = z;
        }
    }

Each node holds:

x, z — grid coordinates (I use Z for depth, matching Unity's coordinate system)
g — cost from the start node to this node
h — heuristic estimate to the goal (Manhattan distance)
f — total estimated cost (g + h); this is what A* uses to prioritise nodes
parent — the node we came from, used to retrace the path at the end

The main search loop

csharp

public List<Vector3> FindPath(Vector2Int start, Vector2Int end)
    {
        List<Node> open = new List<Node>();
        HashSet<(int, int)> closed = new HashSet<(int, int)>();

        Node startNode = new Node(start.x, start.y);
        Node endNode   = new Node(end.x, end.y);

        open.Add(startNode);

        while (open.Count > 0)
        {
            // Pick the node with the lowest f score
            Node current = open[0];
            foreach (var n in open)
                if (n.f < current.f)
                    current = n;

            open.Remove(current);
            closed.Add((current.x, current.z));

            // Goal reached — retrace the path
            if (current.x == endNode.x && current.z == endNode.z)
                return Retrace(current);

            foreach (var neighbor in GetNeighbors(current))
            {
                if (closed.Contains((neighbor.x, neighbor.z)))
                    continue;

                float newG     = current.g + 1;
                Node  existing = open.Find(n => n.x == neighbor.x && n.z == neighbor.z);

                if (existing == null)
                {
                    neighbor.g      = newG;
                    neighbor.h      = Manhattan(neighbor, endNode);
                    neighbor.parent = current;
                    open.Add(neighbor);
                }
                else if (newG < existing.g)
                {
                    existing.g      = newG;
                    existing.parent = current;
                }
            }
        }

        return null; // No path found
    }

A few things worth noting:

The open list is a plain List. A textbook A* implementation uses a priority queue (min-heap) for O(log n) extraction of the lowest-cost node. For my maze sizes (up to 20×20 = 400 cells), a linear scan is fast enough and keeps the code simple. If you're pathfinding on large grids (100×100+), swap this for a SortedSet or a proper priority queue.

The closed set is a HashSet<(int, int)>. This gives O(1) lookups — much faster than a list for checking whether a cell has already been processed.

All step costs are 1. Since we're moving on a uniform grid (each cell is the same size), every step costs the same. If you had terrain weights (mud = 2, road = 0.5), you'd add those here.

FindPath returns null if no path exists. The caller should always check for null before using the result.

The heuristic: Manhattan distance

csharp

private float Manhattan(Node a, Node b)
    {
        return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.z - b.z);
    }

Manhattan distance counts how many steps it would take to reach the goal if there were no walls — purely horizontal and vertical moves. It's the correct heuristic for a grid where you can only move in four directions (no diagonals).

Why not Euclidean distance? Euclidean distance (sqrt(dx² + dz²)) would also work, but it can overestimate the true cost on a grid that doesn't allow diagonal movement. Manhattan distance is admissible — it never overestimates — which guarantees A* finds the optimal path.

The key insight: wall-aware neighbour detection

This is the part that makes this implementation specific to maze navigation. Standard grid A* checks whether adjacent cells exist and aren't obstacles. Maze A* needs to check whether the wall between two cells has been removed.

csharp

private List<Node> GetNeighbors(Node node)
    {
        List<Node> neighbors = new List<Node>();
        MazeCell cell = grid[node.x, node.z];

        // RIGHT — can we move to (x+1, z)?
        if (node.x + 1 < width && !cell.HasRightWall())
            neighbors.Add(new Node(node.x + 1, node.z));

        // LEFT — can we move to (x-1, z)?
        if (node.x - 1 >= 0 && !cell.HasLeftWall())
            neighbors.Add(new Node(node.x - 1, node.z));

        // FRONT — can we move to (x, z+1)?
        if (node.z + 1 < depth && !cell.HasFrontWall())
            neighbors.Add(new Node(node.x, node.z + 1));

        // BACK — can we move to (x, z-1)?
        if (node.z - 1 >= 0 && !cell.HasBackWall())
            neighbors.Add(new Node(node.x, node.z - 1));

        return neighbors;
    }

Each direction check has two parts:

Bounds check — don't walk off the edge of the grid
Wall check — only add the neighbour if the wall between them has been removed

This is why the MazeCell wall data is so important. The A* algorithm doesn't know the maze was generated with DFS — it just asks each cell "which of your walls are open?" and treats open walls as valid moves.

Converting back to world space

csharp

private List<Vector3> Retrace(Node endNode)
    {
        List<Vector3> path = new List<Vector3>();
        Node current = endNode;

        while (current != null)
        {
            Vector3 pos = new Vector3(
                current.x * cellSizeX + originOffset.x,
                0.5f,
                current.z * cellSizeZ + originOffset.z
            );
            path.Add(pos);
            current = current.parent;
        }

        path.Reverse();
        return path;
    }
}

Retrace walks backwards through the parent chain from the goal to the start, building a list of grid positions. Then it reverses the list so it runs start → goal.

Each grid position is converted to a world-space Vector3 by multiplying the cell index by the cell size and adding the origin offset. The y is hardcoded to 0.5f — the vertical centre of a floor-level cell in my scene.

The enemy AI then moves along this list of Vector3 waypoints using Vector3.MoveTowards.

How the enemy uses the path

Once FindPath returns a list of world-space positions, the enemy AI follows them in sequence:

csharp// In the enemy MonoBehaviour

private List<Vector3> path;
private int waypointIndex = 0;

void Update()
{
    if (path == null || waypointIndex >= path.Count) return;

    Vector3 target = path[waypointIndex];
    transform.position = Vector3.MoveTowards(
        transform.position,
        target,
        moveSpeed * Time.deltaTime
    );

    if (Vector3.Distance(transform.position, target) < 0.05f)
        waypointIndex++;
}

The pathfinder is called once when the enemy spawns (or when the player reaches a certain distance away) and the result is cached. For a small maze, recalculating every few seconds is totally fine — but you could also recalculate only when the player changes cells to save performance.

Putting it all together

Here is the flow from game start to enemy movement:

  1. MazeGenerator runs DFS
    → produces MazeCell[,] grid (walls carved)
    → instantiates wall GameObjects in the scene

  2. AStarPathfinder is constructed
    → receives the same MazeCell[,] grid
    → knows cell sizes and world origin

  3. Enemy spawns
    → calls FindPath(enemyGridPos, playerGridPos)
    → receives List of world waypoints

  4. Enemy MonoBehaviour
    → follows waypoints with MoveTowards
    → recalculates path every N seconds (or on player cell change)

The two systems share the MazeCell[,] grid as their single source of truth. The generator writes to it; the pathfinder reads from it. No intermediate data structures, no duplicate wall tracking.

Performance notes

For a 15×15 maze (225 cells), FindPath runs in under 1ms on device. That is fast enough to call every frame if needed, but I recalculate once every 2 seconds in practice, which keeps CPU usage negligible.

If you scale up to larger grids:

Replace the open list with a priority queue. The linear scan for the lowest-f node is O(n) per iteration. At 50×50+ cells this becomes noticeable. C# doesn't have a built-in min-heap, but you can use SortedSet with a custom IComparer, or pull in a priority queue from a NuGet package.
Use a pooled node allocator. Calling new Node() inside a tight loop allocates heap memory and can cause GC spikes. For frequent pathfinding, consider pooling nodes or using a struct-based approach.
Cache the path and recalculate on demand. Don't recalculate every frame unless the maze or player position changes.

What I learned

Building a custom pathfinder instead of relying on Unity's NavMesh taught me a few things:

NavMesh is not always the answer. For dynamic, procedurally generated environments, a grid-based pathfinder that understands your data model (in this case, MazeCell walls) can be simpler, faster, and more accurate than baking a mesh at runtime.

The heuristic matters more than the data structure — at small scales. At maze sizes I'm working with, swapping the list for a heap makes almost no measurable difference. Picking the right heuristic (Manhattan vs Euclidean) has a much bigger effect on path quality.

Wall-awareness is the whole point. Standard A* treats cells as either passable or impassable. Maze pathfinding is more nuanced — a cell is passable from some directions and not others. Encoding that in GetNeighbors rather than as a binary obstacle grid is cleaner and more accurate.

Wrapping up

Here is a summary of everything covered:

DFS generates a connected maze by carving walls between cells, tracked in a MazeCell[,] grid
AStarPathfinder reads that same grid and uses wall data (not obstacle maps) to determine valid moves
Manhattan distance is the correct admissible heuristic for a four-directional grid
Retrace converts grid indices back to world-space Vector3 waypoints
The enemy AI follows those waypoints with Vector3.MoveTowards

The full project — including the maze generator, enemy AI, and more — is in my portfolio:
https://fahadshahbaz.fun/works/AI-Maze-Game

If you have questions about the implementation or want to see the DFS generator code in full, drop a comment below. And if this helped you build something, I'd love to see it.

Abdul Fahad Shahbaz is a Unity game developer based in Lahore, Pakistan, specializing in mobile games, WebGL, and AR. Portfolio: fahadshahbaz.fun