You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于C# WPF在多楼层图片中实现A*算法的室内寻路技术咨询

在C# WPF中实现多楼层室内图片的A*寻路系统

嘿,这个需求我之前做类似项目的时候折腾过,刚好可以给你梳理出一套可行的步骤,从地图预处理到多楼层寻路的实现,一步步来:

一、先搞定楼层地图的预处理

首先,你不能直接拿原始楼层图片喂给A*——算法需要明确知道哪里能走、哪里是障碍。所以第一步是把图片转换成可寻路的节点网格

  • 步骤1:标记障碍区域
    你可以用画图工具(比如PS)给楼层图里的墙、柱子这些障碍涂上特定颜色(比如纯黑色),或者做个WPF交互工具让用户手动标记障碍。之后在代码里,加载图片后遍历每个像素,判断是否属于障碍:

    // 示例:从Image控件获取像素数据,生成对应楼层的寻路网格
    private List<List<Node>> GenerateFloorGrid(Image floorImage, int gridSize, int floorNumber)
    {
        var bitmap = new BitmapImage(new Uri(floorImage.Source.ToString()));
        int rows = bitmap.PixelHeight / gridSize;
        int cols = bitmap.PixelWidth / gridSize;
        var grid = new List<List<Node>>();
    
        for (int y = 0; y < rows; y++)
        {
            var row = new List<Node>();
            for (int x = 0; x < cols; x++)
            {
                // 取网格中心的像素颜色作为判断依据
                int pixelX = x * gridSize + gridSize / 2;
                int pixelY = y * gridSize + gridSize / 2;
                Color pixelColor = GetPixelColor(bitmap, pixelX, pixelY);
                // 假设纯黑色代表障碍区域
                bool isObstacle = pixelColor == Colors.Black;
                row.Add(new Node(x, y, floorNumber, !isObstacle));
            }
            grid.Add(row);
        }
        return grid;
    }
    
    // 辅助方法:获取BitmapImage指定位置的像素颜色
    private Color GetPixelColor(BitmapImage bitmap, int x, int y)
    {
        if (x < 0 || x >= bitmap.PixelWidth || y < 0 || y >= bitmap.PixelHeight)
            return Colors.Black;
        var pixels = new byte[4];
        bitmap.CopyPixels(new Int32Rect(x, y, 1, 1), pixels, 4, 0);
        return Color.FromRgb(pixels[2], pixels[1], pixels[0]);
    }
    

    这里的Node类要包含寻路所需的基础信息:

    public class Node
    {
        public int X { get; set; } // 网格X坐标
        public int Y { get; set; } // 网格Y坐标
        public int Floor { get; set; } // 所属楼层编号
        public bool IsWalkable { get; set; } // 是否可行走
        public Node Parent { get; set; } // A*算法中的父节点(用于回溯路径)
        public int G { get; set; } // 起点到当前节点的实际代价
        public int H { get; set; } // 当前节点到终点的启发估算值
        public int F => G + H; // 总代价(G+H)
    
        public Node(int x, int y, int floor, bool isWalkable)
        {
            X = x;
            Y = y;
            Floor = floor;
            IsWalkable = isWalkable;
        }
    }
    
  • 步骤2:建立坐标映射
    WPF控件的坐标(比如Canvas的坐标)要和你的网格坐标对应上,这样后面绘制路径时才能精准匹配到图片位置。比如网格节点(x,y)对应的Canvas坐标可以计算为:new Point(x*gridSize + gridSize/2, y*gridSize + gridSize/2)

二、实现适配多楼层的A*算法核心

标准A*是单平面的,你需要扩展它支持跨楼层的节点跳转:

1. 核心A*寻路逻辑

public List<Node> FindPath(Node startNode, Node endNode, Dictionary<int, List<List<Node>>> allFloorGrids)
{
    var openList = new List<Node>(); // 待探索节点列表
    var closedList = new HashSet<Node>(); // 已探索节点集合
    openList.Add(startNode);

    while (openList.Count > 0)
    {
        // 找到当前F值最小的节点(优先探索代价最低的路径)
        var currentNode = openList.OrderBy(n => n.F).First();
        openList.Remove(currentNode);
        closedList.Add(currentNode);

        // 到达终点,回溯生成完整路径
        if (currentNode.X == endNode.X && currentNode.Y == endNode.Y && currentNode.Floor == endNode.Floor)
        {
            return RetracePath(startNode, currentNode);
        }

        // 获取当前节点的所有邻居(包括跨楼层的电梯/楼梯节点)
        var neighbors = GetNeighbors(currentNode, allFloorGrids);
        foreach (var neighbor in neighbors)
        {
            if (!neighbor.IsWalkable || closedList.Contains(neighbor))
                continue;

            int newCostToNeighbor = currentNode.G + GetDistance(currentNode, neighbor);
            if (newCostToNeighbor < neighbor.G || !openList.Contains(neighbor))
            {
                neighbor.G = newCostToNeighbor;
                neighbor.H = GetDistance(neighbor, endNode);
                neighbor.Parent = currentNode;

                if (!openList.Contains(neighbor))
                    openList.Add(neighbor);
            }
        }
    }
    return null; // 找不到可行路径
}

// 回溯父节点生成完整路径
private List<Node> RetracePath(Node startNode, Node endNode)
{
    var path = new List<Node>();
    var currentNode = endNode;

    while (currentNode != startNode)
    {
        path.Add(currentNode);
        currentNode = currentNode.Parent;
    }
    path.Add(startNode);
    path.Reverse(); // 反转成从起点到终点的顺序
    return path;
}

// 计算曼哈顿距离(室内寻路用这个比欧几里得更贴合实际)
private int GetDistance(Node a, Node b)
{
    int dx = Math.Abs(a.X - b.X);
    int dy = Math.Abs(a.Y - b.Y);
    // 跨楼层额外增加固定代价(模拟电梯/楼梯的耗时)
    int floorCost = Math.Abs(a.Floor - b.Floor) * 10;
    return dx + dy + floorCost;
}

2. 处理跨楼层邻居节点

关键是GetNeighbors方法,要识别电梯/楼梯这类跨楼层节点,并返回对应楼层的连接节点:

private List<Node> GetNeighbors(Node currentNode, Dictionary<int, List<List<Node>>> allFloorGrids)
{
    var neighbors = new List<Node>();
    int currentFloor = currentNode.Floor;
    var currentGrid = allFloorGrids[currentFloor];

    // 先添加上下左右四个方向的平面邻居
    int[] dx = { -1, 1, 0, 0 };
    int[] dy = { 0, 0, -1, 1 };
    for (int i = 0; i < 4; i++)
    {
        int newX = currentNode.X + dx[i];
        int newY = currentNode.Y + dy[i];
        if (newX >= 0 && newX < currentGrid[0].Count && newY >=0 && newY < currentGrid.Count)
        {
            neighbors.Add(currentGrid[newY][newX]);
        }
    }

    // 检查当前节点是否是跨楼层节点(比如电梯/楼梯)
    if (IsCrossFloorNode(currentNode))
    {
        // 遍历所有楼层,添加对应楼层的同类型跨楼层节点
        foreach (var targetFloor in allFloorGrids.Keys)
        {
            if (targetFloor == currentFloor) continue;
            var targetGrid = allFloorGrids[targetFloor];
            // 假设每个楼层的电梯都在网格(5,5)位置,实际要根据你的地图定义
            var crossNode = targetGrid[5][5];
            if (crossNode.IsWalkable)
            {
                neighbors.Add(crossNode);
            }
        }
    }

    return neighbors;
}

// 自定义判断:当前节点是否为跨楼层节点
private bool IsCrossFloorNode(Node node)
{
    // 这里可以根据你的地图规则来判断,比如固定网格坐标、标记特殊属性等
    return node.X == 5 && node.Y == 5;
}

三、在WPF中绘制寻路路径

拿到A*返回的路径节点后,需要在楼层图片上把路径可视化出来。通常用Canvas叠加在Image控件上方实现:

1. XAML布局示例

<Grid>
    <Image x:Name="FloorImage" Source="/Assets/Floors/Floor1.png" Stretch="Uniform"/>
    <Canvas x:Name="PathCanvas" Background="Transparent"/>
</Grid>

2. 绘制路径的代码

private void DrawPath(List<Node> path, int gridSize)
{
    PathCanvas.Children.Clear();
    if (path == null || path.Count == 0) return;

    var polyline = new Polyline();
    polyline.Stroke = new SolidColorBrush(Colors.Red);
    polyline.StrokeThickness = 3;
    polyline.StrokeLineJoin = PenLineJoin.Round;

    foreach (var node in path)
    {
        // 把网格坐标转换成Canvas控件的像素坐标
        double canvasX = node.X * gridSize + gridSize / 2;
        double canvasY = node.Y * gridSize + gridSize / 2;
        polyline.Points.Add(new Point(canvasX, canvasY));
    }

    PathCanvas.Children.Add(polyline);
}

四、多楼层切换的交互处理

你可以做一个楼层选择控件(比如ComboBox),切换时加载对应楼层的图片和预先生成的网格。当起点和终点在不同楼层时,A*会自动找到跨楼层的路径——比如先走到当前楼层的电梯,再跳转到目标楼层的电梯,最后走到终点。

一些实用优化小Tips

  • 网格大小调整:网格太细会增加计算量,太粗会降低路径精度,建议根据图片尺寸选50x50或30x30像素的网格。
  • 障碍动态更新:如果需要支持用户临时添加/移除障碍,可以在Canvas上做点击交互,然后更新对应网格节点的IsWalkable属性。
  • 路径缓存:对于频繁查询的起点终点组合,可以缓存路径结果,避免重复计算。

内容的提问来源于stack exchange,提问作者Cutie

火山引擎 最新活动