找回密码
 注册帐号

扫一扫,访问微社区

实例教程 游戏调动器系统的开发

4
回复
813
查看
[ 复制链接 ]
排名
64936
昨日变化

1

主题

4

帖子

12

积分

Rank: 1

UID
29032
好友
0
蛮牛币
21
威望
0
注册时间
2014-6-10
在线时间
5 小时
最后登录
2019-3-2
2019-2-3 21:23:52 显示全部楼层 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
游戏架构:
提到架构,引擎,总透着一股高深莫测的味道。这个问题要分两面看,首先,架 构,引擎确实是好东西,也是很多高人前辈心血的总结,会为我们的开发带来莫大的好处,这是毋庸置疑的。但同时,这些东西是可以学习的,或许学习的曲线会有 些陡峭,但是绝不是不可理解的。
有个哥们这么说过“这类东西常常被冠上一个看似很深奥的名字,但是仔细看过以 后,你就会发现这不是我一直在用的东西么?”用这句话来给架构引擎做个注脚倒挺合适。
我们开发系统,总会有一个结构的,即便你没有意识到这点。
游戏开发是一个难度很高的事情,逻辑复杂,对系统的性能要求很高,不断有新鲜 的,好玩的点子涌现出来,而这些点子对开发者来说都是 一场场的噩梦。所以如果有一个灵活的架构,将是非常好的事情,也很幸运。
几年前初接触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()放在这个循环里面就可以了,系统会愉快地按照 你的意思工作了。

回复

使用道具 举报

7日久生情
2181/5000
排名
2353
昨日变化

8

主题

798

帖子

2181

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
40014
好友
16
蛮牛币
3182
威望
0
注册时间
2014-8-15
在线时间
601 小时
最后登录
2019-8-25
QQ
2019-2-4 00:32:08 显示全部楼层
游戏调动器系统的开发
回复 支持 反对

使用道具 举报

排名
20607
昨日变化

0

主题

17

帖子

60

积分

Rank: 2Rank: 2

UID
271941
好友
0
蛮牛币
42
威望
0
注册时间
2018-3-12
在线时间
17 小时
最后登录
2019-6-8
2019-2-4 23:00:40 显示全部楼层
这个对我很有启发,我最近就在研究框架。感谢
回复 支持 反对

使用道具 举报

0

主题

29

帖子

70

积分

Rank: 2Rank: 2

UID
275434
好友
0
蛮牛币
4
威望
0
注册时间
2018-4-3
在线时间
41 小时
最后登录
2019-4-15
2019-2-6 08:30:23 显示全部楼层
thanks for share
回复

使用道具 举报

5熟悉之中
632/1000
排名
6636
昨日变化

0

主题

218

帖子

632

积分

Rank: 5Rank: 5

UID
17089
好友
1
蛮牛币
16
威望
0
注册时间
2014-3-10
在线时间
212 小时
最后登录
2019-8-1
2019-4-29 12:02:40 显示全部楼层
游戏调动器系统的开发
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则