Qt/C++实现的程序演示了使用激光雷达构建地图的整个过程,创建随机地图,使用上下左右键来控制小车的运动,小车在运动的过程中右上角会实时创建以点组成的地图,以点的形式显示小车周围的地图,右下角显示小车在场景中的方向,鼠标可以在地图添加图块障碍物,每一步都会有日志进行记录。 源码: 在Windows和Ubuntu 18.04平台下使用Qt5.13.1_MinGW、Qt5.15.1_MinGW编译运行通过,其他版本请自行尝试。

敲下方向键的瞬间,红色小车在屏幕中央划出一道轨迹。右上角的点云地图像墨水晕染般展开,右下角的指南针指针微微颤动——这个用Qt/C++实现的激光雷达模拟器,把SLAM的趣味性直接拉满。

地图生成器藏在Map类里玩骰子:

void Map::generateRandomMap(int width, int height) {
    std::uniform_real_distribution<double> dist(0, 1);
    for(int x=0; x<width; x+=10) {
        for(int y=0; y<height; y+=10) {
            if(dist(rng) < 0.15) {  // 15%概率生成障碍物
                addObstacle(QPointF(x + 5, y + 5)); // 在格子中心生成
            }
        }
    }
}

这段代码用均匀分布随机撒点,10x10的网格里每隔15%的概率种下障碍物。注意这里有个骚操作:障碍物坐标加了5像素偏移,让障碍物刚好落在网格中心,避免后续碰撞检测时出现边缘问题。

键盘事件处理直接挂钩主窗口:

void MainWindow::keyPressEvent(QKeyEvent *event) {
    const double step = 3.0; // 步长别太大,会穿墙
    QPointF delta(0, 0);
    
    switch(event->key()) {
    case Qt::Key_Left:  delta.rx() -= step; break;
    case Qt::Key_Right: delta.rx() += step; break;
    case Qt::Key_Up:    delta.ry() -= step; break;
    case Qt::Key_Down:  delta.ry() += step; break;
    }
    
    if(!map->checkCollision(carPos + delta)) { // 预判碰撞
        carPos += delta;
        updateLidarScan(); // 触发雷达扫描
    }
}

方向键处理看似简单,但checkCollision里的射线检测才是精髓。注意移动前先做碰撞预测,避免小车卡进墙里——这点在游戏开发里也是常见套路。

激光雷达的模拟扫描藏着三角函数狂欢:

void Lidar::scan(const QPointF& pos, double yaw) {
    points.clear();
    for(int angle = 0; angle < 360; angle += 5) { // 5度步进扫描
        double rad = qDegreesToRadians(angle + yaw);
        QPointF dir(qCos(rad), qSin(rad));
        
        for(double r=0; r<maxRange; r+=0.5) {
            QPointF p = pos + dir * r;
            if(map->hasObstacle(p)) {
                points.append(p);
                break; // 找到障碍物就停
            }
        }
    }
}

这段代码模拟了激光束的物理传播过程。角度循环里的0.5步长是个平衡点——太小了计算量爆炸,太大了会漏检。实际工程中可能需要根据帧率动态调整。

实时点云绘制用QGraphicsScene实现起来异常简单:

void MapView::updatePointCloud(const QList<QPointF>& points) {
    scene->clear();
    for(auto& p : points) {
        scene->addEllipse(p.x(), p.y(), 2, 2, QPen(), QBrush(Qt::blue));
    }
    // 顺便画个小车位置
    scene->addEllipse(carPos.x()-5, carPos.y()-5, 10, 10, 
                     QPen(Qt::red), QBrush(Qt::darkRed));
}

虽然每次全量清空重绘有点暴力,但在点数不多的情况下完全hold住。如果要优化,可以用对象池复用已有的图形项。

鼠标交互的障碍物添加直接挂钩场景的鼠标事件:

connect(scene, &QGraphicsScene::mousePressEvent, [this](QGraphicsSceneMouseEvent* event){
    if(event->button() == Qt::LeftButton) {
        QPointF pos = event->scenePos();
        map->addObstacle(pos); // 添加障碍物
        log("用户添加障碍物于 " + QString::number(pos.x()) + ", " + QString::number(pos.y()));
    }
});

Lambda表达式让事件处理变得直白。注意这里用scenePos而不是屏幕坐标,避免转换失误。

编译时遇到找不到QtCharts的问题?去pro文件里加一句:

QT += core gui widgets charts

这个坑笔者踩过三次才长记性。运行效果嘛——就像在玩简化版的《元气骑士》,只不过地图是你自己亲手“扫”出来的。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐