Unity简单的2D物理系统的实现
遇见的问题
在使用unity创建2D项目的时候,虽然unity自己提供了例如Rigidbody2D与Collider2D等组件来提供对2D项目的支持.但unity的本质依然是一个3D引擎,这里提供的2D的组件依然是基于3D引擎,无法对真正的2D项目提供很好的支持.
解决的办法
unity的2D组件在为2D Platform类型的项目提供支持的时候,只能说功能堪堪可以使用,但是依然有很大的需要改变的地方. 比较好的办法是自己来创建一个满足2D项目需求的物理系统.
一部分效果展示
性能与功能对比
在场景内放置1000个仅携带BoxCollider2D(不含刚体)的会移动的物体(携带自制的动画控制器与AI),此时的帧数仅有6帧.

在场景内放置10000个不携带unity提供的碰撞体的会移动物体,此时帧数尚且能保持15帧左右

在场景内发射1100发速度固定,并且未碰撞时生命时常无限的携带BoxCollider2D与Kinematic类型的Rigidbody2D子弹,此时的帧数也仅有个位数的了.

在上一图中,如果去掉刚体,此时发射1100发子弹帧数依然有100帧左右.
此时继续追加子弹,当子弹数目追加刀1500发时,帧数仅剩个位数.
如果在去掉BoxCollider2D并开启自制物理系统的碰撞检测,放置5个允许子弹碰撞的运动物体(即每发子弹每帧需要与5个物体做碰撞检测,这个数目不包含墙壁等),此时将子弹增加到1万发以上,依然能有很高的帧数

继续将子弹追加刀2万发,效果如下图

最终追加到3万发子弹,帧数仍然有20帧以上
其他说明:这些物体允许与平台(包括地面墙壁顶部与单向板子)碰撞并且禁止同行(或允许单向通行).
这些物体也允许被玩家碰撞,对玩家造成伤害(如果取消怪物对玩家的碰撞效果,改为攻击行为才造成伤害,性能也会更好).
这些物体也允许被子弹碰撞,允许触发子弹造成的伤害或者效果.
目前物体(包括玩家或者怪物)与平台的碰撞使用射线实现,在使用中遇见了挺多的麻烦,性能也相比之下不如自制的物理系统.目前已有解决方案,预计会在下一个版本完全由自制物理系统实现.
单向通行板
允许玩家(或者敌人,或者指定敌人)单向通行(包括跳跃)
无碰撞体与刚体的玩家与无碰撞体与刚体的敌人或者道具碰撞
允许玩家与其他不包含碰撞体或者刚体的物体互相碰撞并且触发碰撞事件
方形碰撞器/圆形碰撞器相互碰撞的实现
方形碰撞器(A)与其他碰撞器(B)的碰撞检测
取A的四个顶点,只要任意顶点落入B的范围之内,则判定为发生碰撞
圆形碰撞器(A)与方形碰撞器(B)的碰撞检测
取B的四个顶点,只要任意顶点落入A的范围之内,则判定为发生碰撞
圆形碰撞器(A)与圆形碰撞器(B)的碰撞检测
计算AB的圆心距即可
简易优化
计算方形碰撞器的时候,根据物体位于的相对方位(上下左右)优先计算左上和右下或者右上和左下两个顶点,可以更少的计算就能获得结果.
动态碰撞体A与动态碰撞体B的X或者Y的距离大于一定值,则一定不相撞,省略后面的计算.
动态碰撞体A与动态碰撞体B的X或者Y的距离大于一定值,根据这个值计算两个物体最短M帧后才会碰撞,在此期间将B移除与A的碰撞检测,在M帧之后放回.
动态碰撞体A与纯粹类静态碰撞体B(如墙壁),同一时间一定只能有A与一个B相撞(完成纯粹类静态碰撞体融合之后),此时只用计算A在情况时候脱离B即可.
注:三角形碰撞器下个版本实现
纯粹类静态碰撞体(墙壁与静态触发点等)目前的难点
由于动态碰撞体和道具类静态碰撞体都是一个固定类型(大小与顶点等),创建的同类的物体碰撞体都是固定的参数.
但是纯粹类静态碰撞体,本身的大小与顶点等参数可能每一个都不同,每单独一个碰撞体就归于一类实在是工作量比较大.
解决方案1
制作固定可重用的Tile,并且相邻的Tile会融合形成一个整体,提高碰撞检测效率.但是可能得做一部分unity editor的开发,然后就是每一种瓦片就得花费一些时间制作一下.最后则是绘制好的地图需要做序列化然后存储,在游戏运行的时候来反序列化,然后生成关卡,在关卡生成的时候会自动完成碰撞体的注册.
解决方案2
依然使用GameObject,会为该对象附加一个碰撞体组件与一个Mono Behavior组件,这个组件会自动获取该对象的碰撞体,并且计算大小与顶点后注册在自制物理系统中.
一旦注册完成则自动销毁该碰撞体与脚本. 这个方案的缺点是没法完成覆盖的碰撞体的融合与拆分,如果地图设计不合适,会提高一部分资源消耗.
非刚体移动的问题与解决方案
角色嵌入墙体的问题
由于在我的自制物理系统中完全抛弃的刚体,那么所有的移动都是瞬移(Translate)来完成的,如果角色速度过大,可能导致角色陷入墙体.
为什么会发生这个情况?
假设当前是第A帧,此时角色的速度为V,正面的墙壁与角色的距离为D,且有D<V,并且D>0.
在上面的场景中,当前帧角色没有与墙壁相撞,这意味着角色当前帧可以向当前方向移动.如果移动完成,此时角色与墙壁的距离为D-V=R.根据条件可知R<0,此时角色就已经嵌入墙壁中了.
如何解决
将角色与墙壁碰撞检测距离延长为当前速度V,当检测到角色与墙壁发生碰撞的时候,此时角色与墙壁的距离D必然有D<V且D>=0,那么当前帧使V=D,下一帧到来时角色最大位移D的距离,此时角色与墙壁的距离为0,角色与墙壁相撞,当前帧我们不在允许角色继续向该方向移动.
斜坡移动的问题
即使unity自身也没有提供2D项目的斜坡移动方案,这个问题当然只能自己解决了.
在下面的例子中,我们假设角色向右移动,且使用了方形碰撞器,并且角色的移动能够越过高度为Y的坎或者坡度小于arctan(Y/V)的坡
如何正确的斜坡移动(向上)
取角色右上(P1)与右下(P2)两个顶点,向右检测D的距离(可以额外用一帧让玩家完全靠近后在做接下来的操作),如果P1检测成功但是P2检测失败,则说明玩家向右移动遇见了斜坡(或者坎).
此时我们声明一个新的点P3,这个点与P2点在X轴距离V个单位,在Y轴距离Y个单位.
之后由P3点向下检测,假设P3与斜坡(坎)的距离为M.如果M<=0,则说明坡度过大或者坎过高,因为这次位移会导致玩家进入墙体,因此禁止位移.如果M>0,则说明玩家的该次位移没有问题.此时玩家的位移向量为(X,Y-M)
如何正确的斜坡移动(向下)
实际上,如果你之前物理系统检测玩家是否在地板上的代码没有问题的话,此时玩家的角色就会正确的自动向下移动才对.
但是如果每次一旦检测到玩家踏空,就调用TransientToFloating()完成状态转化的话,这里确实会出问题.每一帧的移动都会导致玩家转换到浮空状态然后复原站立/奔跑状态(这会导致动画转换鬼畜),在不恰当的场合玩家会做出多次状态转化. 这意味着也许需要专门针对斜坡向下的移动来完成指定的功能. 这部分问题预计下一个版本解决.