当前位置: 首页 > 编程日记 > 正文

走在网页游戏开发的路上(八)

游戏中定时器的设计

0.  前言

在游戏开发中计时器/定时器是必须的,而且会在多处用到,如吃药补血每秒回10点且持续1分钟、玩家从一点到达另一点的过程需要多少时间。下面是定时器在七雄争霸中的几个应用场景,直接上图:

image

image

image

场景1:建筑升级时间

场景2:建筑升级时间

场景3:科技研究时间

类似的场景还有很多,就不一一列举了。但有一点可以肯定的就是,不可能每个地方都去new一个定时器各自管理,这样会消耗大量CPU和内存,从而导致游戏不流畅,画面卡卡的。所有一般游戏中都只维护一个全局的定时器,这也是本文的主要内容——ActionScript3页游开发中如何设计全局的定时器。

1.  定时器的几种设计

下面介绍如何设计游戏中全局的定时器,首先我们来看看常用的定时器设计。通常定时器具有以下功能:

F  启动定时器

F  停止定时器

F  定时器定期执行间隔(总共执行多次)或者超时执行(总共执行1次)

F  有的游戏中还需要暂停定时器、恢复定时器的功能

关于游戏中的定时器的设计有以下两种争议:

1)        每个需要定时器的地方都创建一个,然后问题归结为多个定时器的管理问题;

2)        游戏中只有一个定时器,然后问题归结为一个定时器实现多个定时器的效果。

实际从管理难度以及运行效率上来讲应该选择第2种。

image

image

START_TIMER = O(1)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(n)

START_TIMER = O(n)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

image

image

START_TIMER = O(log(n))

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

START_TIMER = O(n)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

image

START_TIMER = O(1)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

image

(每个桶中元素有序)

START_TIMER = 最坏O(n)、平均O(1)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

(每个桶中元素无序)

START_TIMER = O(1)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = 最坏O(n)、平均O(1)

image

START_TIMER = O(m)m是轮子的数量

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

上面几种定时器设计可以总结为使用4种数据结构实现:HeapListHashWheel。著名的ACE中的定时器按照这4种方式分别都给与了实现,具体的4种定时器都是从ACE_Timer_Queue_T继承,每种定时器用不同的数据结构来实现具体Timer的算法。

1ACE_Timer_Heap定时器,根据触发时间建立一个优先级队列(一个最小堆数据结构)来维护所有的定时器,代价就是删除和插入过程为O(logn),代价比较高。

2ACE_Timer_List定时器,根据触发时间建立一个有序的双向链表,代价就是插入定时器代价较高。

3ACE_Timer_Hash定时器,采用开链的Hash方式每一个桶为一个单链表,在检查所有桶超时的时候会遍历链表所有的元素。为了提高效率这里所用的Hash桶应该足够大,而对于定时器一般是频繁的超时响应定时器,已经插入和删除,响应会采用迭代的方式。所以效率并不是那么高效。

4ACE_Timer_Wheel定时器,采用的一种时间轮的方式,具体实现就好象一个轮子上面有很多插槽,每一个插槽下面包括一个有序双向链表,在Ace中把轮子叫做Wheel,插槽叫做Spoke,每一个定时器被HashSpoke中,而Spoke也可以理解为timer的分辨率,而Spoke的计算公式为:(触发时间 >> 分辨率的位数)&(spoke大小-1)。然后在根据触发时间把定时器插入到每一个Spoke的有序双向链表中,与Ace_timer_Hash的实现类似,只是这里用户可以指定Spoke大小。这里代价就是插入的时候可能最坏为O(n)

2.  定时器的简单实现

我所在的Flash网页游戏项目中使用了简单的实现方式,游戏中只有一个定时器,然后问题归结为一个定时器实现多个定时器的效果。定时器使用ActionScript3中的Timer类。

定时器类(Timer Class)是ActionScript 3.0的内置类,通过AS3的事件分发响应机制实现周期触发。定时器是一个简单却又极为常用的类,系统全面的掌握它是非常必要的。(摘自ActionScript3 帮助文档)

Timer定时器是精确的,但是定时器的执行结果并非绝对精确。无论是Flash还是Flex,最终的应用程序都是以SWF文件存储。而FlashPlayer在解释SWF文件时,会建立基于帧率的周期循环。每次舞台更新的时间间隔是固定的,脚本中的舞台操作会受到时间轴帧率的制约。

作为一个多线程的应用程序,FlashPlayer 执行脚本不需要依赖帧率,但是所有的屏幕输出都要借助FlashPlayer的渲染引擎。如果时间轴帧率为10,则运行时舞台每100毫秒播放一帧。当间隔为80毫秒的定时器触发时,SWF应用程序立刻执行该定时器的侦听函数,但是在定时器侦听函数中的任何屏幕操作,都不会及时的反应在舞台上。只有在100毫秒时,FlashPlayer才会更新舞台显示。定时器在160毫秒第二次触发时,SWF应用程序需在200毫秒更新舞台显示。理论上8000毫秒内执行100次定时器,但实际上在帧率为10SWF应用中,舞台更新只有80次。有可能在舞台刷新间隔内,连续执行两次定时器操作。

定时器的触发事件间隔可以自由设置,所有的定时器事件都不会错过。屏幕显示虽然不是实时更新,但是由于刷新的速度很快,不会造成显著影响。实际上,任何语言的定时器都要受制于系统时钟,都不是绝对精确的。

上述原因也是我们采用定时器的简单实现方式的原因之一,下面上代码。

定时器:

package  
{    
    import flash.events.TimerEvent;
    import flash.utils.Timer;
    import flash.utils.getTimer;
    
    public class MyTimer 
    {
        private static var _instance:MyTimer;        
        private var _timer:Timer;
        private var _timerList:Array;
        
        /*
         * 获取单例类MyTimer的实例 
         * 返回值:
         *         _instance
         */        
        public static function getInstance():MyTimer 
        {
            if (_instance == null)
                _instance = new MyTimer();
            
            return _instance;
        }
        
        /*
         * 构造函数,用于防止单例类生成多个实例
         */
        public function MyTimer() 
        {
            if (_instance != null)
                trace("单例类,请不要实例化");
            return;
        }
        
        /*
         * 注册计时器,首先检查id是否存在,如果不存在,就将定时器插入数组_timerList中;否则啥都不做
         * 参数:
         *         id - 唯一标识一个定时器
         *         interval - 刷新间隔,单位为秒(s)
         *         repeatCount - 重复次数
         *         callback - 回调函数,每隔interval就执行一次
         *         ...args - 回调函数参数 ///注意,参数实际并没有用到,有待改进
         * 返回值:空
         */
        public function registerTimer(id:String, interval:int, repeatCount:int, callback:Function, ...args):void
        {
            if (_timerList == null)
                _timerList = new Array();
                
            if ( check(id) == -1 )
            {
                _timerList.push( { id:id, interval:interval, repeatCount:repeatCount, callback:callback, args:args, tempInterval:0 } );
                startTimer();
            }
            else
            {
                trace(id + "已经存在!!!");
            }
        }
        
        /*
         * 注销计时器,首先检查id是否存在,如果存在,从数组_timerList中删除定时器
         * 参数:
         *         id - 唯一标识一个定时器
         * 返回值:空
         */
        public function removeTimer(id:String):void
        {
            var index:int = check(id);
            if (index != -1)
            {
                _timerList.splice(index, 1);    
            }
        }
        
        /*
         * 检查指定id的Object是否在_timerList数组中,
         * 如果存在返回在_timerList数组中的索引;否则返回-1
         * 参数:
         *         id - String,唯一标识一个定时器 
         * 返回值:
         *         -1 or 指定Object的索引
         */
        private function check(id:String):int
        {
            var len:int = _timerList.length;            
            
            for (var index:int = 0; index < len; index++)
            {
                if (_timerList[index]["id"] == id)
                {
                    return index;
                }
            }            
            
            return -1;
        }
        
        /*
         * 启动计时器
         * 如果_timer为空,生成一个定时器Timer,事件发生间隔1000ms(1s);
         * 监听TimerEvent.TIMER,处理函数为timerHandler
         */        
        private function startTimer():void
        {
            if (_timer == null)
                _timer = new Timer(1000);
            if (!_timer.running)
            {
                _timer.addEventListener(TimerEvent.TIMER, timerHandler);
                _timer.start();
            }
        }
        
        /*
         * 停止计时器
         * 当_timerList数组为空时,即没有用户注册定时器,停止_timer
         */
        private function stopTimer():void 
        {
            _timer.stop();
            _timer.removeEventListener(TimerEvent.TIMER, timerHandler);
        }
 
        /*
         * 运行计时器
         * 如果_timerList数组为空,调用stopTimer()停止计时器;
         * 否则判断_timerList数组中的定时器间隔是否达到,
         *         如果达到,就调用回调函数;
         *         否则啥都不做
         */        
        public function runTimer():void
        {
            var timerComplete:Array = new Array();
            var len:int = _timerList.length;            
            if (len == 0)
            {
                stopTimer();
                return;
            }
            
            for (var i:int = 0; i < len; i++)
            {
                //运行MyTimer管理的所有计时器
                
                _timerList[i]["tempInterval"] += 1;
                //判断是否已经经过interval间隔
                if (_timerList[i]["tempInterval"] == _timerList[i]["interval"])
                {
                    //如果callback不空,执行callback函数
                    if (_timerList[i]["callback"] != null)
                    {
                        _timerList[i]["callback"](_timerList[i]["args"]);
                    }
                    
                    _timerList[i]["tempInterval"] = 0;
                    
                    //判断初始repeatCount是否=0,如果注册时为0,即无限次数
                    //否则每执行一次,就-1;然后判断repeatCount是否=0,如果=0就注销计时器
                    if (_timerList[i]["repeatCount"] != 0)
                    {
                        _timerList[i]["repeatCount"] -= 1;
                        if (_timerList[i]["repeatCount"] == 0)
                        {
                            trace("执行完成......");
                            timerComplete.push(_timerList[i]["id"]);
                        }
                    }
                }
            }
            
            //注销所有已完成的计时器
            len = timerComplete.length;
            if (len != 0)
            {
                trace("注销所有已经完成的计时器...");
                for ( i = 0; i < len; i++)
                {
                    removeTimer(timerComplete.pop());
                }
            }
        }
            
        /*
         * timerHandler是_timer的TimerEvent.TIMER事件处理函数
         * 其中调用runTimer(),管理所有注册的计时器
         */
        private function timerHandler(evt:TimerEvent):void
        {
            runTimer();
        }
    }
}

测试代码:

package 
{
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    
    public class Main extends Sprite 
    {
        
        public function Main():void 
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
        }
        
        private function init(e:Event = null):void 
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            // entry point
            
            var timer:MyTimer = MyTimer.getInstance();
            timer.registerTimer("1", 1, 15, tick);
            
            var timer1:MyTimer = MyTimer.getInstance();
            timer1.registerTimer("2", 5, 0, tick1);    
            
        }
        
        private function tick(...args):void
        {
            trace("tick(1s)");
        }
 
        private function tick1(...args):void
        {
            trace("tick(5s)");
        }
/*        private function complete(evt:TimerEvent):void
        {
            trace("complete...");
        }*/
        
    }
    
}

参考文献

1. Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility

2. ActionScript3帮助文档

3. 一个高效的定时器分析及设计

相关文章:

[epoll]epoll理解

转自&#xff1a;http://blog.51cto.com/yaocoder/888374 1. 流 首先我们来定义流的概念&#xff0c;一个流可以是文件&#xff0c;socket&#xff0c;pipe等等&#xff0c;可以进行I/O操作的内核对象&#xff0c;不管是文件&#xff0c;还是套接字&#xff0c;还是管道&#x…

[kuangbin带你飞]专题五 并查集 E - 食物链 (带权并查集)

E - 食物链 题目链接&#xff1a;https://vjudge.net/contest/66964#problem/E 动物王国中有三类动物A,B,C&#xff0c;这三类动物的食物链构成了有趣的环形。A吃B&#xff0c; B吃C&#xff0c;C吃A。 现有N个动物&#xff0c;以1&#xff0d;N编号。每个动物都是A,B,C中的一种…

关于内网linux系统如果安装nodejs,npm,express,mongodb,forever等

内网的linux系统要安装nodejs以及express等系列的框架&#xff0c;因为系统是局域网和互联网是物理隔离的&#xff0c;所以&#xff0c;没法像官网的安装教程那样直接install了&#xff0c;只能手动安装&#xff0c;这里已经我们自己的linux 系统suse10 为例&#xff1a; 1 No…

【基础知识】如何在word中粘贴出漂亮整洁的代码

使用工具&#xff1a; notepad、WPS 操作实现&#xff1a; 1、右击代码文件使用NPP打开文件 2、选中要复制的代码 3、如图所示&#xff0c;依次点击如下内容 4、直接粘贴到word中&#xff0c;如图

浅析SQL Server数据修复命令DBCC的使用

SQL Server数据库提供了修复命令DBCC&#xff0c;当SQL Server数据库遭到质疑或者是有的无法完成读取时可以尝试用此命令来修复。以下是一些常见的DBCC修复命令&#xff0c;希望会给读者带来帮助。 1. DBCC CHECKDB 重启服务器后&#xff0c;在没有进行任何操作的情况下&#x…

Python之Mysql及SQLAlchemy操作总结

一、Mysql命令总结 1.创建库 create database test1; 2.授权一个用户 grant all privileges on *.* to feng% identified by 1qazWSX; 3.创建表 create table Teacher(teaId int not null, teaname varchar(100), age int, sex enum(M, F), phone int); 4.查询 select * from t…

NSwagStudio for Swagger Api

本案例主要说明如何使用NSwag 工具使用桌面工具快速生成c# 客户端代码、快速的访问Web Api。 NSwagStudio 下载地址 比较强大、可以生成TypeScript、WebApi Controller、CSharp Client 1、运行WebApi项目 URL http://yourserver/swagger 然后你将看到界面如下 1.1 Web API 代…

【html】如何解决标签设置成超链接后字体格式及颜色变化的问题

问题描述&#xff1a; 如图所示&#xff0c;将一个标签设置成超链接后字体颜色和格式会发生改变&#xff0c;如果我只想让它保持原来的格式应该怎么办&#xff1f; 解决方法&#xff1a; 在a标签中添加一个属性&#xff1a; style"color:inherit;" 添加后的代码&…

js判断 IE 浏览器

1 $.browser.msie && ($.browser.version 6.0) 转载于:https://www.cnblogs.com/zhupinglei/archive/2012/04/28/2475186.html

UNIX编程笔记:关于停止的进程接收信号的问题

为什么80%的码农都做不了架构师&#xff1f;>>> 因为资料缺少&#xff0c;按照测试得出来&#xff0c;停止状态的进程貌似只对SIGCONT有反应&#xff0c;而别的默认就是忽略。 转载于:https://my.oschina.net/kut/blog/27736

脱壳 VMProtect 1.70.4

【文章标题】: 脱壳 VMProtect 1.70.4 【文章作者】: hxqlky【作者邮箱】: zmunlkygmail.com【作者主页】: http://www.x5dj.com/hxqlky【下载地址】: 自己搜索下载【加壳方式】: VMProtect 1.70.4【保护方式】: VMProtect 1.70.4【编写语言】: MASM32 / TASM32 【使用工具】:…

互联网协议详解

本文转载自&#xff1a;https://www.cnblogs.com/111testing/p/6942585.html 目录&#xff1a;&#xff1a;&#xff1a;&#xff1a;&#xff1a;&#xff1a; 一、网络协议 二、TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09; TCP头格式…

【java】巨菜博主安装jdk为什么每次都失败?

今天到公司实习第一天&#xff0c;博主兴高采烈地的使用起来的公司配备的电脑&#xff0c;第一步是干啥&#xff1f;当然是安装JDK了&#xff0c;博主平生安装JDK次数数不胜数&#xff0c;但一遍整下来没有任何差错的情况少之又少。今天也不例外&#xff0c;多敲了个空格害我足…

怎么在vs2010中使用ActiveX Test Container(转)

ActiveX Test Container Application is Still Available(转) Hello, I’m Pat Brenner, a developer on the Visual C Libraries team. I’ve noticed some posts on various forums lamenting the loss of the ActiveX Test Container application and I wanted to address …

C#自定义控件四简易时钟

C#自定义控件四简易时钟效果图&#xff1a;简易时钟&#xff0c;顾名思义&#xff0c;简单容易&#xff0c;简单到什么程度呢&#xff1f;界面只有数字和指针&#xff0c;甚至连与当前时间都不能匹配&#xff01;呵呵&#xff01;就这么简单&#xff0c;学习嘛&#xff0c;从简…

GitLab 配置邮箱

设置 SMTP 发送邮件 这里以腾讯企业邮箱为例&#xff0c;其他邮箱可以参考 设置 SMTP 发送邮件。 SMTP 和 POP3/IMAP 协议 SMTP 负责发送邮件&#xff0c;POP3/IMAP 负责接收邮件。其中 IMAP 基本上替换掉了 POP3。 用户在使用客户端&#xff08;例如 Foxmail&#xff09;时&am…

在 Ubuntu Natty 中解除系统托盘限制

在 Ubuntu 11.04 Natty 中&#xff0c;Ubuntu 对顶部面板右上角的通知区域&#xff08;系统托盘&#xff09;采用了白名单制度&#xff0c;只有支持 Indicators 并位于白名单的部分程序才会被显示在系统托盘中&#xff0c;目前支持的程序有&#xff1a; Java apps, Mumble, Win…

Oracle10g客户端远程连接数据库全过程[转]

最近项目用到了oracle&#xff0c;使用的是oracle10g&#xff0c;因为小组内有多人使用数据库&#xff0c;并且oracle数据库很占内 存&#xff0c;就放在单独的一台服务器上&#xff0c;所以最好每个人都装一个oracle10g的客户端。那么客户端到数据库的远 程访问时免不了的了。…

【css】页面出现两个滚动条以及只有一半页面显示内容的解决方法

可能当修改页面的margin等属性时会出现页面只有一半的页面显示内容的情况&#xff0c;此时我们可以修改css代码来解决问题 代码实现&#xff1a; body{overflow:hidden}html{/*overflow-y:scroll;*/ }html{overflow: auto; } 注意&#xff1a;该代码为css代码&#xff0c;需…

Microsoft Dynamics Marketplace

微软对一些产品提供网上销售第三方插件/解决方案的站点叫做 Marketplace&#xff0c;比如 Windows Phone Marketplace, Dynamics Marketplace.这样可以帮助合作伙伴/客户提供一个网上的产品交流平台&#xff0c;Microsoft Dynamics Marketplace 针对微软CRM/ERP 产品&#xff0…

【hdu】4521 小明序列【LIS变种】【间隔至少为d】

题目链接&#xff1a;https://vjudge.net/contest/228455#problem/B 转载于&#xff1a;https://blog.csdn.net/a709743744/article/details/51765252 题目大意&#xff1a; 求最长上升子序列&#xff0c;其中子序列中相邻的两个数的下标差要超过k 解题分析&#xff1a; 子序列…

【bootstrap】bootstrap-4.5.0-example 各个模板展示

前言&#xff1a;博主做前端开发的时候经常用到bootstrap&#xff0c;但挑选模板的时候&#xff0c;需要一个一个的打开文件夹、打开html文件再查看模板是否合适&#xff0c;这样实在有点浪费时间&#xff0c;所以今天博主将各个页面截图展示出来&#xff0c;之后方便大家也方便…

HDU1053 Entropy 哈夫曼树

题目链接&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1053 认真读题&#xff0c;别怕题长&#xff0c;此题考查的就是哈夫曼树并求出最小编码值&#xff0c;注意每一次要将数组清0&#xff0c;否则会出错&#xff01; AC代码&#xff1a; #include<iostream>…

C++用数组和链表分别实现Queue

C用数组和链表分别实现Queue 昨天写了《C用数组和链表分别实现Stack》&#xff0c;今天就是《C用数组和链表分别实现Queue》&#xff0c; 队列就是先来的先被处理掉&#xff0c;后来的就等&#xff0c;直到成为先来的&#xff0c;实现起来感觉和栈差不多。 模板好用的&#xff…

bzoj1150 [CTSC2007]数据备份Backup

大概就是写了道生日礼物那个不知道叫啥的贪心。。。。。 大概就是说这道题和那个比较像。。。 所以留着看看吧&#xff0c;哪天想起了回来做这道题咯~ 转载于:https://www.cnblogs.com/LLppdd/p/9051440.html

004本周总结报告

这一周总的来说并没有学到多少东西。只是学习了java数组相关的知识&#xff0c;发现和C/C中的数组基本一样&#xff0c;同时也了解到堆内存和栈内存的概念。在学习数组时发现java数组的length属性很好用&#xff0c;学习了数组的插入赋值&#xff0c;冒泡和选择排序等并用数组的…

JS保留两位小数

JS保留两位小数 对于一些小数点后有多位的浮点数&#xff0c;我们可能只需要保留2位&#xff0c;但js没有提供这样直接的函数&#xff0c;所以我们得自己写函数实现这个功能&#xff0c;代码如下&#xff1a; function changeTwoDecimal(x) { var f_x parseFloat(x); if…

【资源分享】The Beatles(披头士)乐队所有专辑带封面

资源免费分享&#xff0c;送给各位披头士的粉丝。只求个赞可以吗。 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 链接:https://pan.baidu.com/s/1N5BXA18JeaznYhRRy6kiAw 提取码:5439

Serial Communications in Win32

http://msdn.microsoft.com/en-us/library/ms810467.aspx http://hi.baidu.com/beisika/blog/item/b204d58f6c3bece9513d9297.html

platform_driver_register适配的两种方式及probe是否启动与硬件关系

platform_driver_register2种方式学习 1.platform_device_register与platform_driver_register配合使用&#xff1a; 实例代码摘自下述网址&#xff1a; 这样当两个name一样时&#xff0c;就会嗲用mt65xx_leds_probe这个函数了。 static struct platform_driver mt65xx_leds_d…