不记得哪个黑色星期五,贪吃鱼基本完工的时候,产品突然增加需求,要求金币扔出去后不消失,互相可碰撞,其最终结果还要由服务器控制(没错,至今做的所有游戏都有幕后黑手,=W=).
对于碰撞以前只写过一个球到处碰墙壁的,小球之间的碰撞倒是没有接触,想到他们碰撞过程中的角度变化、速度分配,就不敢往下想了,于是马上想到box2d这个牛逼哄哄的引擎.
但是,使用物理引擎虽然高效、逼真,但所有碰撞都是不可控,包括最终的落点。所以引擎不能解决这次遇到的需求。
不能用引擎,咱自己写也不怕,反正当年物理和高数都学得还不错,嘻嘻、
最后,趁游戏上线后的空闲时间,整理下碰撞的思路,记录成本文。
如果你喜欢先睹为快,可以先去玩玩这个游戏: view demo >>
本文的核心代码是通过游戏中的逻辑移动过来,为了方便,所以代码组织也和cocos2d的思想类似。
cocos2d中有Scene、director、layer层级之分,在demo中也有对应的init、game、ball,既然用到模块层级,当然首选seajs。
util
模块封装的是一些必要的工具函数
ball
模块封装小球move和drawPredict的类
game
模块控制游戏进度,管理小球的运动
init
模块是游戏入口,调用game.js
的初始化和开始接口
游戏难度集中在ball
和game
模块,所以只分析碰撞过程得碰撞检测和处理。
首先建系很重要。由于我们模拟使用left
和top
来改变坐标,所以我们以table
左上角为坐标原点,垂直向下为x轴正方向,水平向左为y轴正方向。
边缘碰撞处理
边缘碰撞处理较为常规,只需要检测小球坐标和边缘的相对关系即可:
上述代码作用是,检测小球坐标和墙壁大小关系,修改小球运动的角度。
计算角度的源码:
计算的是点(left, top)和x正方向的角度,即下图的∠DAC.
左右碰撞处理方案是乘以-1
,∠DAC是入角,∠DAB和∠DAC正好差个-1
.
上下碰撞是分正负处理。如上图中∠EAC和∠EAB互补,如果反向运动,∠EAB和∠CAB的变化应该是反向再选择180°。
最后注意的是,如果小球坐标超过边界坐标,则要它坐标设为边界坐标,不然就会出现靠着边界来回撞的bug,前人留下来的bug找了好久,= = .
小球碰撞处理
a. 碰撞检测是判断他们的距离和2倍半径的关系:
b. 如果速度太大或者重绘频率太小,怎会看到两个小球互相融入的效果,所以处理第一步是还原碰撞初始状态。
上面代码是修正主动碰的小球的位置,让他退回刚好碰撞的位置,其实就是下图的AB之间的距离,相当于球o1从A退到B.
c. 小球斜碰很难分析,我们把他们的速度旋转到x轴方向。
上面的vx是原来速度v在x方向的速度,vy是v在y方向的速度。把原来的速度顺时针旋转两球圆心弦的角度,以后则不考虑y方向的运动。x方向上动量守恒和能量守恒,即
联立求解可得碰撞后的速度大小。
然后将速度旋转回去:
则碰撞后的速度和角度都可以得出:
注意atan2(y, x)计算的是(x,y)到(0,0)的角度,我们这里使用(vx, vy),计算的是(vx, vy)到(0,0)角的余角。
考虑外力
ball.v = ball.v * (1 - util.LOSS); // 碰撞边缘后减速
故循环跳出条件是 Math.round(v) <= 0
上式moveingballs.remove(i)移除数组指定位置的元素,是手动添加的:
预测线
预测线的运动逻辑和小球一样,唯一不同的是,使用while而不是setInterval, 这样预测路线就会比我们的小球提前运动到指定位置。
由于预测路线和小球运动路径相同,且比小球提前到达,那么就能提前知道小球的停止位置,如果判断到小球到达了不能到达的位置,则可以减小他的速度即可。
预测路线的小黄点,是通过
追加到DOM父节点上的,所以如果一次碰撞小球太多,则会卡死的现象。
在cocos2d中是使用在Layer上drawDot(),几乎没有内存消耗,所以不存在卡的现象。
如果要应用到是项目中,建议使用canvas 或者 documentFragment处理。
另外在贪吃鱼游戏中,运动的过程是通过加速度和路程来计算每次增加的位置的,运动效果比刚才的每次增加相同dx,dy的方案要好,如果对运动效果要求较高,可以考虑这种方式。
实际需求中,很多情况下我们不能使用现成的引擎或者框架,必须要去自己造轮子。
像刚才的碰撞处理中完全靠数学和物理知识,可见学好基本理论知识的重要性,就像作为程序员必需的数据结构和算法一样,总有你不知道的哪天就会用上,所以不断学习这些基础的东西,才能在用的时候随机应变。
最后,源码放在github上: https://github.com/freestyle21/CrashBall