2021-11-03

浅谈游戏开发中update的设计与使用

在最近开发游戏中,对内存和CPU的兴趣比较集中,上期发现了一个针对js内存的变大的问题,这期我们就来聊聊CPU,不好的地方需要有缘人指点一番。
按照我的理解,内存和CPU对于程序来说,就是空间和时间的概念,内存的大小决定了程序的容积,CPU的速度决定了程序执行的效率。以往的开发中,程序员会根据内存和CPU做平衡,因为内存和CPU都是有限的,合理利用资源,是空间换时间,还是时间换空间,那么就要看具体的问题。随着现在内存越来越大,CPU做的越来越好,时空转换的问题看似已经不复存在,但是往往在代码层面去使用一些算法和数据结构,可以有事半功倍的效果。
我们这次讨论的一个话题就是关于update,在服务器开发中,update是一个定时器,每帧检测需要触发的行为,我们这里假定内存中有1000个player对象的数据,每个player需要检测各种状态(我觉得是懒人写法),这样就会衍生出以下代码:

//PlayerManager是所有player的管理器,里面players存放了所有内存玩家的数据信息,下面的update受总定时器控制,每帧运行一次,而players里面的每一个对象在这一帧内都要跑完,每个player对象的update里要检测很多状态
PlayerManager.prototype.update = function () {

    let self = this;
    for(let pid in self.players){
        let player = self.players[pid];
        player.update();
    }
};

//player是玩家各种触点检测,上面player.update()直接调取的就是下面的这个方法
Player.prototype.update = function () {

    let player = this;
    let curTime = new Date().getTime();
    //条件:检测体力恢复点
    if(player.getData('下次体力恢复时间戳') <= curTime){
		function1();
    }
    //条件:检测皮肤过期
    if(player.getData('当前皮肤过期时间戳') <= curTime){
        function2();
    }
    //条件:检测在线奖励
    if(player.getData('下次在线奖励领取时间戳') <= curTime){
       function3();
    }
    //.....条件:剩下还有很多需要检测的触点行为
};

分析以上的代码,假如1000个玩家内存中,按照以上的写法就是,每帧for循环要走1000次,可能这一秒没有触发任何条件,当然CPU运行1000次的确不会消耗什么,假如内存中有10000个玩家,或者10万个,然后再加上各种其他的运算量比较大功能在运行,那么CPU的压力是显而易见了,CPU长时间的高负载,会带来网络层阻塞,内存的GC异常,各种随之而来的问题可想而知。
为了解决上的问题,那么我们就来引入了事件管理的概念,这个数据结构不仅可以解决空循环的问题,效率简直成N倍的增长,仅仅是消耗了一些内存来而已。同时需要转换一下程序执行的思维,不能按照上面的写法了。
我先来简单描述一下大概的思路,10000个players当中的某个player(下面简称玩家a)在某个时间消耗了一点体力,然后需要在5分钟之后恢复一点体力,那么我可以根据当前时间计算出5分钟之后的时间戳,那么我们给这个事件定义成事件1,并且赋给事件1一些基础属性,玩家a的pid,将要触发的点的时间戳,将要执行的function1(),同时在玩家a身上挂上事件1的id;又过了几秒玩家a领取了在线奖励,那么我们给这个事件定义成事件2,并且赋给事件2一些基础属性,玩家a的pid,将要触发的点的时间戳function2(),同时在玩家a身上挂上事件2的id;同时刻玩家b领取了在线奖励,那么我们给这个事件定义成事件3,并且赋给事件3一些基础属性,玩家b的pid,function1(),同时在玩家b身上挂上事件1的id;随着在线玩家的活跃越发的频繁,事件越来越多,那么我们就需要来管理这些事件,我们简称事件管理器,如下图所示:
事件管理器示意图
有了事件管理器,那么我们就准确的记录了所有玩家将要执行行为,不需要每次遍历所有的player了,而是单独遍历这些将要执行的事件,事件的排序是按照时间进行排序的。(假如我们不修改成事件管理器的写法,用遍历player的方法,10000个玩家每帧遍历10000次,每个玩家里面还有多个条件判断,假设有5个条件判断,不管是否会走到判断内部,那么就是每帧判断的次数为10000*5=50000次;按照事件管理器的遍历,某一帧10000个玩家全部触发这些事件才能达到50000次,正常情况下,可能每次只执行寥寥无几的几个事件,甚至没有,这CPU节省的不是一般的多,简直无法形容了)。下面就是示意代码:

//listEvent里面就是时间管理器的各个事件
PlayerManager.prototype.update = function () {

    let self = this;
    let curTime = new Date().getTime();
    //维护self.listEvent表,为什么要写在执行的上面,细品
    for(let id of self.needDelete){
        delete self.listEvent[id]
    }
    //虽然是obj但是id的执行还是按照从小到大的顺序执行的
    for(let id in self.listEvent){
        let event = self.listEvent[id];
        if(event.time > curTime){
            return
        }
        let player = self.players[event.pid];
        //执行具体事件方法
        event.fuc(player);
        //将已经执行过的事件id加入删除列表
        self.needDelete.push(id)
    }
};

以上只是一个解决问题的思路,我们是用空间置换了时间,listEvent会额外消耗内存来存储,但是带来的CPU的效率极大的提升。
完成以上 self.listEvent的封装,需要配套出一些相应的函数,比如增加函数,比如更新函数,比如删除函数,比如服务器启动时事件的搜集(这是个大功能,需要对每个事件从player身上进行拆分,并且写入事件管理器),下面就用删除来举例,假如一个玩家自然恢复体力上限是100点,他因为参加某种玩法,消耗了5点体力,还剩95点,按照我们写法,会将n分钟后恢复m点体力的事件写入事件管理器,并在玩家的身上的记录了事件的id。后来玩家突然自己购买了5点体力(或者吃了1个体力丹回复了5点体力),这时候体力已经满了,不需要再恢复了,但是我们已经写入了事件管理器,需要进行删除,解决这类的问题有2种方法,第一种就是找到事件id,从对应的事件管理器中删除,第二种就是在事件的fuc方法中加入判断条件,进行校验执行,不符合的不执行。个人比较倾向于第一种+第二种。如果玩家player身上有多组事件id,那么删除的时候,还需要分析将要删除的是哪个。
上面就是一个典型的时间空间转换的案例,如有不恰当的地方还请大佬指点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值