在游戏开发中计时器/定时器是必须的,而且会在多处用到,如吃药补血每秒回10点且持续1分钟、玩家从一点到达另一点的过程需要多少时间。下面是定时器在七雄争霸中的几个应用场景,直接上图:
场景1:建筑升级时间 |
场景2:建筑升级时间 |
场景3:科技研究时间 |
类似的场景还有很多,就不一一列举了。但有一点可以肯定的就是,不可能每个地方都去new一个定时器各自管理,这样会消耗大量CPU和内存,从而导致游戏不流畅,画面卡卡的。所有一般游戏中都只维护一个全局的定时器,这也是本文的主要内容——ActionScript3页游开发中如何设计全局的定时器。
下面介绍如何设计游戏中全局的定时器,首先我们来看看常用的定时器设计。通常定时器具有以下功能:
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)。
我所在的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<!--CRLF-->
{<!--CRLF-->
import flash.events.TimerEvent;<!--CRLF-->
import flash.utils.Timer;<!--CRLF-->
import flash.utils.getTimer;<!--CRLF-->
<!--CRLF-->
public class MyTimer<!--CRLF-->
{<!--CRLF-->
private static var _instance:MyTimer;<!--CRLF-->
private var _timer:Timer;<!--CRLF-->
private var _timerList:Array;<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 获取单例类MyTimer的实例
<!--CRLF-->
* 返回值:
<!--CRLF-->
* _instance
<!--CRLF-->
*/
<!--CRLF-->
public static function getInstance():MyTimer<!--CRLF-->
{<!--CRLF-->
if (_instance == null)<!--CRLF-->
_instance = new MyTimer();
<!--CRLF-->
<!--CRLF-->
return _instance;
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 构造函数,用于防止单例类生成多个实例
<!--CRLF-->
*/
<!--CRLF-->
public function MyTimer()<!--CRLF-->
{<!--CRLF-->
if (_instance != null)<!--CRLF-->
trace("单例类,请不要实例化");
<!--CRLF-->
return;
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 注册计时器,首先检查id是否存在,如果不存在,就将定时器插入数组_timerList中;否则啥都不做
<!--CRLF-->
* 参数:
<!--CRLF-->
* id - 唯一标识一个定时器
<!--CRLF-->
* interval - 刷新间隔,单位为秒(s)
<!--CRLF-->
* repeatCount - 重复次数
<!--CRLF-->
* callback - 回调函数,每隔interval就执行一次
<!--CRLF-->
* ...args - 回调函数参数 ///注意,参数实际并没有用到,有待改进
<!--CRLF-->
* 返回值:空
<!--CRLF-->
*/
<!--CRLF-->
public function registerTimer(id:String, interval:int, repeatCount:int, callback:Function, ...args):void<!--CRLF-->
{<!--CRLF-->
if (_timerList == null)<!--CRLF-->
_timerList = new Array();
<!--CRLF-->
<!--CRLF-->
if ( check(id) == -1 )
<!--CRLF-->
{<!--CRLF-->
_timerList.push( { id:id, interval:interval, repeatCount:repeatCount, callback:callback, args:args, tempInterval:0 } );<!--CRLF-->
startTimer();<!--CRLF-->
}<!--CRLF-->
else
<!--CRLF-->
{<!--CRLF-->
trace(id + "已经存在!!!");
<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 注销计时器,首先检查id是否存在,如果存在,从数组_timerList中删除定时器
<!--CRLF-->
* 参数:
<!--CRLF-->
* id - 唯一标识一个定时器
<!--CRLF-->
* 返回值:空
<!--CRLF-->
*/
<!--CRLF-->
public function removeTimer(id:String):void<!--CRLF-->
{<!--CRLF-->
var index:int = check(id);<!--CRLF-->
if (index != -1)
<!--CRLF-->
{<!--CRLF-->
_timerList.splice(index, 1);<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 检查指定id的Object是否在_timerList数组中,
<!--CRLF-->
* 如果存在返回在_timerList数组中的索引;否则返回-1
<!--CRLF-->
* 参数:
<!--CRLF-->
* id - String,唯一标识一个定时器
<!--CRLF-->
* 返回值:
<!--CRLF-->
* -1 or 指定Object的索引
<!--CRLF-->
*/
<!--CRLF-->
private function check(id:String):int<!--CRLF-->
{<!--CRLF-->
var len:int = _timerList.length;<!--CRLF-->
<!--CRLF-->
for (var index:int = 0; index < len; index++)<!--CRLF-->
{<!--CRLF-->
if (_timerList[index]["id"] == id)<!--CRLF-->
{<!--CRLF-->
return index;
<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
return -1;
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 启动计时器
<!--CRLF-->
* 如果_timer为空,生成一个定时器Timer,事件发生间隔1000ms(1s);
<!--CRLF-->
* 监听TimerEvent.TIMER,处理函数为timerHandler
<!--CRLF-->
*/
<!--CRLF-->
private function startTimer():void<!--CRLF-->
{<!--CRLF-->
if (_timer == null)<!--CRLF-->
_timer = new Timer(1000);
<!--CRLF-->
if (!_timer.running)
<!--CRLF-->
{<!--CRLF-->
_timer.addEventListener(TimerEvent.TIMER, timerHandler);<!--CRLF-->
_timer.start();<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 停止计时器
<!--CRLF-->
* 当_timerList数组为空时,即没有用户注册定时器,停止_timer
<!--CRLF-->
*/
<!--CRLF-->
private function stopTimer():void<!--CRLF-->
{<!--CRLF-->
_timer.stop();<!--CRLF-->
_timer.removeEventListener(TimerEvent.TIMER, timerHandler);<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* 运行计时器
<!--CRLF-->
* 如果_timerList数组为空,调用stopTimer()停止计时器;
<!--CRLF-->
* 否则判断_timerList数组中的定时器间隔是否达到,
<!--CRLF-->
* 如果达到,就调用回调函数;
<!--CRLF-->
* 否则啥都不做
<!--CRLF-->
*/
<!--CRLF-->
public function runTimer():void<!--CRLF-->
{<!--CRLF-->
var timerComplete:Array = new Array();<!--CRLF-->
var len:int = _timerList.length;<!--CRLF-->
if (len == 0)
<!--CRLF-->
{<!--CRLF-->
stopTimer();<!--CRLF-->
return;
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
for (var i:int = 0; i < len; i++)<!--CRLF-->
{<!--CRLF-->
//运行MyTimer管理的所有计时器
<!--CRLF-->
<!--CRLF-->
_timerList[i]["tempInterval"] += 1;
<!--CRLF-->
//判断是否已经经过interval间隔
<!--CRLF-->
if (_timerList[i]["tempInterval"] == _timerList[i]["interval"])<!--CRLF-->
{<!--CRLF-->
//如果callback不空,执行callback函数
<!--CRLF-->
if (_timerList[i]["callback"] != null)<!--CRLF-->
{<!--CRLF-->
_timerList[i]["callback"](_timerList[i]["args"]);<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
_timerList[i]["tempInterval"] = 0;
<!--CRLF-->
<!--CRLF-->
//判断初始repeatCount是否=0,如果注册时为0,即无限次数
<!--CRLF-->
//否则每执行一次,就-1;然后判断repeatCount是否=0,如果=0就注销计时器
<!--CRLF-->
if (_timerList[i]["repeatCount"] != 0)<!--CRLF-->
{<!--CRLF-->
_timerList[i]["repeatCount"] -= 1;
<!--CRLF-->
if (_timerList[i]["repeatCount"] == 0)<!--CRLF-->
{<!--CRLF-->
trace("执行完成......");
<!--CRLF-->
timerComplete.push(_timerList[i]["id"]);
<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
//注销所有已完成的计时器
<!--CRLF-->
len = timerComplete.length;<!--CRLF-->
if (len != 0)
<!--CRLF-->
{<!--CRLF-->
trace("注销所有已经完成的计时器...");
<!--CRLF-->
for ( i = 0; i < len; i++)
<!--CRLF-->
{<!--CRLF-->
removeTimer(timerComplete.pop());<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
/*
<!--CRLF-->
* timerHandler是_timer的TimerEvent.TIMER事件处理函数
<!--CRLF-->
* 其中调用runTimer(),管理所有注册的计时器
<!--CRLF-->
*/
<!--CRLF-->
private function timerHandler(evt:TimerEvent):void<!--CRLF-->
{<!--CRLF-->
runTimer();<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
}<!--CRLF-->
测试代码:
package<!--CRLF-->
{<!--CRLF-->
import flash.display.Sprite;<!--CRLF-->
import flash.events.Event;<!--CRLF-->
import flash.utils.Timer;<!--CRLF-->
import flash.events.TimerEvent;<!--CRLF-->
<!--CRLF-->
public class Main extends Sprite<!--CRLF-->
{<!--CRLF-->
<!--CRLF-->
public function Main():void<!--CRLF-->
{<!--CRLF-->
if (stage) init();
<!--CRLF-->
else addEventListener(Event.ADDED_TO_STAGE, init);
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
private function init(e:Event = null):void<!--CRLF-->
{<!--CRLF-->
removeEventListener(Event.ADDED_TO_STAGE, init);<!--CRLF-->
// entry point
<!--CRLF-->
<!--CRLF-->
var timer:MyTimer = MyTimer.getInstance();
<!--CRLF-->
timer.registerTimer("1", 1, 15, tick);
<!--CRLF-->
<!--CRLF-->
var timer1:MyTimer = MyTimer.getInstance();
<!--CRLF-->
timer1.registerTimer("2", 5, 0, tick1);
<!--CRLF-->
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
private function tick(...args):void<!--CRLF-->
{<!--CRLF-->
trace("tick(1s)");
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
private function tick1(...args):void<!--CRLF-->
{<!--CRLF-->
trace("tick(5s)");
<!--CRLF-->
}<!--CRLF-->
/* private function complete(evt:TimerEvent):void
<!--CRLF-->
{
<!--CRLF-->
trace("complete...");
<!--CRLF-->
}*/
<!--CRLF-->
<!--CRLF-->
}<!--CRLF-->
<!--CRLF-->
}<!--CRLF-->
1. Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility
2. ActionScript3帮助文档