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循环。

 

Windows 窗口和消息

消息能够被分为 “进队列” 和 “不进队列”。进队的消息是由Window放入程序消息队列中的。在程序的消息循环中,重新返回并分配给窗口过程。不进队的消息在Windows 调用窗口时直接发送给窗口过程。

计时器消息不是异步的,这个消息是低优先级的,只有在消息队列中没有其他消息才接收他。

 

posted @ 2010-06-11 13:50  向恺然  阅读(4865)  评论(15编辑  收藏  举报

我必须说的是:我崇尚开源,但鄙视剽窃。本博客所有引用的图片,文章,和代码,均只作为研究学习使用,不作为商业应用。如果我无意中冒犯了您,请发消息留言,我将立即删除。