[转载]Silverlight游戏开发心得(1)——调度器

[转载]Silverlight游戏开发心得(1)——调度器 – 游戏开发日志–向恺然 – 博客园.

前言:

说实话,很久没有正儿八经写东西了,都不知道咋写了。如果不幸看到这篇文章的哥们们,凑合看吧。

我这个比不了深蓝的东西,也不是教程。只能说是拾人牙慧,人云亦云吧,从各种 书籍里汲取自己能看得懂的东西,结合自己的开发经验,弄成自己的一套东西。《游戏编程精粹》、《游戏人工智能编程案例精粹》(这两本是邮电出版的)《游戏 编程中的人工智能技术》(这本是清华的),都是些不错的书。如果您已经看过而且理解,建议不要浪费时间再往下看了。个人的经验,这个过程很重要。在一些新 领域,新技术,比如Silverlight 技术,往往没有什么现成的范例可以模仿;即便有相像的东西,结合到具体的应用,也是千差万别的。

我自己的体会:学习知识的时候要低调——潭水之所以深不可测,正是因为所处的 位置极低——不要带有成见,耐心地,小心地汲取他人的经验,批判地继承,认真地分析,从中学到本质的东西为我所用;使用的时候要大胆一些,哪怕你写出来的 东西,简陋糟糕,完整的,有用的代码比 优雅的理论要好得多。不担三分险,难练一身胆;不要期望一次就写出来完美的代码,现在的简陋就是明天优秀的开始。我是鼓励大家大胆地对学习到的东西进行改 造,大刀阔斧地进行改造,不断地尝试各种方法。在这个过程中会有更深的理解过程,同时也会有自己的东西产生。

游戏架构:

提到架构,引擎,总透着一股高深莫测的味道。这个问题要分两面看,首先,架 构,引擎确实是好东西,也是很多高人前辈心血的总结,会为我们的开发带来莫大的好处,这是毋庸置疑的。但同时,这些东西是可以学习的,或许学习的曲线会有 些陡峭,但是绝不是不可理解的。

有个哥们这么说过“这类东西常常被冠上一个看似很深奥的名字,但是仔细看过以 后,你就会发现这不是我一直在用的东西么?”用这句话来给架构引擎做个注脚倒挺合适。

我们开发系统,总会有一个结构的,即便你没有意识到这点。

游戏开发是一个难度很高的事情,逻辑复杂,对系统的性能要求很高,不断有新鲜 的,好玩的点子涌现出来,而这些点子对开发者来说都是 一场场的噩梦。所以如果有一个灵活的架构,将是非常好的事情,也很幸运。

几年前初接触Silverlight 游戏开发的时候,学习到了一种游戏的开发方式。

(图1. 这是在公司里做技术交流的时候发言的PPT)

类似雷电的一个小游戏,在游戏里驱动一个主循环,在主循环里面驱动场景的循 环,里面好像是些地图不断地滚动,上面还有些云彩什么的,在场景里的维护者一个游戏对象列表,驱动这个列表里的所有对象进行循环,这些游戏对象继承自一个 基类,最后做的事情就是驱动这些对象循环。然后整个游戏就会优雅地跑起来,看上去也很酷。这些小飞机们,都会有自己的“智能”,能判断周围发生的一切,这 都源于他们每隔30ms就循环一次。

这样基于轮询的方式其实这样也不坏,也能够做很多的事情,在目前的硬件水平 下,运行的也不慢。

但随着对游戏的期望值不断增高,不满足于做些小飞机了,希望有更酷的动画,更 多的状态,更高的智能;

比如这样的东东:(我还没想好给这个游戏起个什么名字,要是有朋友有兴趣,帮 起一个?)

在原有的基础上进行改进的余地已经不大了,最后一层循环里的游戏对象,越老越复 杂,类像吹气球一样的膨胀起来。而且总觉得,有事没事都要循环一下,看看有没有什么事情发生,似乎总是有着“低效”的嫌疑。(插一句,似乎在游戏里的运行 证明,这也没什么,仍然可以让游戏运行的很好)。在这种情况下,有必要寻求更多的变化和帮助。在编程里面我信奉这样的原则:面向抽象编程,而不是面向实现 编程。has-a 比 Is-a 更好。于是有了新的架构设计

(图上标注的是更加好的游戏架构,如果写成更加灵活的价格更合适。)

我没有办法应用“电梯原则”解释这张图。(和客户做电梯从一楼到11楼,这点 时间内把项目说清楚)对于这种结构化的方式,可以构造的很合理,各部分和谐美妙的工作在一起;也可以为了结构化而结构化,画出来很酷的结构示意图,结果是 一团乱麻缠绕在一起。我建议大家这样看吧:把所有的箭头都去掉。你仅仅理解说,整个系统被划分成了:调度器,实体,实体消息处理,数据,状态机,状态机管 理…. 或者更干脆一点,简单一句话:系统被划分成了各种部分。一篇文章介绍整个系 统是不现实的,那样只会泛泛而谈,收获不大。就像这篇文章提到的《共 享的精神》,我会把这些年的SilverlightGame 开发经验,在几个月内和大家分享的。

首先让我们先介 绍一下调度器吧。

在《游戏编程精粹3》里面有这样一段话:

1.1      调度游戏中的事件

一个调度其能有效帮助以下游戏技术的实现,他们包括物理仿真,人物运动,碰撞检测,游戏中的人工智能,渲染。在所有这些技术中有一个关键问题就是时间。在 不同的时间里,当数百个不同的物体和过程都需要更新时,这些仿真技术的非常多种东西变得非常复杂。

调度器的重要能力在于他能够动态地增加和删除物体,这能使新物体非常平滑地加入到游戏里面去,和其他游戏里面的物体一起参加仿真,然后在不必的时候从调度 里面把他删除。

1.1.1 调度器的组成

调度器的基本组件包括任务管理器,事件管理器和时钟。通过这些组件调度器就能生成基于时间或基于帧的事件,然后调用相应的事件处理器。

任务管理器处理任务的注册和组织。每个任务都有一个包含了一个管理器能调用的回调 函数的接口。任务管理器维护了一个任务列表,其中包含了每一个任务的调度信息—例如开始时间,执行频率,持续时间,优先级和其他的属性。他也可能包含 一个用户数据的指针或性能统计信息。

事件管理器是调度器的核心部分。任务管理器里面的每一个任务都定义了一个或多个其 需要处理的事件。一个事件指的是个任务需要执行的时间。事件管理器的责任就是要产生必须的事件以执行相应的任务。

真实时间和虚拟时间:一个真实时间的调度在概念上是非常简单的—时间管理器不停地 进行循环,察看一个真实的时间时钟,每当目标到达的时候他就会触发一个事件。一个虚拟事件的调度器会把时间分成帧。任务在帧之间以批处理的方式进行,在虚 拟时间里运行,然后在每帧渲染出来的时候和真实的时间进行同步。

时钟组件是用来跟踪真实时间,当前的仿真时间和帧数的。时间管理器负责事件的排序 和产生。在某些情况下,多个任务可能会设置在同一个时间运行。有较高优先级的先执行。如果优先级相等或系统没有优先级就轮流执行。我们经常需要动态地更改 一个已注册的任务属性,这可能会牵涉到更改他的优先级,周期,持续时间或需求在他找到还没有结束的时候就将他删除。为了能更新任务的属性,我们必须使用一 个外部的方法来找到他,能使用一个唯一的注册ID来标志一个任务。

1.1.2 一个简单的调度器

调度器的设计主要集中在两个组件上面—–调度器引擎本身和ITask插件接口。要使调度器运行起来,必须要有一个调用他的程式。在一个非图像里面的 程式里面,这需求把他放在一个循环里面然后执行顺序里面然后执行就能。While (running) scheduler.ExecuteFrame();有两种方法把调度器集成在一个消息驱动的图像界面上。第一种方法是修改消息循环来处理消息和调用调度 器。这是个最容易想到的方法,不过有个缺点,就是当窗口大小来来改动的时候调度器会停止工作。第二种方法是创建一个视窗系统时钟,利用时钟消息来调用调度 器。由于时钟消息并不会被窗口的拖动打断,调度器就能在后台就接续运行了。

仿真:调度器能用来驱动仿真系统。为了实现动画和碰撞检测功能,大多数仿真引擎都将时间分成独立的小片。

上面的论述精彩而简洁,但可惜,如果不经过动手,是无法理解的。

本来想写一个能够图形演示的Demo,不过这会带来一些问题,要完成这样一个 Demo,就不仅仅是调度器所能完成的了,要包括很多东西,而这些东西对于熟悉整个游戏的朋友们来说好办,对于其他朋友来说,就会增加新的困扰,甚至因此 学习中断。至少我学习一些陌生的知识的时候,总是希望这个专题“单纯”一些。最后决定弄一个Silverlight的壳子,但更像控制台程序的东西。之所 以不直接做成控制台程序,是因为要保证所有用的API都是Silverlight能用的,而且能够为以后的Silverlight项目说使用。

首先大家 看看,这个Demo的结构。很简单,下面对每个类进行一下介绍。

Scheduler类:

这个类当然是核心了。

在它里面维护着一些重要的变量: 帧任务队列的头任务,时间任务 队列的头任务,还有一个渲染任务。

还有一些标志时间的变量,当然随着程序的扩展,你可以加入更多的东西。

而它的方法包括:

  • 注册任务(把一个任务注册到帧任务队列/时间任务队列,或者是注册成渲染任务)
  • 取得帧任务队列/时间任务队列的头任务
  • 插入任务到帧任务队列/时间任务队列
  • 根据ID删除任务
  • 根据ID暂停任务(注意,这里时间上是把任务的状态标志设置为暂停,不要因词害 意,实际上后面的编程都说明了一点,这些方法是协同工作的,单独一个方法是完成不了一个功能性的任务,如果你这么做了,往往这个方法就设计的太复杂了)
  • 执行任务队列里的任务和渲染任务

这里我要提请大家注意,调度器的方法是很时钟紧密联系在一起的,正因为这样,才可 以发送延时消息之类的。里面的代码不是很复杂,但也不那么直观,一些精巧的地方还是需要反复理解的。

Clock类:

虽然这个类很重要,但它并不复杂,里面就是些标志时间的变量:帧数,当前时间,每 帧持续时间,系统总持续时间…

但我希望你不要忽视这个类,整个游戏都是和时间有关的,会不断地用到

还有一些重要但却没有什么实际内容的类,幸好我的注释写的详细,大家看起来不会困 难

Clock

public class Clock { public int FrameElapsed; public bool IsRunning; /// <summary> /// 获取系统启动后的时间,这是一个绝对值 /// </summary> public int ThisTime; public int LastTime; /// <summary> /// 系统启动后的运行的时间,调用Reset后会归零,这是一个累计, /// 每经过一个Update,就累计一个帧长度,这个帧长度是 ThisTime - LastTime /// </summary> public int SystemTime; public int SystemOffset; public int PauseTime; public int FrameCount; public int FrameStart; public int FrameEnd; /// <summary> /// 仿真时间 /// </summary> public int SimulationTime; public int SimulationTimeOffset; private static Clock instance; /// <summary> /// 这样写是为了避免一种特殊情况,在还没有建立实例的时候,有多个线程调用这个属性,那么它就会被创建多个实例,所以采取了一个Lock /// 但也有更简洁的写法 public static readonly Clock Instance = new Clock();我为了提醒自己多线程的特殊情况而选择了个麻烦的做法 /// </summary> public static Clock Instance { get { if (instance == null) { lock (typeof(Clock)) { if (instance == null) instance = new Clock(); } } return instance; } } private Clock() { //单例模式 Reset(); } public void Reset() { IsRunning = false; ThisTime = Environment.TickCount; LastTime = ThisTime; SystemTime = 0; PauseTime = 0; SystemOffset = ThisTime; FrameCount = 0; FrameEnd = 0; FrameStart = 0; SimulationTime = 0; SimulationTimeOffset = 0; } /// <summary> /// 每一个时钟循环所要做的事情 /// </summary> public void Update() { int elapsed = 0; LastTime = ThisTime; ThisTime = Environment.TickCount; elapsed = ThisTime - LastTime; if (elapsed < 0) elapsed = -elapsed; SystemTime += elapsed; } /// <summary> /// 启动时钟,更新此时帧数,设置虚拟时间 /// </summary> public void BeginFrame() { FrameCount++; Update(); if (IsRunning) { FrameElapsed = FrameEnd - FrameStart; FrameStart = FrameEnd; FrameEnd = SystemTime - SimulationTimeOffset; SimulationTime = FrameStart; } } /// <summary> /// 启动程序,如果是暂停状态,要进行一个更新 /// </summary> public void Run() { if (!IsRunning) { Update(); SimulationTimeOffset += (SystemTime - PauseTime); } IsRunning = true; } /// <summary> /// 暂停程序,并且如果是启动状态,要进行一次更新,更新暂停时间为系统运行时间 /// </summary> public void Stop() { if (IsRunning) { Update(); PauseTime = SystemTime; } IsRunning = false; } /// <summary> /// 设置虚拟时间,如果提供的值大于等于上一个虚拟时间,这更新虚拟时间为新值 /// </summary> /// <param name="_newTime">提供的值</param> public void AdvanceTo(int _newTime) { if (IsRunning && (_newTime >= SimulationTime)) SimulationTime = _newTime; } /// <summary> /// 设置虚拟时间,让虚拟时间等于FrameEnd /// </summary> public void AdvanceToEnd() { if (IsRunning) SimulationTime = FrameEnd; } }
调度器

public class Scheduler { public static readonly Scheduler Instance = new Scheduler(); /// <summary> /// 保证渲染事件的ID总是1 /// </summary> private const int RENDER_TASK_ID = 1; private TaskInfo timeTaskHead; private TaskInfo frameTaskHead; private TaskInfo renderTask; //渲染任务 private int nextID; private int frameTaskStart; private int frameTaskEnd; private int frameTaskElapsed; private TaskInfo taskInfo; private Scheduler() { renderTask = new TaskInfo(); nextID = RENDER_TASK_ID + 1; } public void ExecuteFrame() { Clock.Instance.BeginFrame(); Scheduler.Instance.taskInfo = null; int start = Clock.Instance.SystemTime; TaskInfo current = GetHeadTask(TASKTYPE.TIME); while (current != null) { Clock.Instance.AdvanceTo(current.InfoTime.next); //current.PTask.Execute(current.ID, Clock.Instance.SimulationTime, current.Data); //和下面一句起同样的作用 current.ExecuteTask(current.ID, Clock.Instance.SimulationTime, current.Data); current.InfoTime.last = current.InfoTime.next; current.InfoTime.next += current.InfoTime.interval; if ((current.InfoTime.duration == 0) || (current.InfoTime.duration >= current.InfoTime.next)) { InsertTimeTask(current); } else { current = null; } current = GetHeadTask(TASKTYPE.TIME); } Clock.Instance.AdvanceToEnd(); current = GetHeadTask(TASKTYPE.FRAME); while (current != null) { current.ExecuteTask(current.ID, Clock.Instance.SystemTime, current.Data); current.InfoTime.last = current.InfoTime.next; current.InfoTime.next += current.InfoTime.interval; if ((current.InfoTime.duration == 0) || (current.InfoTime.duration >= current.InfoTime.next)) { InsertFrameTask(current); } current = GetHeadTask(TASKTYPE.FRAME); } if(renderTask!=null) renderTask.ExecuteTask(renderTask.ID, Clock.Instance.FrameCount, renderTask.Data); } /// <summary> /// 注册任务 /// </summary> /// <param name="_taskType">任务类型</param> /// <param name="_time">任务的时间信息</param> /// <param name="_task">任务执行方法</param> /// <param name="_data1">附加信息</param> public void RegistTask(TASKTYPE _taskType, int _start, int _interval, int _duration, int _budget, ExecuteTask _execute, Object _data) { if (_taskType == TASKTYPE.RENDER) { renderTask.ExecuteTask = _execute; renderTask.Data = _data; renderTask.Status = STATUS.ACTIVE; renderTask.ID = RENDER_TASK_ID; return; } else { TaskInfo newTask = new TaskInfo(); newTask.Status = STATUS.ACTIVE; newTask.Data = _data; newTask.ExecuteTask = _execute; newTask.ID = nextID++; newTask.InfoTime.start = _start; newTask.InfoTime.interval = _interval; newTask.InfoTime.duration = _duration; newTask.InfoTime.budget = _budget; newTask.InfoTime.next = _start; if (_duration == 0) newTask.InfoTime.duration = 0; else newTask.InfoTime.duration = _start + _duration - 1; if (_taskType == TASKTYPE.FRAME) InsertFrameTask(newTask); else if (_taskType == TASKTYPE.TIME) InsertTimeTask(newTask); } } public void Run() { Clock.Instance.Run(); } public void Stop() { Clock.Instance.Stop(); } /// <summary> /// 返回时间任务队列/帧任务队列里的第一个任务,同时把这个任务从队列里清除掉 /// </summary> /// <param name="_taskType">任务类型</param> /// <returns>对应类型的头任务</returns> public TaskInfo GetHeadTask(TASKTYPE _taskType) { TaskInfo result = null; if (_taskType == TASKTYPE.FRAME) { if ((frameTaskHead!=null) && (frameTaskHead.InfoTime.next <= Clock.Instance.FrameCount)) { //像这样和顺序有关的关联操作 要小心谨慎,多做测试,否则很容易出错 result = frameTaskHead; frameTaskHead = frameTaskHead.NextTask; result.NextTask = null; } } else if (_taskType == TASKTYPE.TIME) { if( (timeTaskHead != null) && (timeTaskHead.InfoTime.next <= Clock.Instance.FrameEnd) ) { result = timeTaskHead; timeTaskHead = timeTaskHead.NextTask; result.NextTask = null; } } return result; } /// <summary> /// 把一个任务从 帧任务链表/时间任务链表 中删除,并返回这个任务 /// </summary> /// <param name="_taskID">需要找到的任务ID</param>//思考:这似乎不是一个高效的做法,字典,树,或者多线程都是解决的方式 public void RemoveTask(int _taskID) { bool isFind = false; //如果已经找到结果,就不再判断 if ((_taskID == RENDER_TASK_ID) && (renderTask!=null)) { renderTask.ExecuteTask = null; renderTask.Status = STATUS.DELETE; renderTask = null; return; } TaskInfo current = frameTaskHead; if (current != null) { if (current.ID == _taskID) { frameTaskHead = frameTaskHead.NextTask; current.Status = STATUS.DELETE; current.NextTask = null; return; } while (current.NextTask != null) { if (current.NextTask.ID == _taskID) { current.NextTask.Status = STATUS.DELETE; current.NextTask = current.NextTask.NextTask; isFind = true; break; } current = current.NextTask; } if (isFind) return; } current = timeTaskHead; if (current != null) { if (current.ID == _taskID) { timeTaskHead = timeTaskHead.NextTask; return; } while ((current.NextTask != null) && !isFind) { if (current.NextTask.ID == _taskID) { current.NextTask.Status = STATUS.DELETE; current.NextTask = current.NextTask.NextTask; break; } current = current.NextTask; } } return; } /// <summary> /// 将指定ID的Task设置为暂停,并返回这个任务 /// </summary> private void PauseTask(int _taskID) { bool isFind = false; //如果已经找到结果,就不再判断 if (_taskID == RENDER_TASK_ID) { renderTask.Status = STATUS.PAUSE; return; } TaskInfo current = frameTaskHead; if (current.ID == _taskID) { frameTaskHead.Status = STATUS.PAUSE; return; } while ((current.NextTask != null) && !isFind) { if (current.NextTask.ID == _taskID) { current.Status = STATUS.PAUSE; isFind = true; } current = current.NextTask; } if (isFind) return; current = timeTaskHead; if ((current.ID == _taskID) && !isFind) { timeTaskHead.Status = STATUS.PAUSE; return; } while ((current.NextTask != null) && !isFind) { if (current.NextTask.ID == _taskID) { current.Status = STATUS.PAUSE; isFind = true; } current = current.NextTask; } return; } private void InsertFrameTask(TaskInfo _newTask) { if (frameTaskHead == null) { _newTask.NextTask = null; frameTaskHead = _newTask; } else if (frameTaskHead.InfoTime.next > _newTask.InfoTime.next) { _newTask.NextTask = frameTaskHead; frameTaskHead = _newTask; } else { TaskInfo current = frameTaskHead; while (current != null) { if (current.NextTask == null) { _newTask.NextTask = null; current.NextTask = _newTask; break; } else if (current.NextTask.InfoTime.next >= _newTask.InfoTime.next) { _newTask.NextTask = current.NextTask; current.NextTask = _newTask; break; } current = current.NextTask; } } } private void InsertTimeTask(TaskInfo _newTask) { if (timeTaskHead == null) { _newTask.NextTask = null; timeTaskHead = _newTask; } else if (timeTaskHead.InfoTime.next > _newTask.InfoTime.next) { _newTask.NextTask = timeTaskHead; timeTaskHead = _newTask; } else { TaskInfo current = timeTaskHead; while (current != null) { if (current.NextTask == null) { _newTask.NextTask = null; current.NextTask = _newTask; break; } else if (current.NextTask.InfoTime.next >= _newTask.InfoTime.next) { _newTask.NextTask = current.NextTask; current.NextTask = _newTask; break; } current = current.NextTask; } } } }
代码

public class TaskInfo { public ITask PTask { set; get; } //有具体执行方法的ITask,这里实际上和委托是同一个意思,我觉得委托更方便一些 public ExecuteTask ExecuteTask { set; get; } //指向执行任务的具体方法 public TaskInfo NextTask { set; get; } //指向下一个任务 public int Priority { set; get; } //任务优先级 public int ID { set; get; } //标识任务的唯一性 public object Data { set; get; } //任务数据 public STATUS Status { set; get; } //任务活动属性 public TimeInfo InfoTime; //任务的时间属性 } public struct TimeInfo { public int start { set; get; } //开始 public int interval{ set; get; } //间隔 public int duration{ set; get; } //持续 public int last{ set; get; } //最后 public int next{ set; get; } //下一个 public int budget{ set; get; } //预算 } /// <summary> /// 任务的状态 /// </summary> public enum STATUS:int { ACTIVE = 0, //活动任务 PAUSE, //暂停任务 DELETE //删除任务 } /// <summary> /// 任务类型 /// </summary> public enum TASKTYPE : int { TIME = 0, //时间任务 FRAME, //帧任务 RENDER //渲染任务 } /// <summary> /// 每一个具体的任务都需要继承这个接口并实现这个执行方法 /// </summary> public interface ITask { void Execute(int _id, int _time, object _data); } //任务的具体执行方法 public delegate void ExecuteTask(int _id,int _time,object _data);

接下来的时间就简单了,需要做的就是注册一些简单的任务,我只是注册了一些 Debug.Write,然后在MainPage里面构建一个循环,把调度器的ExecuteFrame()放在这个循环里面就可以了,系统会愉快地按照 你的意思工作了。

“纸上得来终觉浅,须知此事要躬行。”最终需要亲爱的朋友,一个字符一个字符地 敲打出来,否则真的很难转化为自己的东西。我不知道会不会有朋友提出希望得到Zip源代码Demo,我想这不是我提倡的学习方式,实际上代码已经在这里 了,在后面的文章中也不会存在Zip这样的东西,记得我最初学习编程的时候,一个有经验的编程人员就告诫我,全部的代码都要敲,哪怕是一个for循环。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏