| 赞 | 79 |
| VIP | |
| 好人卡 | |
| 积分 | 71 |
| 经验 | |
| 最后登录 | 2026-1-26 |
| 在线时间 | 706 小时 |
- 梦石
- 2
- 星屑
- 5065
- 在线时间
- 706 小时
- 注册时间
- 2018-12-11
- 回帖
- 213

|
加入我们,或者,欢迎回来。
您需要 登录 才可以下载或查看,没有账号?注册会员
×
本帖最后由 lisliz 于 2020-10-5 16:12 编辑
今天酒喝多了,话就有点多。之前我在招募板块发帖动作游戏企划招队友,有人喊我用PGMMV来着。
用pgmmv直接写好的脚本做动作游戏,确实看起来会更快,但是你要想,pgmmv它再好,始终是别人写好给你的,你并不懂里面是什么原理,这样你就很难定制自己想要的东西,而且你要了解它还会有很大的接触成本。
这样就好比各位做一款好点的剧情游戏不让你用原创素材一样,是非常麻烦的,非原创素材(类比PGMMV),他并不是写给你的游戏的,而且你多半还改不了它,无形中限制了你去精雕细琢的可能性。
我截图一哈这两天空洞骑士STEAM底下的评价哈,大家看看。
你说人家游戏为啥就好玩,“好玩”这两个字分量有多重?那大家觉得用人家做好的战斗插件或者说写好的PGMMV能达到这个分量么?
这种玩法上登峰造极的作品,肯定是需要一个非常强大的策划,和一个什么好玩的东西都能实现的神仙(gong ju ren)程序。所以,看上面的红色字体,现在知道原因了吧。
好的,我们接下来就在MZ里实现一个小型物理引擎。以下折叠过于硬核,慎点!包括碰撞检测,各种数学,物理等烧脑逻辑。
[fold=MZ里物理引擎实现的过程]
一个2D横板动作游戏,恐怕最重要的核心脚本就是物理引擎了。它负担着一个游戏的碰撞判定,物体的运动轨迹计算,受力分析,实现动量定理等能体现经典力学的公式。所有的关卡设计都要在这个系统上进行书写,它是游戏世界的法则,是万物的规律,还直接影响玩家的操作和手感。其重要性不言而喻,甚至可以直接用生死线来衡量。
我对这个系统的重视度已经拉到五颗星。
而偏偏这个系统还是最最最难做好和技术力要求最最高的,对于程序来说简直可以算是美术里的清明上河图。
为什么说它要求的技术力是顶尖水平,可以看看这个知乎上的讨论:https://www.zhihu.com/question/4 ... =970094161945346048
要设计物理引擎,首先要实现碰撞检测功能,因为要执行物理法则,必须知道两个物体是否相撞。动作游戏里比较常见的地形有矩形,三角形等。
两个多边形(以下均为凸多边形)是否相交,你们说在脚本里怎么判定?
相交有两种情况,一种是一个多边形完全容纳了另一个多边形。
情况A:像下面这个三角形和矩形。
对于情况A,如果一个多边形所有顶点都在另一个多边形内部,就可以认为该多边形完全在另一个多边形内部。
关于某个点是否在多边形内部,可以利用向量叉积去判断,参考资料:https://www.cnblogs.com/calence/p/9988172.html
我先把向量叉积的实现脚本贴出来,等下会多次用到这个。
[pre lang="javascript"]// 两个向量的叉积
Utils.vectorProduct = function(vc1, vc2) {
return vc1.x * vc2.y - vc1.y * vc2.x;
};
//【点basePoint和点p1连成的向量】和【点basePoint和点p2连成的向量】的叉积
Utils.basePointVectorProduct = function(basePoint, p1, p2) {
const vc1 = {x:p1.x - basePoint.x,y:p1.y - basePoint.y};
const vc2 = {x:p2.x - basePoint.x,y:p2.y - basePoint.y};
return this.vectorProduct(vc1, vc2);
};
[/pre]
如图,例如多边形ABCDE,求点P是否在多边形内部。对于程序,只需要依次判断,向量叉积AB*AP,BC*BP,CD*CP,DE*DP,EA*EP的符号是否全部相同,这个不难理解,向量叉积本身就是判断A向量在B向量的哪一侧,如果全部都在同一侧(符号相同),自然就在多边形内部。
但是要注意一种特殊情况,就是点P在多边形的边界上,例如在边AB上,这种向量叉积AB*AP=0,会导致程序判定出问题,因此如果发现叉积=0的情况则该轮结果舍弃,不判断。大家可以看到我的实现里,会判断pd1是否等于0,如果是则不赋值给下一轮判断,等于直接跳过。
[pre lang="javascript"]// 点是否在凸多边形中
Utils.isPointInConvexPolygon = function(p, vertices) {
let vertex = vertices[0];
let pd = 0;
vertices.push(vertex);
for(let i = 1; i < vertices.length; i++) {
const pd1 = this.basePointVectorProduct(vertex, p, vertices);
if(pd1 * pd < 0) {
vertices.pop();
return false;
}
if(pd1 !== 0) {
pd = pd1;
}
vertex = vertices;
}
vertices.pop();
return true;
};[/pre]
这样,依次判断多边形A的所有顶点是否在多边形B中,再判断多边形B的所有顶点是否在多边形A中,就可以判断出上述情况A的发生了。
然而,这还不够,两个多边形相交还有另一种情况。
情况B:另外一种是有一截露在外面,如下:
这种情况,两个多边形的边必然有相交的情况。所以这个问题转化成两个多边形的边(线段)是否相交。关于线段是否相交,还是要用向量叉积去算。参考这个文章:https://www.cnblogs.com/tuyang1129/p/9390376.html
两个线段,AB和CD是否相交,则判断向量叉积AB*AC于AB*AD结果异号,且向量叉积CD*CA于CD*CB结果异号。但是凡事都有特殊情况,倘若AB和CD在同一条直线上,则前面叉积结果都会是0。无法用叉积的方法判断。这种情况需要判断A是否在CD上或B是否在CD上,则代表AB与CD相交。
另外,特殊情况还有如下两种:
第一种:A和C是同一个点,等于AB和CD两条线段首尾相连
第二种:A在线段CD上,但AB与CD不在一条直线上
以上所有情况我在代码里都注明了,大家可以看看我的js实现。
[pre lang="javascript"]// 两个线段是否相交
Utils.isSegmentIntersect = function(line1, line2) {
const pd1 = this.basePointVectorProduct(line1.p1, line1.p2, line2.p1);
const pd2 = this.basePointVectorProduct(line1.p1, line1.p2, line2.p2);
const pd3 = this.basePointVectorProduct(line2.p1, line2.p2, line1.p1);
const pd4 = this.basePointVectorProduct(line2.p1, line2.p2, line1.p2);
const c1 = pd1 * pd2;
const c2 = pd3 * pd4;
if(c1 > 0 || c2 > 0) {
return false;
}
else if(c1 < 0 && c2 < 0) {
return true;
}
else if(pd1 === 0 && pd2 === 0 && pd3 === 0 && pd4 === 0) { // 特殊情况,ABCD在同一直线上
const b1 = this.pointBigger(line1.p1, line2.p1); // 判断点A是否在CD上
const b2 = this.pointBigger(line1.p1, line2.p2);
if(b1 === 2 || b2 === 2) { // 特殊情况,A和C是同一个点,或者A和D是同一个点
return true;
}
if(b1 ^ b2) {
return true;
}
const b3 = this.pointBigger(line1.p2, line2.p1); // 判断点B是否在CD上
const b4 = this.pointBigger(line1.p2, line2.p2);
if(b3 === 2 || b4 === 2) { // 特殊情况,B和C是同一个点,或者B和D是同一个点
return true;
}
if(b3 ^ b4) {
return true;
}
return false;
}
else { //两线段不平行但顶点重合,或者A线段的顶点在B线段上
return true;
}
};
Utils.pointBigger = function(p1, p2) {
if(p1.x > p2.x) { return 1; }
if(p1.x === p2.x && p1.y > p2.y) { return 1;}
if(p1.x === p2.x && p1.y === p2.y) { return 2; }
return 0;
};
[/pre]
好的,终于要大功告成了,两个多边形是否相交,只需判断情况1和情况2是否发生。因此:
[pre lang="javascript"]// 两个凸多边形是否相交
Utils.isConvexPolygonIntersect = function(vertices1, vertices2) {
if(this.isConvexPolygonContains(vertices1, vertices2)) { // 多边形1是否被多边形2包含
return true;
}
if(this.isConvexPolygonContains(vertices2, vertices1)) { // 多边形2是否被多边形1包含
return true;
}
let vertex1 = vertices1[0];
for(let i = 1; i < vertices1.length; i++) { // 多边形1是否有任意一条边与多边形2相交
const line1 = {p1: vertex1, p2: vertices1};
let vertex2 = vertices2[0];
for(let j = 1; j < vertices2.length; j++) {
const line2 = {p1: vertex2, p2: vertices2[j]};
if(this.isSegmentIntersect(line1, line2)) {
return true;
}
vertex2 = line2.p2;
}
vertex1 = line1.p2;
}
return false;
};[/pre]
你以为到这就完成了么,还早哩!我技术最高难度代表岂是这么简单就能搞定的?现在有一个判断是否碰撞的算法,但是他不能帮我们计算下面这种情况:
例如,当游戏时间在第一帧时,紫色是矩形物体的所在位置,而到了第二帧,矩形移动到红色框的位置,我们通过算法已知矩形与三角形发生了碰撞,但是:如何计算出矩形与三角形正好相切(绿色矩形)的所在位置?
逐像素去扫描?你觉得JS引擎是否能受的了这计算量?
解决这个问题之前,我想先介绍一下如何将物理引擎接入到RM中。首先,物理引擎最终输出的是物体的位置。我们可以将RM里的Game_CharacterBase加装成碰撞对象。
Game_CharacterBase里要定义如下这么几个变量,才能让物理引擎运转起来。
- 物体的当前位置px/py
- 经过物理引擎修正后的位置rx/ry
- 物体的速度vx/vy
- 物体的质量m
- 物体的弹力elastic
- 物体的表面摩擦力friction
具体的思路就是每帧都根据物体当前的速度运算出当前物体应该运动到哪里了,然后再用碰撞检测算法把物体修正到一个正确的位置避免卡墙等问题发生。
RM里,Game_CharacterBase里决定该对象在屏幕中显示的坐标的函数是screenX()和screenY(),所以我们只要把物理引擎的运算结果输出到这两个函数里,就可以在RM里看到实际效果。
上面那个矩形和三角形的问题,我是采用了二分逼近法去避免逐像素去分析。
具体思路就是,将红色矩形拉回到距离紫色矩形1/2的位置,看看是否相交,如果是继续拉回离紫色矩形1/4的位置,否则,往反方向拉1/4的位置。循环8次,直到逼近最终的结果。
[/fold]
好,经过国庆这几天几乎没日没夜摧残身心的脚本编写和调试,终于重构了一个还算看得过去的运行在RPGMAKER MZ上的2D横板物理引擎。
说起重构,因为之前也有在RMMV上编写过2D物理引擎,所以这次已经是个人的第四代重构版本了。第二代版本发布于今年2月份,作为《喵可莉的兔玩偶》STEAM版本的彩蛋房间小游戏,但是因为被玩家吐槽手感屎,所以发布第二天就删除掉了。
第三代版本发布于今年8月22日,作为《喵可莉的兔玩偶》STEAM上的DLC内容的一个隐藏要素(小游戏)呈现给玩家,现今仍然可以游玩到。
以下折叠是第四代重构物理引擎实现的功能展示:
[fold=惯性和摩擦力]
大家可以通过GIF看到我站在一个左右来回移动的飞板上。随着飞板突然改变方向,我的身体并没有立刻也跟着改变方向,而是因为惯性往原来方向滑动一小段距离,但因为摩擦力的原因最终还是和飞板的速度保持一致。
[/fold]
[fold=重力和跳跃]
GIF里这个白毛猫猫就是我,我给自己写了二段跳和空中冲刺的能力。看起来有没有觉得手感不错?
[/fold]
[fold=地形和碰撞]
我最大限度的保留了RM原有的地图和事件功能,因此,RM的地图编辑器和事件编辑器仍然正常可用。
上图是物理引擎功能展示所用地图在RM本体里的表现。并且,还可以实现6楼所述的特殊地形,如下图GIF:
[/fold]
[fold=动量定理和弹性碰撞]
在这个物理引擎中,物体都有一个elastic(弹力)参数,elastic=0,代表该物体执行的是完全非弹性碰撞,反之elastic=1,该物体执行的是完全弹性碰撞。当然,也能为elastic赋值0.5代表这个物体碰撞介于弹性碰撞和非弹性碰撞之间。
值得一提的是,在游戏中,有一类物体虽然拥有碰撞判定,但是它的质量为无穷大,也不受任何外力影响。这类物体类似于上面的飞板,不会被任何碰撞改变轨迹。这类物体发生弹性碰撞时,处理会比较特殊,但还是遵循物理学法则的。
我们着重展示下质量非无穷大的物体的碰撞计算结果。
上面的GIF,我的角色是白毛,质量m=1且elastic=0,然后NPC角色黑毛也是一个质量m=1且elastic=0的物体,我可以把她顶起来,推下去,但因为摩擦力和重力的原因,推动很困难的。
这张GIF和上面的一样,但我把黑毛的质量m改成了0.2,根据动量定理,我能更轻易的把她推走,而且能顶的更高。
那如果我再把黑毛的elastic(弹力)参数设置成1会发生什么?大家请看下图。
当然,如果黑毛是一个弹力十足的物体(elastic=1),那我们俩执行的碰撞就是完全弹性碰撞,而且黑毛本身也会在地板上“弹个不停”,这是物理法则,对吧!
[/fold]
[fold=感想]
我都快神经衰弱了,这东西是真的难做好。国庆五天没日没夜的就肝这个了,代码改来改去的。现在这个物理引擎也有局限性,而且我还只是挑好的展示,还有一些判定有BUG,我先躺会儿,晚上继续修。
之前做喵兔老是觉得自己是程序不会画画没文学功底做剧情游戏很揭短,好嘛,我们换个游戏类型。然后做了动作游戏才知道,这两种游戏都不是省油的灯,各有各的难点。说出来不怕你们笑,其实我是很想做和rabi-ribi,空洞骑士一样好玩的游戏的,想的是好,做起来可真要命。
[/fold] |
评分
-
查看全部评分
|