更新日期:2026年8月27日。
项目源码:获取源码。
索引
- 坦克的核心功能
- 八、坦克移动碰撞检测
- 1.获取坦克前方地块
- ①.坦克完全处于道路中间
- ②.坦克处于道路两边
- ③.识别坦克位置
- 2.与地块进行碰撞检测
- ①.砖墙
- ②.钢板
- ③.海水
- ④.玩家老巢
- 3.碰撞后处理
- ①.坦克完全处于道路中间
- ②.坦克处于道路两边
- 4.半边地块的碰撞检测
- 5.限制向上移动
坦克的核心功能
本篇我们介绍坦克的核心功能实现,在上一篇的基础上,整个关卡地图已经创建完成,下一步便是创建坦克单位,实现游戏真正的核心玩法。
内容接上篇:【Unity】TankBattle联机坦克大战(四)坦克的核心功能(上)。
八、坦克移动碰撞检测
由于帧同步所必须的确定性,所以碰撞检测不能基于Unity的物理系统来实现,我们必须实现一套简单的碰撞检测逻辑,并保证其在不同设备、不同环境下保持确定性。
1.获取坦克前方地块
当坦克进行移动时,碰撞检测的本质也即是判断其能否抵达即将移动到的下一位置,而我们的坦克只能向前移动(向后移动时会调转车头,转变为向前移动),所以移动前,首先需要获取即将移动到的下一位置的地块,也即是坦克前方的地块。
注意:后续均以坦克
向上移动为例。
//坦克前方位置(坦克中心坐标 + 坦克尺寸的一半)inttankFrontPos=RealPos.y+TB_UILevel.BlockSizeHalf;获取坦克前方地块要根据坦克所处位置分两种情况进行处理:
- 1.坦克完全处于道路中间:获取坦克前方的一个地块。
- 2.坦克处于道路两边:获取坦克前方的两个地块(一个地块各碰到一半)。
①.坦克完全处于道路中间
这种情况处理起来比较简单,直接以坦克当前坐标为参数,计算出前方地块坐标,再以坐标获取地块即可:
//获取其前方地块TB_Blockfront=_uILevel.GetBlockByPos(pos.x,tankFrontPos);这里要先实现一个能以坐标获取地块的通用方法:
/// <summary>/// 获取一个位置所在的地块/// </summary>publicTB_BlockGetBlockByPos(intposX,intposY){intx=(posX/BlockSize)-1;if(posX%BlockSize>0)x++;inty=(posY/BlockSize)-1;if(posY%BlockSize>0)y++;x=Math.Clamp(x,0,Rows-1);y=Math.Clamp(y,0,Cols-1);return_blocks[x,y];}②.坦克处于道路两边
当坦克处于道路两边时,其位置左右横跨了两个地块,所以其前方的地块也应该分为左右两个:
//获取其前方左侧、右侧地块TB_BlockfrontLeft=_uILevel.GetBlockByPos(pos.x-TB_UILevel.BlockSizeHalf,tankFrontPos);TB_BlockfrontRight=_uILevel.GetBlockByPos(pos.x+TB_UILevel.BlockSizeHalf,tankFrontPos);③.识别坦克位置
如何识别坦克的位置是前两种中的哪一种?只需如下的简单判断即可:
//坦克横坐标(向上移动时)除以地块尺寸能够除尽,则在垂直方向上完全处于道路中间if(pos.x%TB_UILevel.BlockSize==0){}//否则处于道路两边else{}2.与地块进行碰撞检测
获取到了坦克前方的地块,接下来只需要进行碰撞检测即可,首先,我们依然得分类型进行处理。
①.砖墙
由于砖墙可拆分为4行*4列=16个组成片,所以碰撞检测最为精细,首先要检测最下面的一行(行号0)组成片是否与坦克产生碰撞(因为坦克是向上移动,所以最下面的一行理应最先碰到)。
某行砖墙组成片能够达成碰撞(对坦克产生阻挡排斥效果)的条件如下:
- 1.该行砖墙组成片未被完全破坏;(比如还剩下两边的一块,也能产生阻挡效果,这在原版里经常被用作
挡路机关) - 2.
坦克前方y坐标高于该行砖墙组成片的低坐标(向上移动);
挡路机关:
满足如上两个条件,则表明坦克碰到了该行砖墙组成片,应该被阻挡排斥,代码实现如下:
//!IsDestroyRow(0):行号0的整行组成片未被完全破坏//frontY > pieceY:坦克前方y坐标高于该行组成片的低坐标if(!IsDestroyRow(0)&&frontY>pieceY){//阻挡排斥:将该行组成片低坐标,赋予坦克前方y坐标,实现将坦克挡在后面的效果frontY=pieceY;}检测某一行的组成片是否完全被销毁:
/// <summary>/// 指定一行的组成片是否完全被销毁(仅针对砖墙或钢板)/// </summary>/// <param name="row">行号</param>publicboolIsDestroyRow(introw){if(Type==BlockType.BrickWall){if(row>=0&&row<4){return_brickWallPieces[0,row].IsDestroy&&_brickWallPieces[1,row].IsDestroy&&_brickWallPieces[2,row].IsDestroy&&_brickWallPieces[3,row].IsDestroy;}}elseif(Type==BlockType.SteelPlate){if(row>=0&&row<2){return_steelPlatePieces[0,row].IsDestroy&&_steelPlatePieces[1,row].IsDestroy;}}returnfalse;}如果第一行砖墙组成片未产生碰撞,则依次判断后面三行:
//判断第一行if(!IsDestroyRow(0)&&frontY>pieceY){}//判断第二行(第二行的低坐标 = 第一行低坐标 + 地块尺寸的四分之一)elseif(!IsDestroyRow(1)&&frontY>(pieceY+TB_UILevel.BlockSizeQuarter)){}//判断第三行(第三行的低坐标 = 第一行低坐标 + 地块尺寸的二分之一)elseif(!IsDestroyRow(2)&&frontY>(pieceY+TB_UILevel.BlockSizeHalf)){}//判断第四行(第四行的低坐标 = 第一行低坐标 + 地块尺寸的四分之三)elseif(!IsDestroyRow(3)&&frontY>(pieceY+TB_UILevel.BlockSizeHalf+TB_UILevel.BlockSizeQuarter)){}完事后,这一块砖墙的碰撞检测才算完成。
②.钢板
钢板与砖墙的碰撞检测类似,只不过钢板的组成片为2行*2列=4个,所以只需检测两行:
//判断第一行if(!IsDestroyRow(0)&&frontY>pieceY){}//判断第二行(第二行的低坐标 = 第一行低坐标 + 地块尺寸的二分之一)elseif(!IsDestroyRow(1)&&frontY>(pieceY+TB_UILevel.BlockSizeHalf)){}③.海水
海水也能产生阻挡效果,其是整个地块生效,所以处理极其简单:
//如果碰撞到的地块为海水elseif(Type==BlockType.Seawater){//则直接阻挡排斥:将该地块低坐标,赋予坦克前方y坐标frontY=pieceY;}④.玩家老巢
玩家老巢也能产生阻挡效果,其是整个地块生效,所以处理方式与海水一致:
//如果碰撞到的地块为玩家老巢elseif(Type==BlockType.Lair){frontY=pieceY;}3.碰撞后处理
碰撞后的处理也应分两种情况。
①.坦克完全处于道路中间
这种情况只需将碰撞产生的排斥坐标赋予坦克即可:
//获取其前方地块TB_Blockfront=_uILevel.GetBlockByPos(pos.x,tankFrontPos);//与地块碰撞检测,如果产生碰撞,则返回排斥后坐标pos.y=front.TankCollideBlockUp(tankFrontPos,0);②.坦克处于道路两边
这种情况需要判断坦克前方的两个地块,以其中的排斥坐标的较小值作用到坦克上(因为我们处理的是向上移动,较小的坐标意味着越靠下,所以其越先阻挡坦克的前进):
//获取其前方左侧、右侧地块TB_BlockfrontLeft=_uILevel.GetBlockByPos(pos.x-TB_UILevel.BlockSizeHalf,tankFrontPos);TB_BlockfrontRight=_uILevel.GetBlockByPos(pos.x+TB_UILevel.BlockSizeHalf,tankFrontPos);//先判断左侧地块intleftPos=frontLeft.TankCollideBlockUp(tankFrontPos,1);//再判断右侧地块intrightPos=frontRight.TankCollideBlockUp(tankFrontPos,-1);//左侧和右侧取最小值pos.y=Mathf.Min(leftPos,rightPos);如下图,左侧地块的排斥坐标较小,其产生的阻挡效果才生效:
4.半边地块的碰撞检测
如上,坦克处于道路两边时需要检测地块的一半(左半边或右半边)是否产生碰撞,所以,我们必须为碰撞检测方法增加一个参数pos:
/// <summary>/// 坦克向上移动时碰撞到地块/// </summary>/// <param name="y">坦克前方位置的y坐标</param>/// <param name="pos">碰撞位置(-1:碰到左半边,0:碰到整个地块,1:碰到右半边)</param>/// <returns>被地块排斥后的新y坐标</returns>publicintTankCollideBlockUp(inty,intpos)当仅检测半边地块的碰撞时,也只需检测半边组成片是否被销毁:
//!IsDestroyRowHalf(0, -1):行号0的左半行组成片未被完全破坏if(!IsDestroyRowHalf(0,-1)&&frontY>pieceY){frontY=pieceY;}5.限制向上移动
基于上文的代码基础,我们将其统一为了一个限制坦克向上移动的方法:
/// <summary>/// 限制坦克向上移动(碰到砖墙、钢板、海水等)/// </summary>privateVector2IntLimitMoveUp(Vector2Intpos){//坦克前方位置inttankFrontPos=pos.y+TB_UILevel.BlockSizeHalf;pos.y=tankFrontPos;//坦克位置在垂直方向上处于两个地块之间if(pos.x%TB_UILevel.BlockSize==0){//获取其前方左侧、右侧地块TB_BlockfrontLeft=_uILevel.GetBlockByPos(pos.x-TB_UILevel.BlockSizeHalf,tankFrontPos);TB_BlockfrontRight=_uILevel.GetBlockByPos(pos.x+TB_UILevel.BlockSizeHalf,tankFrontPos);//先判断左侧地块intleftPos=frontLeft.TankCollideBlockUp(tankFrontPos,1);//再判断右侧地块intrightPos=frontRight.TankCollideBlockUp(tankFrontPos,-1);//左侧和右侧取最小值pos.y=Mathf.Min(leftPos,rightPos);}//坦克位置在垂直方向上处于一个地块上else{//获取其前方地块TB_Blockfront=_uILevel.GetBlockByPos(pos.x,tankFrontPos);//碰到整个地块pos.y=front.TankCollideBlockUp(tankFrontPos,0);}//坦克位置回到中心pos.y-=TB_UILevel.BlockSizeHalf;returnpos;}同理,向下移动、向左移动、向右移动等都需要完全按照本文的碰撞检测流程走一遍,只是坐标与方向上存在不同,原理是一致的,所以在这里我就不再赘述了。
最终,根据坦克朝向的不同,调用不同的方法即可:
//限制移动(碰到砖墙、钢板、海水等)switch(Dir){caseDirection.Up:RealPos=LimitMoveUp(RealPos);break;caseDirection.Down:RealPos=LimitMoveDown(RealPos);break;caseDirection.Left:RealPos=LimitMoveLeft(RealPos);break;caseDirection.Right:RealPos=LimitMoveRight(RealPos);break;}