性能优化3-分帧寻路+寻路任务统一管理

news2025/1/19 2:59:22

前言

当项目里的地图越来越大,一些性能上的问题开始逐渐出现,比如寻路。玩家在操控角色移动的时候,指引需要实时更新,同时一些npc也需要做移动,容易出现cpu占用率短时间过高,甚至掉帧的情况。

去年底的时候,由于希望在性能优化方面做一些研究,在论坛找到了江南百景图研发负责人 其中提到了分帧寻路+寻路任务统一管理的优化思路。

针对地图很大、建筑物和人物都很多的情况下,这些算法一起执行就会很损耗性能。所以我们用了 分时寻路 ,就是把寻路过程由一帧分到若干帧去进行计算,这样就不会在某一个时间段集中进行大量运算,对游戏性能也不会有太大的影响。

统一管理寻路任务,同一时间只为一个角色服务。也许有人会问,那岂不是一个角色在哪里走、其他对象都在那边等着?其实真正在游戏里不会有这种奇怪的表现。首先每个角色寻路的起始和结束时间都不一样,再者这个同一时间是非常短的,就等于把角色寻路分配到了不同帧里,交替进行执行。

感觉方案很有意思,便尝试做了出来。寻路算法基于a*实现,只支持四方向移动。a*算法在网上有大量文章,文末贴了一篇文章链接,本文中不会细说。

开发环境

浏览器:Chrome

开发语言:JavaScript

引擎版本:CocosCreator 2.4.3

词语缩写对照

分帧寻路:本文提到的优化技术,同分时寻路。

开放队列:a*算法中的开放队列(open list)。

研究过程

分享文章中讲的很清楚,就是把寻路这件事情拆成好几段,每帧做一段。

在a*算法中,每次会从开放队列中取出一个坐标,我们需要修改这里的代码,限制取坐标的最大数量

从开放队列取坐标时,需要保证每次取出来的都是最有价值的坐标(离终点最近),地图尺寸过大时, 保证坐标的正确取出也是不可忽视的损耗点。我们可以使用优先队列降低性能损耗,但js中没有内置的实现,所以这里我们还需要实现一个优先队列。

分享文章中提到,他们还将寻路任务进行统一管理。当某帧有多个寻路任务时,仍会出现单帧时间过长的问题,所以还是一步到位吧!

一般情况下,同屏npc且需要同时寻路的数量不会特别多,所以我们实现一个队列型的任务管理器即可。

实现思路

需求可大致拆分为如下步骤:

  1. 实现提交寻路任务的入口函数,存储寻路任务相关数据(起点、终点、障碍物等)。
  2. 实现寻路函数。
    1. 取出需要执行的任务,若任务为空则退出。
    2. 执行一次寻路,判断是否超出最大步数,是则退出,否则循环步骤b。
    3. 若完成寻路,加载下一个的任务。
  1. 新建寻路管理类,在游戏中持有寻路管理类,并每帧调用寻路函数。

方案涉及文件

文件名

说明

Game.js

游戏类,持有并调用寻路管理器

PathFinder.js

寻路管理类,实现分帧寻路、任务信息存储等功能

PriorityQueue.js

简易版优先队列

方案涉及类

类名

声明于

说明

PathFinder

PathFinder.js

寻路管理类

RoadPoint

PathFinder.js

路径点类,存储路径点的位置、父路径点等信息,并计算cost值

FindRoadTask

PathFinder.js

寻路任务类,存储寻路的相关数据如碰撞信息、起点终点等

PriorityQueue

PriorityQueue.js

简易版优先队列

实现代码解析

首先实现提交寻路任务的函数。若当前无任务则立即执行,有任务则加入等待列表

// PathFinder.js
/**
 *  添加一个寻路任务。此函数为外部调用入口
 * @param {FindRoadTask} task 寻路任务 
 */
addFindRoadTask(task) {
    if (!this._finding) {
        this._startFindRoadTask(task);
    } else {
        this._taskList.push(task);
    }
}

接着实现开始寻路任务的函数。取出并缓存寻路任务的相关数据,如最大步数、起点、终点等。

// PathFinder.js
/**
* 开始执行寻路任务
* @param {FindRoadTask} task 寻路任务 
*/
_startFindRoadTask(task) {
    const { maxWalkPerFrame } = task.config;
    
    this._finding = true;
    this._nextRoadPointId = 0;
    this._maxWalkPointAmount = maxWalkPerFrame || Number.MAX_VALUE;
    
    // 重置任务相关数据,存储task相关数据
    this._resetData(task);
}

实现由外部调用的更新函数,应被每帧调用。持续从开放队列中取出路径点,超出最大每帧寻路次数时停止。

// PathFinder.js
/**
* 此函数应由外部引用者每帧调用
*/
update() {
    if (this._finding) {
        this._findPath();
    }
}
/**
* 执行一次寻路
*/
_findPath() {
    let walkPointAmount = 0;
    
    while (walkPointAmount++ < this._maxWalkPointAmount) {
        // 无可到达的路径点 寻路失败
        if (!this._waitQueue.length) {
            this._findFail();
            break;
        }
        
        // 访问下一个最优先路径点
        const point = this._waitQueue.poll();
        this._onWalkPoint && this._onWalkPoint(point);
        const success = this._visitRoadPoint(point);
        
        if (success) {
            this._findSuccess();
            break;
        }
    }
}

寻路完成时,开始下一个寻路任务

// PathFinder.js
/**
* 寻路任务结束回调。不论寻路成功或失败都会调用本函数
*/
_onFindOver() {
    if (!!this._taskList.length) {
        this._startFindRoadTask(this._taskList.shift());
    } else {
        this._finding = false;
    }
}

最后,在Game中持有寻路管理类,每帧调用寻路管理类的update函数

// Game.js
onLoad() {
    this._pathFinder = new PathFinder();
},
update(dt) {
    this._pathFinder.update();
}

实现起来其实并不难。寻路的具体实现这里没有贴出来,可以自行实现或在源码中查看。

使用范例

在Game中创建寻路任务,并调用addFindRoadTask函数提交给寻路管理类。通过最后一个参数中的maxWalkPerFrame属性标记每帧最大访问路径点数量。当寻路完成时会调用onFindFinish函数, 并通过参数传递完整路径。

// Game.js
start() {
    // 测试用的起点、终点坐标
    let playerPos = { x: 2, y: 2 };
    let targetPos = { x: 70, y: 95 };

    // 读取地图的“wall”图层 初始化碰撞信息
    const { width, height } = this.tileMap.getLayers()[0].getLayerSize();
    const walls = new Array(height).fill(0).map(() => new Array(width).fill(false));
    const wallLayer = this.tileMap.getLayers().find((layer) => layer.getLayerName() === "wall");
    for (let i = 0; i < height; i++) {
        for (let j = 0; j < width; j++) {
            if (wallLayer.getTileGIDAt(j, i)) {
                walls[i][j] = true;
            }
        }
    }
    // tiledmap坐标原点为左上角 需要反转y轴
    walls.reverse();

    let tsak1 = new FindRoadTask(playerPos, targetPos, walls, this.onFindFinish.bind(this));
    this._pathFinder.addFindRoadTask(tsak1);
}
/**
 * 寻路结束回调
 * @param {[RoadPoint]} path 路径点列表
 */
onFindFinish(path) {
    if (path) {
        for (const point of path) {
            const { x, y } = point;
            const node = cc.instantiate(this.nodePoint);
            this.nodePointParent.addChild(node);
            this.setNodePos(node, x, y);
        }
    } else {
        console.log("寻路失败");
    }
}

寻路结束时打印路径。this.nodePoint是一个蓝色小圆点,onFindFinish函数中会根据路径在地图上生成若干小圆点。

代码上传于git仓库,有需要可自取。比较简陋,以测试功能为主。

为了方便观察效果,游戏中有一个蓝色方块和一个红色方块,蓝色表示起始位置,红色表示终点位置。另外还有一个‘find’按钮,可以将游戏类update中对于寻路管理类的调用挪到按钮的点击事件中,实现手动模拟分帧的效果(点一下一帧)。

演示项目的地图中,灰色图块表示路,黑色图块表示障碍物。

效果对比

测试案例

一张100*100,方块大小32*32的地图。

同一起点,寻路四次,同一帧内提交任务。起点位于地图右下角区域,四个目标点分别位于地图四个角区域。

文字还是不够直观,上图吧!#狗头

数据为在游戏中统计前三秒(180帧)的寻路函数耗时,耗时并不是精准的,但可以作为参考。横轴为帧,纵轴为寻路函数的耗时,单位为ms。

图中有三条不同颜色的折线,其中:

蓝色:不设置最大寻路次数(将会每帧执行一个寻路任务)。

绿色:设置每帧最大寻路次数为50

黄色:设置每帧最大寻路次数为100

可以明显看出,不设置寻路次数时,前4帧的耗时明显偏高设置最大次数的绿色和黄色曲线相对来说更平缓

什么?你说没有原始对照组?看着蓝色线想象一下,一帧里发生两个5ms耗时的寻路任务会怎么样🤣。

由于最大寻路次数配置的不同,黄色线大约在80帧左右,耗时逐渐贴近0,绿色线则是在155帧左右。每帧执行多少次比较合适还得结合实际情况进行设置。

总结

  1. 大地图下的寻路是不可避免的性能问题,使用分帧寻路可以压平cpu的曲线,避免因为寻路导致的占用率上升甚至掉帧等情况。
  2. 同时间有多个寻路任务时,将按照队列顺序处理,先进先出。此方法足够满足一般寻路需求,若希望优化任务执行时的优先级,还可以参考cpu调度算法进行优化。
  3. 项目中把最大寻路次数的配置放到了提交任务时,每个任务可以配置不同的最大次数。也可以直接将次数限制直接写在管理器中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/434981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT,音乐,与数据库

小编君是个不务正业&#xff0c;喜欢搞跨界&#xff0c;干啥啥不成的DBA&#xff0c;大概在十一年前就有个不成熟的妄念&#xff0c;能否用计算机来写音乐&#xff1f; ▌用ChatGPT来搞音乐&#xff1f; 音乐是一个个的音符&#xff0c;按照乐理规则排列的。音符之间是否和谐…

横扫一线大厂面试的高并发笔记到底有多硬核?

处处需要高并发 “为什么Java面试必问高并发&#xff1f;” 这个问题已经让程序员们倍感头疼&#xff0c;尤其是想要跳槽到更大公司的程序员&#xff0c;能否漂亮的回答高并发的问题已经成为求职者是否是一个优秀程序员的评判标准&#xff0c;大厂面试尤为明显。 不得不说&am…

springMvc(二)

一、SSM整合 前面我们已经把 Mybatis 、 Spring 和 SpringMVC 三个框架进行了学习&#xff0c;今天主要的内容就是把这三个 框架整合在一起完成我们的业务功能开发&#xff0c;具体如何来整合&#xff0c;我们一步步来学习。 1.1 流程分析 (1) 创建工程 创建一个Maven的we…

BUUCTF-PWN-ciscn_2019_n_1

1.溢出到shell位置 下载文件 checksec file 发现是64位的 然后 放入ida 查看字符串 发现 cat flag 然后看看 F5反编译 我们发现了system 然后get()函数 我们可以使用栈溢出 我们看看 v1函数的地址 是30h 然后是64位的 所以 基地址是8字节 我们开始查看 system的地址 所以…

Web3 营销:项目方如何使用空投奖励以及激励真实用户

Apr. 2023, Daniel 在GameFi中&#xff0c;举行成功的空投活动是最难做到的事情之一。 虽然今年Arbitrum和Blur的活动再次使空投成为一个热门话题&#xff0c;但通过空投赚取收益造成了一个机器人问题&#xff0c;即空投者使用多个账户来进入游戏系统。 Trading volume on Bl…

【LeetCode】剑指 Offer 61. 扑克牌中的顺子 p298 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/ 1. 题目介绍&#xff08;61. 扑克牌中的顺子&#xff09; 从若干副扑克牌中随机抽 5 张牌&#xff0c;判断是不是一个顺子&#xff0c;即这5张牌是不是连续的。2&#xff5e;10为数字本身…

活动需求中灵活使用Redis提升生产力

抽奖 一堆用户参与进来&#xff0c;然后随机抽取几个幸运用户给予实物/虚拟的奖品&#xff1b;此时&#xff0c;开发人员就需要写上一个抽奖的算法&#xff0c;来实现幸运用户的抽取&#xff1b;其实我们完全可以利用Redis的集合&#xff08;Set&#xff09;&#xff0c;就能轻…

汽车CAN、LIN汇总

目录&#xff1a; 一、准备知识 1、什么是CAN 2、汽车网络发展时间轴 3、如何通信 4、CAN总线结构 1&#xff09;ISO 11898 2&#xff09;CAN 和 J1850的比较 3&#xff09;CAN 和 UART的比较 5、关于节点 1&#xff09;什么是节点 2&#xff09;节点&#xff1a;报文传…

Jdbc开发结构

一、导入jar包&#xff0c;放置lib目录下&#xff0c;需要的有&#xff1a; &#xff08;1&#xff09;alibaba连接池&#xff08;druid&#xff09; &#xff08;2&#xff09;apache工具类&#xff08;commons-dbutils&…

网络协议-HTTP入门和基础工具链

Http协议介绍 发明者&#xff1a;蒂姆伯纳斯-李 英国计算机科学家&#xff08;1955-&#xff09; 万维网&#xff08;1990HTTP协议&#xff09; 世界第一个浏览器 第一个服务端程序 创办MIT人工智能实验室 HTTP协议 超文本传输协议&#xff08;Hyper Text Trabsfer Prot…

一文带你搞清 ChatGPT 与 Azure OpenAI 的区别

这两周是我从2017年开始全职涉入 NLP 领域后最忙的两周&#xff0c;无数的同事和客户都在向我提出一个询问&#xff1a;ChatGPT 可以帮到我们什么&#xff1f; 特别是在2023年3月31日我做了一场微软 Azure OpenAI [布局助力企业]拥抱新智能时代的演讲之后&#xff0c;这几天我…

信号系统中使用的继电器

继电器是什么 继电器是一种电气开关&#xff0c;它使用电磁力来控制一个或多个电气电路的操作。继电器通常由电磁铁、触点和弹簧等部件组成。当电磁铁被激活时&#xff0c;它会产生磁场&#xff0c;吸引或释放触点&#xff0c;从而打开或关闭电路。 继电器的分类 继电器分为…

CSS学习(5) - 布局

文章首发于我的个人博客&#xff1a;欢迎大佬们前来逛逛 文章目录 CSS布局display属性width和max-widthposition 属性溢出浮动和清除floatclear 布局案例 CSS布局 display属性 display 属性是CSS布局的最重要的属性。 display属性规定是否/如何显示元素。 display元素通常与…

Python词云

词云图wordcloud 1.安装第三方库 j i e b a 库、 m a t p l o t l i b 、 w o r d c l o u d 库 jieba库、matplotlib、wordcloud库 jieba库、matplotlib、wordcloud库 2.过程 1.使用 j i e b a jieba jieba 库对数据进行分词整理&#xff0c;转为 t x t txt txt文件&#…

AI和ML:数据中心的新前沿创新和优化

数据中心现在正在将人工智能(AI)和机器学习(ML)技术集成到其基础架构中&#xff0c;以保持竞争力。通过在传统数据中心架构中实施人工智能驱动层&#xff0c;企业可以创建自主数据中心&#xff0c;无需人工干预即可优化和执行通用数据工程任务。 随着对数据处理和存储的需求持续…

【行为型模式】策略模式

文章目录 1、简介2、结构3、实现方式3.1、案例引入3.2、结构分析3.3、具体实现 4、对比模板方法模式5、策略模式优缺点6、应用场景 1、简介 策略模式(Strategy)是一种设计模式&#xff0c;它允许在运行时根据需要选择算法的行为。这个模式将每个算法封装到一个类中&#xff0c…

Oracle VM VirtualBox安装开放麒麟桌面版本操作

1.环境 Oracle VM VirtualBox版本6.1.18 开放麒麟桌面版本openkylin 0.0.5 https://mirror.lzu.edu.cn/openkylin-cdimage/yangtze/openkylin-0.9.5-x86_64.iso 1.创建新虚拟电脑 ql 并将ios导入 然后点击启动 注意&#xff1a; vm box如果鼠标设置不当的话 基本上不可能完成…

PEIS源码 体检源码 医院体检系统源码

PEIS体检管理系统源码 PEIS源码 体检源码 医院体检系统源码 本套PEIS医院体检管理系统源码&#xff0c;采用C#语言开发&#xff0c;C/S架构&#xff0c;前台开发工具为Vs2012&#xff0c;后台数据库采用oracle大型数据库。有演示。 文末获取联系 PEIS体检管理系统适用于大中型…

鹅厂狂招工程师,国产自研芯片“沧海”斩获8项世界第一

前言 4月17日&#xff0c;腾讯云官方披露&#xff0c;在由莫斯科国立大学举办的最新一届MSU硬件视频编码比赛中&#xff0c;腾讯自研的编解码芯片“沧海”&#xff0c;经过数月的严格测试&#xff0c;获得了所参加的两个赛道8项评分的全部第一。 MSU为视频压缩领域最具影响力…

TensorFlow-GPU【易安装】(全网最全、通俗易懂、小白友好)

写在前面&#xff1a;CSDN的小伙伴们&#xff0c;很长时间没有发文了&#xff0c;自从靠运气侥幸考上研究生&#xff0c;就一直在苦苦寻找自己的研究方向。在跟风“随大流”之后&#xff0c;选择了深度学习这一领域&#xff0c;也是一场噩梦的开始&#xff01; 为了更好的学习吴…