作者:吴振华(kylinx)
E-mail:game-diy@163.com
Homepage:http://kylinx.yeah.net
OICQ:30784290
Date:2003/6/4
!!!转载请保证文档的完整性!!!
从开始写游戏到现在,总感觉自己在渲染场景这方面的思想做得不好。虽然写了3个DEMO,但是在渲染场景这方面总是显得很笨重。前几天无意间翻开一本《微机原理》,有几个字跳入了我的眼中——流水线!大概是灵光一闪吧,脑子里就有了使用流水线的方法渲染场景这个念头。
好了,进入正题,大家先想一想那些即时战略游戏吧,比如说星际(我最爱的即时战略游戏)
在游戏画面刷新的时候,尤其是在战斗的时候,凌乱的爆炸,血的效果,魔法的效果……总之有很多很多的动画序列需要处理(比如爆炸这个动画),而且这些动画又是不定时产生的。
在更新画面的时候,该怎样处理呢?如果用一种常见的方法就是这样:(伪代码如下)
UpdateScreen()
{
DrawMapGround(); //画地图最底层
DrawMapLayer1(); //画没有遮挡住精灵的层
DrawAllSprite();//画所有精灵,精灵动画可以在里面实现
DrawMapLayer2();//画遮挡住精灵的层
If(有爆炸的动画)
ShowBomb();
….
}
可以看出这样做是根本不能实现的
原因如下:
1:地图和精灵之间是有遮挡关系的,且精灵和精灵之间也是有遮挡关系的。这样做的话虽然可以实现正确的遮挡关系(当然需要做更多的判断,上面只是很简单的伪代码),但是大量的判断绝对让人受不了!再说,如果爆炸是在精灵层的下面发生的呢?(比如,一只航母下面的一量坦克爆炸了),这时候怎么处理?如果直接爆的话肯定是看见航母在爆炸了!
那么在爆炸的上面再画一次航母?嗬嗬,对于高效的代码来说绝对不允许这样!
2:爆炸的动画是不定个数的,并且每一个爆炸的每一帧都也许不会同时进行(比如,一个炸弹的动画有10帧。第一个炸弹爆炸到了第5帧的时候,第二个炸弹开始爆炸)再加上1所说的层的问题。所以上面的方法行不通
3:(以上两点就足够说明了,这点就不用说了吧,嘿嘿……)
下面谈谈该怎么解决这个问题(以下是解决这个问题的个人想法,并不敢确定星际中是否是这样做的!!!若觉得不值得一看请止目)
考虑这样一个任务队列:
Queue中假设有任务T1,T2,T3,T4;
其中T1的任务是画地图的最底层
T2是显示神族的一个龙骑士
T3是显示龙骑士发出的炮弹动画
T4是一个闪电兵对龙骑士释放闪电的动画
我们希望的结果是这样:
T1永远执行下去,直到游戏退出(废话,要不然就没有地图了)
T2在龙骑士被K掉以后就从队列中删除
T3在炮弹到达目标后,爆炸,然后从队列中删除
T4闪电动画播放完成后,从队列中删除
好了,大家看到这样处理就会很有很好的效果
下面问题又来了,怎样在这个队列中把任务插入到合适的位置呢?又怎样任务完成后,自动从队列中删除呢?
我先给大家分析一下
要把任务插入到合适的位置,这就要判断该任务的“优先级别”,在游戏中可以用所在的“层”来表示,最优先执行的,也就是层最低的(因为正确的贴图总是从低层贴到高层嘛),通过优先级别很容易找到任务该插入的位置
要把任务从队列中删除。可以这样做:在每一个任务中都设置一个状态bActive,如果bActive为真,表示这个任务还没有完成。否则,就删除这个任务节点!
好了,思想就这么简单!下面看具体实现
我是这样做的:(注意红色部分)
写一个任务的类名曰KGDTask(在我现在正在写的游戏库中叫KGDLib (KGD---KylinxGameDevelop),由于是讲解,所以一些其他不重要的成员我没写上来)
class KGDTaskQueue; //这个就是任务队列
class CTask
{
friend class CtaskQueue; //为了方便,声明CtaskQueue为Ctask的友元,访问私有成员
DWORD m_dwID; //任务的ID,通过这个ID从队列中寻找该任务(如果需要自己寻找的话)
DWORD m_nPriority; //任务的优先级
BOOL m_bTaskActive; //任务是否完成,如果完成,m_bTaskActive=false;,否则等于true
Void*pExecuteParam; //执行任务函数的参数
Void*pCheckTaskEndParam; //检查任务是否完成的函数的参数
Void FreeResource() //释放所有资源
{
Release(); //释放从本类派生下的类的资源
Delete this; //为了在CtaskQueue中,这里代劳delete删除任务的指针
}
Public:
virtual DWORD ExecuteTask(void*param)=0;
//任务执行的函数!每个任务从此类派生的都必须重载写此函数
virtual BOOL CheckTaskActive(void*param)=0;
//检查任务是否完成的函数,格式必须为:如果完成则返回false否则返回true
virtual DWORD Release()=0; //释放派生类的资源
KGDTask() //构造
{
pExecuteParam=NULL;
pCheckTaskEndParam=NULL;
m_bTaskActive=false;
}
void SetExecuteParam(void*param)
{
pExecuteParam =param;
}
void SetCheckTaskEndParam(void*param)
{
pCheckTaskEndParam=param;
}
void CreateTask(DWORD nID,DWORD nPriority, //创建任务void*pExeParam=NULL,void*pChkTskEndParam=NULL)
{
m_nTaskID=nID;
m_nPriority=nPriority;
m_bTaskActive=true;
pExecuteParam=pExeParam;
pCheckTaskEndParam=pChkTskEndParam;
}
void SetPriority(DWORD nPriority) //设置任务优先级
{
m_nPriority=nPriority;
}
void ForceEndTask(DWORD code=0) //强行退出任务
{
m_bTaskActive=false;
}
DWORD GetTaskID() //返回任务ID
{
return m_nTaskID;
}
};
class KGDTaskQueue
{
struct KGDTASKNODE //任务节点
{
KGDTask*pTask; //任务
KGDTASKNODE*next; //下一个任务
KGDTASKNODE() //节点的构造函数
{
pTask=NULL;
next=NULL;
}
KGDTASKNODE(KGDTask*ptsk) //节点的构造函数
{
pTask=ptsk;
next=NULL;
}
};
KGDTASKNODE*m_pTaskHead; //任务队列头
DWORD m_nQueueLength; //当前队列中任务个数
int RemoveTask(KGDTASKNODE * pNode); //从队列中删除节点
public:
enum
{
ERR_OK=0,
ERR_NOMEMORY,
ERR_NOEXIST,
ERR_IDEXIST
};
KGDTaskQueue();
virtual ~KGDTaskQueue();
int AddTask(KGDTask * cTask); //添加任务
int ClearTaskQueue(); //清空任务队列
int ExecuteTask(); //“流水线”式的执行任务
BOOL FindTaskWithID(DWORD dwID,KGDTask**ppTask);
//通过ID查找任务,如果成功,ppTask指向待查找的任务
DWORD GetQueueLength() //返回任务个数
{
return m_nQueueLength;
}
};
KGDTaskQueue::KGDTaskQueue() //构造
{
m_pTaskHead=new KGDTASKNODE();
m_nQueueLength=0;
}
KGDTaskQueue::~KGDTaskQueue() //析构
{
ClearTaskQueue();
delete m_pTaskHead;
}
BOOL KGDTaskQueue::FindTaskWithID(DWORD dwID,KGDTask**ppTask) //查找任务
{
if(!ppTask)
return false;
KGDTASKNODE*pNode=m_pTaskHead->next;
while(pNode) //顺着队列查找
{
if(pNode->pTask->m_nTaskID==dwID)
{
*ppTask=pNode->pTask;
return true;
}
pNode=pNode->next;
}
return false;
}
int KGDTaskQueue::ClearTaskQueue() //清空队列
{
KGDTASKNODE*pTemp=NULL;
KGDTASKNODE*pNode=m_pTaskHead->next;
while(pNode)
{
pTemp=pNode;
pNode=pNode->next;
pTemp->pTask->FreeResource();
delete pTemp;
}
m_pTaskHead->next=NULL;
m_nQueueLength=0;
return ERR_OK;
}
int KGDTaskQueue::ExecuteTask() //执行队列中的任务
{
KGDTASKNODE*pHeadNode=m_pTaskHead->next;
KGDTASKNODE*pTemp=NULL;
while(pHeadNode)
{
pTemp=pHeadNode->next;
if(pHeadNode->pTask->m_bTaskActive) //如果此任务仍然激活
{
pHeadNode->pTask->ExecuteTask(pHeadNode->pTask->pExecuteParam);//执行此任务
pHeadNode->pTask->m_bTaskActive=pHeadNode->pTask->CheckTaskActive(pHeadNode->pTask->pCheckTaskEndParam);
//对此任务作结束与否的判断
}
else //如果此任务不再激活(已经完成或者被强行退出)
{
pHeadNode->pTask->FreeResource(); //释放此任务的资源
RemoveTask(pHeadNode); //从队列中删除此任务节点
}
pHeadNode=pTemp;
}
return ERR_OK;
}
int KGDTaskQueue::RemoveTask(KGDTASKNODE*pNode) //删除节点
{
KGDTASKNODE*pHeadNode=m_pTaskHead->next;
KGDTASKNODE*pTempPrev=pHeadNode;
pHeadNode=pHeadNode->next;
if(pTempPrev==pNode) //如果待删除的节点是第1个节点(在我写的这个结构中,对列头节点不存放东西)
{
m_pTaskHead->next=pTempPrev->next;
delete pTempPrev;
m_nQueueLength--;
return ERR_OK;
}
while(pHeadNode)
{
if(pHeadNode==pNode)
{
pTempPrev->next=pHeadNode->next;
delete pHeadNode;
m_nQueueLength--;
return ERR_OK;
}
pTempPrev=pHeadNode;
pHeadNode=pHeadNode->next;
}
return ERR_NOEXIST;
}
int KGDTaskQueue::AddTask(KGDTask * cTask) //添加任务
{
KGDTASKNODE*pNode=new KGDTASKNODE(cTask); //创建任务节点
if(NULL==pNode)
return ERR_NOMEMORY;
if(m_pTaskHead->next==NULL) //如果失空队列
{
m_nQueueLength=1;
m_pTaskHead->next=pNode;
return ERR_OK;
}
KGDTASKNODE*pTemp=m_pTaskHead->next;
KGDTASKNODE*pTempPrev=pTemp;
pTemp=pTemp->next;
if(pTempPrev->pTask->m_nPriority<=pNode->pTask->m_nPriority) //与第一个节点作优先级的判断
{
m_pTaskHead->next=pNode; //如果要插入的节点的优先级比第一个节点的优先级还要高,就这样插入
m_nQueueLength++;
pNode->next=pTempPrev;
return ERR_OK;
}
while(pTemp) //在队列中插入
{
if(pTemp->pTask->m_nPriority<=pNode->pTask->m_nPriority)
{
pNode->next=pTemp;
pTempPrev->next=pNode;
m_nQueueLength++;
return ERR_OK;
}
pTempPrev=pTemp;
pTemp=pTemp->next;
} //编历队列完成
pTempPrev->next=pNode; //插入尾部
m_nQueueLength++;
return ERR_OK;
}
好了!每个任务都从KGDTask中派生而来,比如
class MyTask:public KGDTask
{
…//你自己的定义
public:
BOOL InitMyTask();//自己对这个类做初始化
virtual DWORD ExecuteTask(void*param);
virtual BOOL CheckTaskEnd(void*param);
virtual DWORD Release();
};
添加到任务中就这样处理(在任何函数中)
//记住,每一个任务的ID应该独立哦,比如用#define 定义在某个头文件中
//KGDTaskQueue*queue=new KGDTaskQueue;//假设queue是全局变量
MyTask*pMyTask=new MyTask();
MyTask->CreateTask(…..);
MyTask->InitMyTask(….);
Queue->AddTask(MyTask);
…..
在游戏更新画面的函数中,使用
Queue->ExecuteTask(); //象流水线一样执行任务
不需要自己释放MyTask,在执行的过程中会自动释放的(任务完成后)
强行结束任务就是
(如果MyTask是局部的,要在另一个函数中结束它,就应该先找到它的ID)
MyTask*pMyTask;
queue->FindTaskWithID(MyTaskID,&pMyTask);
pMyTask->ForceEndTask();
好了!万事OK!
终于写完了!声明一下,在任务队列中,也许我用的入队算法效率不高,这个就要靠大家自己去研究数据结构吧!嗬嗬,毕竟我只是在说一种更新游戏画面的思想,说得不对的地方,还恳请大家不吝赐教!game-diy@163.com