如果你经常写AMXX,你应该会知道有个 pev->groupinfo 变量,但我猜大部分人都不会用这个变量,这个变量涉及很多实体处理功能,下面列举几个最常用的。
① 玩家与非玩家实体之间的碰撞检测
② 非玩家实体之间的碰撞检测
③ Trace系列检测函数的目标过滤
下面我一个个介绍这些功能具体怎么实现和利用。
一、玩家与非玩家实体之间的碰撞检测(包括玩家与玩家)
玩家的移动处理是在pm_shared.c里PM_Move函数实现的(PM意为PlayerMove,HLSDK包含此文件),如果你走路撞上一些实体或者地图的墙体,你会无法前进,被挡住,这是因为
PM检查到你前方有障碍物,不让你继续前进。那么PM是怎么实现这个检查的呢?原来引擎为PM提供了一个专用的Trace函数,叫做PM_PlayerTrace,这个函数可以
模拟一个玩家大小的BOX朝指定位置移动,并返回途中碰到的实体,如果途中碰到了某个实体,这个实体正是“障碍物”,那么你将被这个障碍物阻挡,无法继续前进。
像下图这样:
蓝色的Box是玩家的Hull(Hull在HL引擎中意为碰撞盒子),huang色Box是某个实体或者地图的Hull。具体实现原理让我们暂时忽略吧,如果有需要可以留言。
如果我们有一些特殊的需求,例如:让玩家不要碰到某些实体(就是让玩家可以穿过某些实体),这怎么办呢?没事,引擎为我们留下了可以定制的接口。
首先我们要进一步了解PM(PlayerMove)这个东西,引擎每次只处理一个玩家,也就是说,引擎会依次给每个玩家调用PM_Move函数来实现每个玩家的移动处理,
调用PM_Move函数之前,引擎会先准备一个叫做physents的数组(这个数组在pm_defs.h文件里的playermove_t结构体里定义),而调用PM_PlayerTrace来检查
障碍物时,PM_PlayerTrace只会检查physents里的实体,聪明的你已经猜到了,要想让玩家穿过某些实体,这些实体就一定不能出现在physents这个数组里。
这个数组由引擎管理的,我们必须按照引擎的规定来处理这个数组(歪门邪道快离开)。我们有必要了解一下PM_Move是在什么时机执行的,如下所示:
CmdStart -> PlayerPreThink -> [填充physents数组] -> PM_Move -> PlayerPostThink -> CmdEnd
我们现在已经知道引擎是什么时候准备physents数组了,有个非常好的消息是,引擎填充这个数组的时候,提供了一种方法让我们可以控制哪些实体可以被填充
到这个数组里(默认是全部pev->solid不为SOLID_NOT和SOLID_TRIGGER的实体)。这就是 groupinfo 派上用场的一个地方。
引擎会逐个检查所有pev->solid符合上述条件的实体的groupinfo,与玩家的(再次注意!引擎每次只处理一个玩家!所以请把这里的“玩家”当成“自己”)groupinfo
进行对比,如果符合条件,这个实体就会被加入到physents数组里。引擎使用位与(&)运算符来计算实体的groupinfo和玩家的groupinfo,根据结果和判断方法
来决定是否把实体加入数组。引擎定义了两种判断方法,使用 g_engfuncs.pfnSetGroupMask 函数的 op 参数来设置,分别是 GROUP_OP_AND 和 GROUP_OP_NAND,
默认为 GROUP_OP_AND 。
#define GROUP_OP_AND 0 #define GROUP_OP_NAND 1
void (*pfnSetGroupMask) ( int mask, int op );
如果是 GROUP_OP_AND ,两个实体的groupinfo的计算结果为零,则不加入数组,代码像这样:
if ( mode == GROUP_OP_AND && (check->pev->groupinfo & player->pev->groupinfo) == 0 ) continue;
如果是 GROUP_OP_NAND,两个实体的groupinfo的计算结果不为零,则不加入数组,代码像这样:
if ( mode == GROUP_OP_NAND && (check->pev->groupinfo & player->pev->groupinfo) != 0 ) continue;
如果你不懂位与(&)计算,那我就举个简单的例子:
int groupinfoA = (1<<1) int groupinfoB = (1<<1) // 结果 groupinfoA & groupinfoB > 0 ------------------------------------ int groupinfoA = (1<<1) int groupinfoB = (1<<2) // 结果 groupinfoA & groupinfoB = 0
上面我已经知道了引擎会在PlayerPreThink之后、PM_Move之前检查那些要加入数组的实体,所以我们可以在PlayerPreThink的时候把该玩家要穿过的所有实体的groupinfo设置一个值,该玩家则设置一个不同的值,例如:
void PlayerPreThink( player ) { player->pev->groupinfo = ( 1<<1 ); for ( int i = 0; i < num; i++ ) { // 这个数组是要穿透的实体列表 entities[i]->pev->groupinfo = ( 1<<2 ); } }
我们使用 GROUP_OP_AND 判断方法,所以要设置一下:
g_engfuncs.pfnSetGroupMask( 0, GROUP_OP_AND );
注:AMXX可以使用fakemeta模块的engfunc函数
引擎执行完PlayerPreThink函数,我们就设置好了所有需要处理的实体的groupinfo,接着引擎就会检查所有实体,因为我们知道 (1<<1) & (1<<2) 结果是等于0的,所以根据GROUP_OP_AND判断方法,
与该玩家的groupinfo计算结果等于0的实体将不会加入physents数组,也就不会被PM_PlayerTrace检测,自然也就不会成为“障碍”。(可以穿过)
当引擎执行完PM_Move之后,“玩家”已经移动了,你已经实现了目的,所以你必须要清理你设置过的实体的groupinfo,以免造成意外,可以在PlayerPostThink里进行清理。
注:你可以任意选择一种判断方法,如果你选择 GROUP_OP_NAND 你可以把 player 和 entities 的groupinfo都设为同一个值,计算结果将不为0,实体将不会加入数组。
void PlayerPostThink( player ) { player->pev->groupinfo = 0; for ( int i = 0; i < num; i++ ) { entities[i]->pev->groupinfo = 0; } }
请注意!如果你要穿透一个玩家,你必须让你穿透的那个玩家也穿透你,不然当你穿进去,与那个玩家的模型重叠,那么你可以自由移动,但是那个玩家会卡住无法移动(非玩家并且可移动的实体同理),甚至可能会被GameRules Kill掉。
以上是服务端的处理办法,此时已经可以正常进行穿透,但是还有些瑕疵,因为客户端也会运行一份PM_Move(同样也有一个physents数组),当客户端运行PM_Move时,你上面处理的那些实体还是会被
加入客户端的physents数组进行碰撞检测。造成的结果就是,你可以穿透这些实体,但是穿过去的时候会“卡”一下因为服务端没有检查那个实体,但是客户端却检查了它。这很不正常(除非你特意这样做),为了不让客户端把我们已经穿
透的这些实体加入physents数组,我们要在 AddFullPack 函数里把该实体的state->solid改成SOLID_NOT,这样客户端就不会把该实体加入physents数组了。
int AddToFullPack( state, e, ent, host, ... ) { // state 该state会被发送到host端 // e 当前要发送的实体索引(实体ID) // host state将会被发送到此host for ( int i = 0; i < num; i++ ) { // 这个entities还是该玩家需要穿透的实体数组,判断当前发送的实体是否在列表里 if ( e == host->entities[i] ) { state->solid = SOLID_NOT;
break; } } }
OK,一切处理完成,你可以完美穿越任何想要穿过的实体(除了World实体,因为World是无条件添加到physents里的,不然你就无法站在地上了,会往下掉)
二、非玩家实体之间的穿透
非玩家实体之间的碰撞,大多数情况是 SOLID_BBOX SOLID_SLIDEBOX SOLID_BPS 这些有Hull的实体之间的碰撞,例如一个箱子可以叠在另一个箱子上。
由于引擎实现的原因,使用groupinfo来控制显得比较困难,所以引擎也提供了专用的接口,即 ShouldCollide 接口 ,参考:http://tieba.baidu.com/p/5384669344 这里不多做介绍了。
三、Trace系列函数中groupinfo的使用
我们经常用TraceLine TraceHull等方法来检查是否能击中一个目标,我们知道如果Trace从一个实体内部出发(作为起点),我们必须要在ignoreEntty写上该实体,以忽略它本身,否者Trace将会被该实体本身挡住。
此用法通常来说没有什么大问题,但是有些时候,我们希望忽略许多实体(例如模拟射击时不让Trace检测到队友),但ignoreEntity参数只有一个,这可麻烦了。
此时groupinfo再次派上用场,与上面的PlayerMove类似,我们需要在Trace前设置想忽略的实体的groupinfo,但我们上面的PlayerMove是以一个玩家作为基准,来对比其它实体的,那么TraceLine这些函数
是以什么作为基准呢?答案就是Trace函数的ignoreEntity参数。这个参数所指定的实体将会被Trace函数用作对比其它实体的基准,类似上文中的PM一样,我们只需要在Trace前随便选一个需要忽略的实体,并且像PM
一样设置好groupinfo,即可忽略任何想忽略的实体,例如:
g_engfuncs.pfnSetGroupMask( 0, GROUP_OP_AND ); // me 表示自己 me->pev->groupinfo = ( 1<<1 ); for ( int i = 0; < num; i++ ) { // friends里是队友们 friends[i]->groupinfo = ( 1<<2 ); } // me传到ignoreEntity参数,所以会被Trace忽略掉 // 然后Trace还会拿me的groupinfo和其它碰撞到的实体的groupinfo进行对比 TraceLine( start, end, ..., me, &result ); // 不要忘了清理 me->pev->groupinfo = 0; for ( int i = 0; < num; i++ ) { friends[i]->groupinfo = 0; }
有了如上的代码,你就可以让Trace函数忽略任何想忽略的实体,是不是非常⑥呢?