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

游戏中定时器的设计

0.  前言

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




场景1:建筑升级时间


场景2:建筑升级时间


场景3:科技研究时间

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

1.  定时器的几种设计

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

F  启动定时器

F  停止定时器

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

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

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

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

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

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




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)




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)



START_TIMER = O(1)

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)



(每个桶中元素有序)

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)



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

STOP_TIMER = O(1)

PER_TICK_BOOKKEEPING = O(1)

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

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

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

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

4)ACE_Timer_Wheel定时器,采用的一种时间轮的方式,具体实现就好象一个轮子上面有很多插槽,每一个插槽下面包括一个有序双向链表,在Ace中把轮子叫做Wheel,插槽叫做Spoke,每一个定时器被Hash到Spoke中,而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次定时器,但实际上在帧率为10的SWF应用中,舞台更新只有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...");
        }*/
        
    }
    
}
时间: 2024-10-26 22:51:06

走在网页游戏开发的路上(八)的相关文章

大家快来玩转盘抽奖游戏(走在网页游戏开发的路上(七))

.  抽奖流程 其实我们的Flash只是一个显示作用,要转到哪个位置(中哪个奖品)是后台来完成的.而且每个奖品的概率是不同的,不是等概率的,我想没有转盘抽奖游戏是等概率的.从玩家点击"抽奖"开始到结束,与后台的交互如下: 转盘抽奖的大致流程是这样的: F  玩家点击Flash中的"抽奖"按钮: F  Flash调用web页面中的Javascript函数,告诉它玩家开始抽奖了.当然Flash调用JS的时候是带了参数的,比如是谁在抽奖等详细信息: web页面中的Java

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

AS3之类 0.  前言 类:面向对象的基础,类是对象的抽象表示形式,类用来存储有关对象可保存的数据类型及对象可表现的行为的信息. 类的定义: [dynamic] [public | internal] [final] class className [ extends superClass ] [ implements interfaceName[, interfaceName... ] ] {     // 此处是类定义 } 在ActionScript 3.0中,可使用以下四个属性之一来修饰

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

游戏中的背景音乐和声效 0.  前言 不管是大型客户端游戏还是轻量级的网页游戏,游戏中背景音乐和声效是必不可少的.好的背景音乐.声效会给游戏增色,本文不从策划/设计等角度去考虑,只从程序实现上面讲在网页游戏开发中如何去实现背景音乐.声效.背景音乐和声效有以下几个要求: ü  背景音乐与声效是分开的,可以独立设置开关 ü  背景音乐一般循环播放一直存在 ü  声效点击才触发,这种声音任何时候只播放一个,如果两个瞬间点击多个按钮,只播放最后一个声音 为了使背景音乐和声效分开,可以使用不同的声道来播放

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

起步 --此系列谨记录我步入页游开发队伍的历程. 0.写在前面 相信有很多和我一样的人,曾多次问google.问baidu.问各大论坛--如何开发游戏?开发游戏如何入门?由于游戏开发本身其复杂.庞大.涉及东西比较多,始终不得其道,最终激情无情的被时间这把杀猪刀给磨灭.之后又一次激情澎湃,又一次不了了之-- 本人喜欢玩游戏,也有幸在研究生毕业能够加入腾讯QQ游戏开发部门,本系列将记录如何步入网页游戏开发的历程.此系列,至少是目前阶段,主要关注如何使用ActionScript 3.0开发网页游戏(本

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

游戏中的图像资源 当今游戏早已不再是黑白机的时代,游戏都由色彩丰富.精致的图像,流畅的动画构成.Flash游戏也不例外,Flash既支持矢量图又支持位图,他们各有优缺点.本文的目的即是介绍何时使用矢量图,何时使用位图,如何在两者之间权衡? 1.    前言 首先让我们了解一下何谓矢量图,何谓位图,及各自的优缺点.这些内容与游戏无直接关系,但是了解他们的差异有助于我们在游戏中如何选择. 1.1 矢量图 矢量图(摘自:百度百科)使用直线和曲线来描述图形,这些图形的元素是一些点.线.矩形.多边形.圆和

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

AS3之函数 0.  前言 函数:完成某个目标任务的代码块,它是代码重用的最小单位. 函数是可在ActionScript中调用的基本代码单位.ActionScript中用户定义的函数和内置函数都由Function对象来表示,该对象是Function类的实例. 类的方法与Function对象略有不同.与普通函数对象不同,方法和与其关联的类对象紧密关联.因此,方法或属性具有在同一类的所有实例中共享的定义.可以从实例提取方法并将其处理为"绑定"方法(保留与原始实例的链接).对于绑定方法,th

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

AS基础过关 0.  ActionScript简介 ActionScript是Macromedia(现已被Adobe收购)为其Flash产品开发的,最初是一种简单的脚本语言,现在最新版本3.0,是一种完全的面向对象的编程语言,功能强大,类库丰富,语法类似JavaScript,多用于Flash互动性.娱乐性.实用性开发,网页制作和RIA应用程序开发. ActionScript是一种基于ECMAScript的脚本语言,可用于编写Adobe Flash动画和应用程序.由于ActionScript和Ja

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

AS3事件模型 --AS3的灵魂之一 0.  前言 ActionScript 3.0事件模型使用方便,而且符合标准,它与Adobe Flash Player显示列表(display list)完美集成在一起.ActionScript 3.0的事件模型是基于DOM 3的事件规范[1],是业界标准的事件处理体系结构,为ActionScript 3.0程序员提供了强大而直观的事件处理工具. 为了清晰理解AS3事件模型,我们必须首先知道什么是事件模型?事件模型组成?DOM3事件模型? 1.  事件模型及

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

Flash动画原理 --动画是将静止的画面变为动态的艺术.实现由静止到动态,主要是靠人眼的视觉残留效应.利用人的这种视觉生理特性可制作出具有高度想象力和表现力的动画影片. 0.  前言 像所有的动画显示一样,Flash的动画原理也是通过不断的刷新屏幕,利用每次屏幕上显示对象位置的不同.大小色彩的变化等产生动画效果.动画编程的关键是一定要有变化,而且该变化需要在一定时间内来完成(以达到欺骗人眼,使分解的画面连续起来,达到运动的效果).Flash中使用帧频来控制每秒钟刷新屏幕的次数,通过使用的帧频的