《球球大作战》源码解析:移动算法

作者:罗培羽 2019-03-06
系列文章
《球球大作战》源码解析——(1)运行起来
《球球大作战》源码解析:服务器与客户端架构
《球球大作战》源码解析:移动算法
《球球大作战》源码解析(6):碰撞处理

《球球大作战》源码解析(7):游戏循环
《球球大作战》源码解析(8):消息广播

服务端处理了游戏的各种游戏逻辑,怎样让小球移动是重点之一。若想做服务端运算的游戏,这一部分代码是很值得参考的。

SetInterval

nodejs中的setInterval是定时器,如下的代码表示每隔1秒执行一次myfunc方法。

  1. setInterval(myfunc,1000);

  2. function myfunc(){
  3. console.log("hehe");
  4. };
复制代码

3个定时器

源码中设置了3个定时器,分别是moveloop、gameloop和sendUpdates。其中moveloop每秒执行60次,它主要处理小球的移动计算。

  1. setInterval(moveloop, 1000 / 60);
  2. setInterval(gameloop, 1000);
  3. setInterval(sendUpdates, 1000 / c.networkUpdateFactor);
复制代码

moveloop

Moveloop方法的代码如下,它对每个玩家执行tickPlayer,处理每个小球的移动。

  1. function moveloop() {
  2.     for (var i = 0; i < users.length; i++) {
  3.         tickPlayer(users[i]);
  4.     }
  5.     for (i=0; i < massFood.length; i++) {
  6.         if(massFood[i].speed > 0) moveMass(massFood[i]);
  7.     }
  8. }
复制代码

moveMass

moveMass就是移动质量,小球可以喷射身体的一部分出去,喷出去的部分变成了食物。每个喷射出去的“质量”都会被保存在massFood列表里。moveMass会处理这些“质量”的运动轨迹,先是喷射出去、然后减速、停止。


moveMass的代码比较简单,先看看这一部分。Mass的运动分是个减速过程,喷射时Mass带有较高的初速度,然后每一帧减速0.5,直到停止。代码后半部分还对Mass撞上边缘的情况做处理。

  1. function moveMass(mass) {
  2.     var deg = Math.atan2(mass.target.y, mass.target.x);
  3.     var deltaY = mass.speed * Math.sin(deg);
  4.     var deltaX = mass.speed * Math.cos(deg);

  5.     mass.speed -= 0.5;
  6.     if(mass.speed < 0) {
  7.         mass.speed = 0;
  8.     }
  9.     if (!isNaN(deltaY)) {
  10.         mass.y += deltaY;
  11.     }
  12.     if (!isNaN(deltaX)) {
  13.         mass.x += deltaX;
  14.     }

  15.     var borderCalc = mass.radius + 5;

  16.     if (mass.x > c.gameWidth - borderCalc) {
  17.         mass.x = c.gameWidth - borderCalc;
  18.     }
  19.     if (mass.y > c.gameHeight - borderCalc) {
  20.         mass.y = c.gameHeight - borderCalc;
  21.     }
  22.     if (mass.x < borderCalc) {
  23.         mass.x = borderCalc;
  24.     }
  25.     if (mass.y < borderCalc) {
  26.         mass.y = borderCalc;
  27.     }
  28. }
复制代码

tickPlayer

tickPlayer是处理玩家移动的核心部分,它处理了小球的移动、分身、吞食,代码也比较长,整体代码结构如下所示,首先做心跳判断,如果客户端太久没有发送心跳协议,那么向客户端发送kick协议,断开连接。其后调用movePlayer,处理玩家移动。


分身的过程

movePlayer计算了小球各个分身的移动过程,在游戏中,玩家可以让小球分身,具体过程如下。

1、玩家点击按钮后,大球分身,分出来的小球沿着移动方向弹射


2、弹射过后,分身们逐步靠近,最终连在一起


3、经过一段时间,分身合并,又成为一个大球


分身的过程分为弹射、靠近、紧靠、合并四个阶段。

怎样表示小球

既然小球有可能分身,那么它便不能单一的由x,y坐标和质量(半径)表示。该项目采用这样的表示方法(伪代码)。如果小球没有分身,那么cells数组长度为1,表示只有1个“细胞”,如果分裂成多个,则cells数组长度增加。player的x和y表示所有cell的中心坐标,由程序计算得出,target是目的地的坐标,即玩家鼠标指向的位置。每个cell都有质量,而cell的半径由质量换算而来,计算公式为radius=4+Math.sqrt(mass)*6。

  1. class player
  2. {
  3. var x;  //中心点坐标x
  4. var y;  //中心点坐标y
  5. var  target.x  //目的地坐标
  6. var  target.y
  7. var[]  cells//分身数组


  8. }

  9. class cells
  10. {
  11. var x;
  12. var y;
  13. var mass; //质量
  14. var radius //半径,由mass换算而来
  15. }
复制代码

计算每个cell的目的地

由于每个cell都是独立运动的,每一个cell都有自己的移动目的地,计算每个cell目的地的代码如下。

  1. function movePlayer(player) {
  2.     var x =0,y =0;
  3.     for(var i=0; i<player.cells.length; i++)
  4.     {
  5.         var target = {
  6.             x: player.x - player.cells[i].x + player.target.x,
  7.             y: player.y - player.cells[i].y + player.target.y
  8.         };
  9.         ……
  10.      }
  11.      ……
复制代码
……

具体来说,每个cell的目的地就是总目的地的坐标加上它自己相对中心点的偏移,如下图所示。


计算移动速度

分身过程中,每个阶段都有不同的速度,一开始弹射的时候速度很快,然后匀加速减少,减到一定程度后速度沿着对数函数曲线减小。

  1. var dist = Math.sqrt(Math.pow(target.y, 2) + Math.pow(target.x, 2));
  2.         var deg = Math.atan2(target.y, target.x);
  3.         var slowDown = 1;
  4.         if(player.cells[i].speed <= 6.25) {
  5.             slowDown = util.log(player.cells[i].mass, c.slowBase) - initMassLog + 1;
  6.         }
  7.         ……
  8.         if(player.cells[i].speed > 6.25) {
  9.             player.cells[i].speed -= 0.5;
  10.         }
  11.         ……
  12.         var deltaY = player.cells[i].speed * Math.sin(deg)/ slowDown;
  13.         var deltaX = player.cells[i].speed * Math.cos(deg)/ slowDown;
复制代码
……

如果接近目的地,cell距离目的地只有50+player.cells<i>.radius的距离。那么调整速度,越接近移动速度越慢。

  1. var dist=Math.sqrt(Math.pow(target.y,2)+Math.pow(target.x,2));

  2. ……

  3. if(dist<(50+player.cells<i>.radius)){

  4. deltaY*=dist/(50+player.cells<i>.radius);

  5. deltaX*=dist/(50+player.cells<i>.radius);

  6. }
复制代码

在计算完y方向的移动速度deltaY和x方向的移动速度deltaX后,给cell的位置赋值,完成移动操作。

  1. if (!isNaN(deltaY)) {
  2.             player.cells[i].y += deltaY;
  3.         }
  4.         if (!isNaN(deltaX)) {
  5.             player.cells[i].x += deltaX;
  6.         }
复制代码

合并处理

若分身经过一定时间(其实是以距离来判断),便需要合并,在下面的代码中,它会遍历其他所有的cell,这里分为两种情况。(1)if(distance<radiusTotal)(2)distance<radiusTotal/1.75
  1. for(var j=0; j<player.cells.length; j++) {
  2.             if(j != i && player.cells[i] !== undefined) {
  3.                 var distance = Math.sqrt(Math.pow(player.cells[j].y-player.cells[i].y,2) + Math.pow(player.cells[j].x-player.cells[i].x,2));
  4.                 var radiusTotal = (player.cells[i].radius + player.cells[j].radius);
  5.                 if(distance < radiusTotal) {
  6.                     ……(1)
  7.                     }
  8.                     else if(distance < radiusTotal / 1.75) {
  9.                         ……(2)
  10.                     }
  11.                 }
  12.             }
  13.         }
复制代码

如果distance<radiusTotal,也就是两个小球相互包含了,这里又分为两种情况(1)如果是还没到合并时间,那么让他们远离一点点。(2)如果到了合并时间,那就合并吧。

  1.   if(player.lastSplit > new Date().getTime() - 1000 * c.mergeTimer) {
  2.                         if(player.cells[i].x < player.cells[j].x) {
  3.                             player.cells[i].x--;
  4.                         } else if(player.cells[i].x > player.cells[j].x) {
  5.                             player.cells[i].x++;
  6.                         }
  7.                         if(player.cells[i].y < player.cells[j].y) {
  8.                             player.cells[i].y--;
  9.                         } else if((player.cells[i].y > player.cells[j].y)) {
  10.                             player.cells[i].y++;
  11.                         }
  12.                     }
复制代码

如果到了合并时间,且distance<radiusTotal/1.75(这个条件一定会满足,因为前面有distance<radiusTotal的判断),那么就合并。合并部分的代码如下,其中cells表示删除某个cell,合并了,那就两个删成一个。

  1. player.cells<i>.mass+=player.cells[j].mass;

  2. player.cells<i>.radius=util.massToRadius(player.cells<i>.mass);

  3. player.cells.splice(j,1);
复制代码

边界处理

moveplayer后面的代码便是做边界处理,保证cell在边界之内,代码如下所示。

  1. if(player.cells.length > i) {
  2.             var borderCalc = player.cells[i].radius / 3;
  3.             if (player.cells[i].x > c.gameWidth - borderCalc) {
  4.                 player.cells[i].x = c.gameWidth - borderCalc;
  5.             }
  6.             if (player.cells[i].y > c.gameHeight - borderCalc) {
  7.                 player.cells[i].y = c.gameHeight - borderCalc;
  8.             }
  9.             if (player.cells[i].x < borderCalc) {
  10.                 player.cells[i].x = borderCalc;
  11.             }
  12.             if (player.cells[i].y < borderCalc) {
  13.                 player.cells[i].y = borderCalc;
  14.             }
  15.             x += player.cells[i].x;  
  16.             y += player.cells[i].y;
  17.    }
复制代码

重新计算中心点

player.x和player.y是所有cell的中心点,重新计算。

  1. player.x = x/player.cells.length;
  2. player.y = y/player.cells.length;
复制代码

经过上述步骤,实现了moveplayer的功能。由于判断是否被其他cell包含的时候,采用了两次for,效率上是否还有优化的空间呢?下一节再看看吞食食物、玩家相互吞食的处理方式。

还是放个广告吧,笔者出版的一本书《Unity3D网络游戏实战》充分的讲解怎样开发一款网络游戏,特别对网络框架设计、网络协议、数据处理等方面都有详细的描述,相信会是一本好书的。



专栏地址:https://zhuanlan.zhihu.com/p/27313638

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多