最近比较流行团体战斗的游戏,比如肉鸽+SLG,肉鸽+割草等类型。这里面团体战斗的时候,如何防止战斗中的角色堆叠是其中的一个重点,第一次做这类游戏的朋友感觉很难,实际上非常简单,今天就直接告诉大家如何做到,而不是”装神弄鬼“的来渲染”各种系统与技术多么NB“。先看效果:
1: 为什么不用"物理引擎"与"RVO"
很多人的第一反应就是为什么不用"物理引擎","动态壁障"来处理,这里主要是和这类游戏的需求有关系。因为在 ”走位“的过程中允许角色物体之间可以相互穿透,主角不会被怪物"卡住"。所以就不能使用"物理引擎"与"动态壁障"。需要自己编写规则来控制角色的团体战斗与移动,同时也需要自己制定规则来提供机制防止怪物之间的堆叠。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀
要满足这样的需求,那么寻路导航,战斗队形,与堆叠处理都需要自己编写代码来写规则。
2: 出现"堆叠"的常见情况与处理规则
堆叠: 每个角色都有一个互斥半径。当两个角色的距离小于角色1的互斥半径 + 角色2的互斥半径时,这时我们称角色A与角色B堆叠。

如果自己控制角色的移动,战斗我们就要分析出来出现堆叠的常见的处理情况与处理规则:
1: 同类队友更容易堆叠,异类(自己与敌人)基于攻击范围等距离控制,不容易堆叠在一起。如:角色与怪物走进了自己的攻击区域,则发起攻击,不会继续前进,自然就通过攻击来将"异类"的堆叠避免,如果有必要,也可以把同类堆叠处理改成全类防堆叠处理。算法不变。只是运算规模会增加。
2: 枚举战斗中的需要处理”堆叠“的重要的状态,Idle, Attack, Run。这里我们把这些状态分成两类,"动态角色(Idle || Attack)"与静态角色(Run)", 于是整个战斗场景中的堆叠就分成了3种情况:
(1)两个静态的角色堆叠在一起。为了让他们保持安全距离,处理时,就沿着两人连线的方向,相互推开即可,直到他们恢复安全距离(A的互斥半径+B的互斥半径之和)。

(2)两个动态的角色堆叠在一起,如果这两个角色是不同方向的移动,那么分开只是时间问题,无需要做任何的处理。如果两个角色的方向相同,这个时候就会发生堆叠在一起来行走。处理方式如下: 让其中一个角色A回退回到上一次前进的结果(视觉上,角色A停止不前进),让另外一个角色B正常行走,等他们拉开距离后恢复。比如A, B两个角色同向向右移动,当A,B发现彼此"堆叠"后,A迭代完移动,然后堆叠处理模块再把A往回迭代,这样A就不动,B正常移动,这样多次处理后,A与B保持安全距离了,则A继续移动。这样便分开了两个"同向堆叠移动"的物体。如何判断同向堆叠,规则为"当两个角色的速度的方向的夹角小于5度"的时候视为"同向"。

(3)一个静态的角色与一个动态的角色堆叠在一起,这种情况不用处理,交给时间,它们两个自然会分开。

3: 结构与算法
1: "堆叠处理"发生的时期:
"堆叠模块"在每次迭代的最后的逻辑处理。如下图的代码接口,战斗迭代的时候,堆叠发生在寻路导航,AI处理,技能释放等迭代的最后,最后再调用堆叠模块"来"修正堆叠"现象。

2: 找出战斗场景中的"堆叠对"。
private FindLegionNearPairUpdate(): void {// 找出entity靠的太近的entity对;FightWorldMgr.Instance.ForEachTypeEntity(EntityType.Legion, (e: CharactorEntity2D)=> {var target = EntityCtrlUtils.FindOneOverlapEntityInWorld(e, EntityType.Legion);if(target) {// 排除掉重复的, A, B, B, A这种情况;if(!this.IsPairAlreadyExsit(this.legionEntitiesPair, e, target)) {var pair = new NearPairEntitys();pair.entityA = e;pair.entityB = target;this.legionEntitiesPair.push(pair);}}return 1;});}
public static FindOneOverlapEntityInWorld(entity: CharactorEntity2D, entityType: number): CharactorEntity2D {var target = null;FightWorldMgr.Instance.ForEachTypeEntity(entityType, (e: CharactorEntity2D)=>{if(entity === e) { // 同一个entity,排除;return 1;}// 判断两个物体的距离let dis = Vec3.squaredDistance(entity.uTransform.pos, e.uTransform.pos);let minLen = entity.uInfo.charactorData.MutexR + e.uInfo.charactorData.MutexR;// console.log("yes it is", minLen, dis, entity.uTransform.pos.y, e.uTransform.pos.y);if(dis <= (minLen * minLen)) {target = e;return -1;}// endreturn 1;});return target;}
3: 处理堆叠对,分成"静态类堆叠"处理与"动态类堆叠"处理:
step1: 找出场景中的"堆叠对"
step2: 处理每个"对谍对",排除掉不用处理的情况;
update(dt: number): void {// 军团// this.legionEntitiesPair = [];this.legionEntitiesPair.length = 0;this.FindLegionNearPairUpdate();// console.log(this.legionEntitiesPair.length);this.HandleNearMutexPair(this.legionEntitiesPair, dt);// 怪物// this.monsterEntitiesPair = [];// this.monsterEntitiesPair.length = 0;// this.FindMonsterNearPairUpdate();// this.HandleNearMutexPair(this.monsterEntitiesPair, dt);}
private HandleNearMutexPair(mutexPairSet: Array<NearPairEntitys>, dt: number): void {for(var i = 0; i < mutexPairSet.length; i ++) {var pairData = mutexPairSet[i];// 处理静态if((pairData.entityA.uStatus.status === CharactorStatus.Idle || pairData.entityA.uStatus.status === CharactorStatus.Attack1 || pairData.entityA.uStatus.status === CharactorStatus.Attack2) &&(pairData.entityB.uStatus.status === CharactorStatus.Idle || pairData.entityB.uStatus.status === CharactorStatus.Attack1 || pairData.entityA.uStatus.status === CharactorStatus.Attack2)) {this.ProcessNearMutexWithStationary(pairData, dt);}// 处理动态else if(pairData.entityA.uStatus.status === CharactorStatus.Run &&pairData.entityB.uStatus.status === CharactorStatus.Run) {this.ProcessNearMutexWithDynamic(pairData, dt);}}}
// 静止状态下的距离互斥处理private ProcessNearMutexWithStationary(pairData: NearPairEntitys, dt: number): void {var dirAB = v3(0, 0, 0);Vec3.subtract(dirAB, pairData.entityB.uTransform.pos, pairData.entityA.uTransform.pos);dirAB = dirAB.normalize();var len = this.mutexSpeed * dt;var offsetX = len * dirAB.x;var offsetZ = len * dirAB.z;// entityA 反方向移动pairData.entityA.uTransform.pos.x -= offsetX;pairData.entityA.uTransform.pos.z -= offsetZ;// end// entityB 反向移动pairData.entityB.uTransform.pos.x += offsetX;pairData.entityB.uTransform.pos.z += offsetZ;// end// 后续考虑攻击围绕着目标来移动,暂时不处理console.log("ProcessNearMutexWithStationary", len, offsetX, offsetZ);// end}
// 两个角色在一条先上行走,这样看上去就重叠了,需要处理;private ProcessNearMutexWithDynamic(pairData: NearPairEntitys, dt: number): void {console.log("ProcessNearMutexWithDynamic");var rA = Math.atan2(pairData.entityA.uAStarNav.vz, pairData.entityA.uAStarNav.vx);var rB = Math.atan2(pairData.entityB.uAStarNav.vz, pairData.entityB.uAStarNav.vx);if(Math.abs(rA - rB) > (5 * Math.PI) / 180) { // 速度的方向超过了5度,那么就不用管;return;}// 改变一个其中一个,停一停,等一等,反方向回退pairData.entityA.uTransform.pos.x -= pairData.entityA.uAStarNav.vx * dt;pairData.entityA.uTransform.pos.z -= pairData.entityA.uAStarNav.vz * dt;pairData.entityA.uAStarNav.passedTime -= dt;// end}
END
845

被折叠的 条评论
为什么被折叠?



