// 以下代码摘自quake2-v3.21版本.
//-------------------------------------------------------------------------------
// 服务端由服务端引擎和Game模块组成(quake.exe game.dll)
// 服务端的帧循环,这儿将关键调用摘出.
//-------------------------------------------------------------------------------
SV_Frame
// 处理客户端玩家命令
SV_ReadPackets
SV_ExecuteClientMessage
SV_ClientThink (cl, &newcmd);
// game模块针负责计算玩家在游戏世界的表现
ge->ClientThink (cl->edict, cmd);
// game
SV_RunGameFrame ();
// 在game模块实现了游戏世界各对象与游戏世界的交互,包括物理交互
G_RunEntity (ent);
// 保存游戏对象帧相对信息
ClientEndServerFrames
ClientEndServerFrame
G_SetClientFrame
// 交服务器引擎负责将游戏对象信息以帧间差分形式发给客户端玩家
SV_SendClientMessages
SV_SendClientDatagram (c);
SV_BuildClientFrame (client);
SV_WriteFrameToClient (client, &msg);
SV_WritePlayerstateToClient (oldframe, frame, msg);
SV_EmitPacketEntities (oldframe, frame, msg);
// 游戏逻辑模块的帧结构,下面是关键调用
SV_RunGameFrame ()
// 计算游戏逻辑帧,一般100ms一次, 首选完成游戏对象和游戏世界的交互,
// 然后运行游戏对象的AI, 最后更新游戏对象的状态
G_RunEntity (ent)
SV_RunThink (ent)
monster_think (edict_t *self)
M_MoveFrame (self);
//---------------------------------------------------------------------------------------------------
// 客户端由客户端引擎和ref_gl.dll模块组成(quake.exe ref_gl.dll)
// 下面是Q2的客户端引擎,主要完成对服务器发过来的数据帧解析,游戏
// 世界的渲染,游戏客户端的输入处理,向服务器的命令发送,游戏对象的
// 动画绘制。
//----------------------------------------------------------------------------------------------------
// 客户端的帧结构如下
CL_Frame
// 解析服务器发过来的数据
CL_ReadPackets ();
CL_ParseFrame
// 根据上帧间差值更新游戏对象状态
CL_ParsePlayerstate (old, &cl.frame);
CL_ParsePacketEntities (old, &cl.frame);
// 响应客户端操作,向服务端发送玩家自身动作命令
CL_SendCommand ();
// 在这儿实现客户端平滑运动,采用预测方法不等服务器返回状态即更新自已状态,
// 前提是在超时时间到达之前,否则帧会卡住..
CL_PredictMovement ();
// 玩家运动核心函数,调用引擎检测游戏对象和游戏世界间的碰撞和反应,
// 是Q2的碰撞检测引擎的高级调用接口。
Pmove
CL_PMTrace
// 最后分解为BOX和游戏世界的连续碰撞检测和碰撞反应.
CM_BoxTrace
// 这儿是检测游戏对象间的碰撞。这儿的游戏对象主要指玩家,
// 游戏对象的组织采用相当于KD树的组织方法,将游戏世界进
// 行按平行坐标轴方向均匀划分,虽然在数据存储上利用了静态场景
// 的BSP场景数据存储,但是确是一个动态的游戏对象组织方法,也
// 是现今游戏普遍采用的方法(动态场景和静态场景分开管理)。在 每
// 次可移动对象更新时先将其从场景树中取出(unlinkentity),更新完成
// 后再linkentity(重新加入动态对象场景树)。
CL_ClipMoveToEntities
//--------------------------------------------------------------------------------------------------
// 通过这个调用进行各种客户端的显示图像的绘制,主要是关于场景绘制,游戏对象的
// 绘制。M2模型动画是关键帧动画。早期卡马克就想到了在Q2中采用3D的显示方法,
// 即在显 示图像时采用绘制 视角相差15度的两幅视图,通 过 3D眼 镜 实现3D效果。
//---------------------------------------------------------------------------------------------------
SCR_UpdateScreen ();
V_RenderView
CL_AddEntities
cl.lerpfrac = 1.0 - (cl.frame.servertime - cl.time) * 0.01;
CL_CalcViewValues ();
CL_AddPacketEntities (&cl.frame);
re.RenderFrame (&cl.refdef);
R_DrawWorld ();
currentmodel = r_worldmodel;
currententity = &ent;
ent.frame = (int)(r_newrefdef.time*2);
//------------------------------------------------------
// 进行游戏世界的绘制
// 这儿展示了BSP场景在场景渲染加速方面的使用方法,如果仔细
// 分析会发现Q2的BSP场景在绘制时实现了back face culling(背
// 面剔除), 而且几乎没有任何代价。而且可以做到从前往后渲染
// 场景。当然,Q2场景渲染的速度快的方法功劳还是首推他的场
// 景 PVS(场景可视的图形的集合)信息,此信息的预生成和优
// 化存储和快速解码也是值的深入学习的。当然现在BSP的主要
// 不是用在渲染方面,更多用在了碰撞检测方面,光线跟踪方面,
// 这点也在Q2中展示出了具体的实现方法。当然下面只是渲染的
// 部分内容。
R_RecursiveWorldNode (r_worldmodel->nodes);
// M2模型绘制, 精灵等的图元,老的M2关键帧动画作为学习动画
// 原理,动作插值入门资料还可以。
R_DrawEntitiesOnList ();
currententity = &r_newrefdef.entities[i];
currentmodel = currententity->model;
R_DrawSpriteModel (currententity);
R_DrawBrushModel (currententity);
R_DrawInlineBModel ();
R_DrawAliasModel (currententity);
GL_DrawAliasFrameLerp (paliashdr, currententity->backlerp);
//--------------------------------------------------------------------------------------------
// 模型动画控制: 由于Q2采用关键帧动画,在服务器上按照服务器设定的帧速进行动画
// 的控制,默认为每帧100ms, 动画信息由各个monster文件单独标明。其中monster AI
// 也由该该文件标明
//---------------------------------------------------------------------------------------------
// 下面指出在Q2中当同时出现各种动画要求时的动画优先级
// 动作转换优先级: ANIM_DEATH -> ANIM_ATTACK -> ANIM_PAIN -> ANIM_JUMP ->ANIM_WAVE -> ANIM_BASIC
// 下面是各个动作单独摘要
ANIM_BASIC(默认动作, 在没有其它动画播放要求时的动画)
动画的更新:
// 先检测当前是否为其它几种动作,若没有做其它的动作,则目前只会是这stand 和run几// 种动作中的一种,根据xyspeed(水平移动速度)区分 stand 和 run。
G_SetClientFrame (edict_t *ent)
client->anim_priority = ANIM_BASIC
// ANIM_WAVE(应该不是游泳的动作,而是从空中落到地上的缓冲动作,振动效果)
// 紧跟着ANIM_JUMP动作后的下一个动作
G_SetClientFrame (edict_t *ent)
ent->client->anim_priority = ANIM_WAVE;
// ANIM_JUMP(跳跃动作)
// 条件为(!ent->groundentity), 即是不是在空中,如果在空中漂浮则施展该动作
G_SetClientFrame (edict_t *ent)
client->anim_priority = ANIM_JUMP;
ANIM_PAIN(受伤动作, 换武器时的动作和受伤动作采用同一模型动作)
// 以下三种情况均可以导致该动画的播放
P_DamageFeedback (edict_t *player)
ChangeWeapon (edict_t *ent)
G_SetClientFrame (edict_t *ent) // 由此处完成ANIM_PAIN的下一帧。
// 以下几种情况可以引起该动作的播放
ANIM_ATTACK(攻击动作)
Weapon_Generic(...)
weapon_grenade_fire (edict_t *ent, qboolean held)
Weapon_HyperBlaster_Fire (edict_t *ent)
Machinegun_Fire
Chaingun_Fire (edict_t *ent)
G_SetClientFrame (edict_t *ent) // 由此处完成ANIM_PAIN的下一帧。
// 以下几种情况可以引起该动作的播放
ANIM_DEATH(死亡动作)
ThrowClientHead (edict_t *self, int damage))
player_die(...)
G_SetClientFrame (edict_t *ent) // 由此处完成ANIM_PAIN的下一帧。
// 以下几种情况可以引起该动作的播放
ANIM_REVERSE(主要用于枪械的反复动作)
weapon_grenade_fire (edict_t *ent, qboolean held)
Weapon_Generic(...)
G_SetClientFrame (edict_t *ent) // 由此处完成ANIM_PAIN的下一帧。
//------------------------------------------------------
// 物理系统分析
//-----------------------------------------------------
Q2的物理系统放在了game模块,即游戏逻辑模块,实现了几种最基本的物模运动模型
MOVETYPE_PUSH:
MOVETYPE_STOP:
MOVETYPE_NONE:
MOVETYPE_NOCLIP:
MOVETYPE_STEP:
MOVETYPE_TOSS:
MOVETYPE_BOUNCE:
MOVETYPE_FLY:
MOVETYPE_FLYMISSILE:
其中最重要的是MOVETYPE_STEP, 是玩家的物理运动模型,其实现为
SV_Physics_Step
SV_FlyMove (ent, FRAMETIME, mask);
其中SV_FlyMove的实现是核心函数。是该函数实现了角色和世界间平滑的连续碰撞检测。其实现方法仍然对现在的游戏开发有很大的参考价值。是个很好的学习材料。
主要做下面工作:
检查是否在地面上
增加重力加速度
增加摩擦力(角速度,直线移动速度)
设定没有站在地面上,根据现在的速度向前运动,遇到障碍则延障碍滑动
下面是monster(怪物)的物理运动模型M_walkmove
详细实现在SV_movestep中:
物体总共三种类型
1)陆地行走的物体
2)空中飞行的物体
3)水中游动的物体
可以成功移动的前提条件是满足下面之一:
1)物体在地面上
2)物体是可飞行的或可在水面游动的
对于陆地的物体:
检查如果满足下面的条件之一就禁止移动:
1)目的地在物体中
2)目的地是水中
3)前面是陡峭的地方且物体不允许离开地面
碰撞检测方法利用引擎提供的trace方法.值的注意的是怪物和人类玩家的运动方式是不一样的,也就是说怪物不会撞到墙上,再按墙面滑动,因为怪物发现前面距离墙面已经很近了,
以至于再走一步就撞墙了这样它就会停止向前运动.就防止撞墙了.
总结:
以上分析了Q2的几个模块内的帧循环调用顺序,和功能的摘要。Q2的清晰的组织结构,优秀的C语言编码风格。展现了计算机图形学在3D游戏中的灵活运用,到目前为止仍是游戏开发人员的极好的学习资料。
作者:Perit 整理于2010.6