《球球大作戰》原始碼解析(6):碰撞處理

遊資網發表於2019-03-20
《球球大作戰》原始碼解析(6):碰撞處理

系列文章
《球球大作戰》原始碼解析——(1)執行起來
《球球大作戰》原始碼解析:伺服器與客戶端架構
《球球大作戰》原始碼解析:移動演算法
《球球大作戰》原始碼解析(6):碰撞處理

《球球大作戰》原始碼解析(7):遊戲迴圈
《球球大作戰》原始碼解析(8):訊息廣播

小球移動過程中,可能會碰到食物、其他玩家和病毒,如果碰到食物,則吞食食物,質量增加;如果碰到其他玩家,體積大的吃掉體積小的,如果吞食病毒,分身解體。tickPlayer中有一段遍歷所有cell的程式碼,它處理了遊戲中的碰撞事件。

  1. for(var z=0; z<currentPlayer.cells.length; z++) {

  2.     ……

  3. }
複製程式碼

程式碼中定義了一個SAT.Circle型別的playerCircle,它指的是以currentCell.x和currentCell.y為圓心,currentCell.radius為半徑的圓。後續將會用這個圓形去和場景中的物體做碰撞檢測。

  1. var V = SAT.Vector; //一開始定義
  2. var C = SAT.Circle;


  3. var playerCircle = new C(
  4.             new V(currentCell.x, currentCell.y),
  5.             currentCell.radius
  6.         );
複製程式碼

吞食食物

吞食食物的程式碼如下所示,foodEaten表示被吃掉的食物列表,程式對food列表的所有食物執行funcFood方法,即是使用 SAT.pointInCircle看看食物是不是被包含在玩家的面積之內。然後再對每個foodEaten執行deleteFood方法,即刪除掉這個食物。food.map(funcFood)表示對food陣列的每個元素傳遞給指定的函式,並返回一個陣列,該陣列由函式的返回值構成。funcFood返回的是玩家是否吞食了食物,形成true/false的列表。reduce() 方法接收一個函式作為累加器,陣列中的每個值(從左到右)開始縮減,最終為一個值,是ES5中新增的一個陣列逐項處理方法。針對map(funcFood)返回的true/false列表,如果該食物被包含(為true),則將它新增到返回值中。

  1. var foodEaten = food.map(funcFood)

  2.             .reduce( function(a, b, c) { return b ? a.concat(c) : a; }, []);



  3.         foodEaten.forEach(deleteFood);





  4.     function funcFood(f) {

  5.         return SAT.pointInCircle(new V(f.x, f.y), playerCircle);

  6.     }



  7.     function deleteFood(f) {

  8.         food[f] = {};

  9.         food.splice(f, 1);

  10.     }
複製程式碼

看到這裡作者還是比較失望的,因為本來期待有更好的方法,減少計算量。像這樣兩兩判斷誰不會啊!

吞食massFood

massFood是玩家噴射出的“質量”處理過程與吞食食物類似,獲取被吃掉的mass的列表massEaten,然後從massFood列表中刪掉它。

  1. var massEaten = massFood.map(eatMass)
  2.             .reduce(function(a, b, c) {return b ? a.concat(c) : a; }, []);

  3.      ……

  4.         var masaGanada = 0;
  5.         for(var m=0; m<massEaten.length; m++) {
  6.             masaGanada += massFood[massEaten[m]].masa;
  7.             massFood[massEaten[m]] = {};
  8.             massFood.splice(massEaten[m],1);
  9.             for(var n=0; n<massEaten.length; n++) {
  10.                 if(massEaten[m] < massEaten[n]) {
  11.                     massEaten[n]--;
  12.                 }
  13.             }
  14.         }
複製程式碼

吞食病毒

如果不小心吞食了病毒,玩家會被迫分身,程式碼如下所示。


  1.         var virusCollision = virus.map(funcFood)

  2.            .reduce( function(a, b, c) { return b ? a.concat(c) : a; }, []);



  3.         if(virusCollision > 0 && currentCell.mass > virus[virusCollision].mass) {

  4.           sockets[currentPlayer.id].emit('virusSplit', z);

  5.         }
複製程式碼

下圖為吞食病毒導致的分身前後,綠色圓形為病毒,大球aa吞食病毒後,立即分解為兩個小球。

《球球大作戰》原始碼解析(6):碰撞處理

《球球大作戰》原始碼解析(6):碰撞處理

增加質量

如果玩家吞食了食物或massfood,小球會變大,相關程式碼如下。

  1. if(typeof(currentCell.speed) == "undefined")

  2.             currentCell.speed = 6.25;

  3.         masaGanada += (foodEaten.length * c.foodMass);

  4.         currentCell.mass += masaGanada;

  5.         currentPlayer.massTotal += masaGanada;

  6.         currentCell.radius = util.massToRadius(currentCell.mass);

  7.         playerCircle.r = currentCell.radius;
複製程式碼

吞食其他玩家

接下來是使用四叉樹計算玩家之間的碰撞,筆者就在想,前面都用了那麼多個for迴圈了,這可是每個玩家都對food,massfood,病毒都for一次啊。這裡用四叉樹意義很大麼?為什麼不一開始就都用呢?

先使用tree.put構建四叉樹,四叉樹可以把判斷的範圍變小,把每個玩家都放進去,然後通過tree.get(currentPlayer, check)獲取發生碰撞的玩家。最後再對每個可能發生碰撞的玩家執行collisionCheck。


  1.         tree.clear();

  2.         users.forEach(tree.put);

  3.         var playerCollisions = [];



  4.         var otherUsers =  tree.get(currentPlayer, check);



  5.         playerCollisions.forEach(collisionCheck);
複製程式碼

接下來看看check,它遍歷玩家身上每個cells,然後使用SAT.testCircleCircle測試是否圓在圓內,如果是的話返回一個response結構,該結構裡面包含對方玩家的id、name、座標等資訊。然後構建playerCollisions陣列。

  1. function check(user) {

  2.         for(var i=0; i<user.cells.length; i++) {

  3.             if(user.cells[i].mass > 10 && user.id !== currentPlayer.id) {

  4.                 var response = new SAT.Response();

  5.                 var collided = SAT.testCircleCircle(playerCircle,

  6.                     new C(new V(user.cells[i].x, user.cells[i].y), user.cells[i].radius),

  7.                     response);

  8.                 if (collided) {

  9.                     response.aUser = currentCell;

  10.                     response.bUser = {

  11.                         id: user.id,

  12.                         name: user.name,

  13.                         x: user.cells[i].x,

  14.                         y: user.cells[i].y,

  15.                         num: i,

  16.                         mass: user.cells[i].mass

  17.                     };

  18.                     playerCollisions.push(response);

  19.                 }

  20.             }

  21.         }

  22.         return true;

  23.     }
複製程式碼

然後是對發生碰撞的玩家執行邏輯,把它吃掉。

  1. function collisionCheck(collision) {

  2.         if (collision.aUser.mass > collision.bUser.mass * 1.1  && collision.aUser.radius > Math.sqrt(Math.pow(collision.aUser.x - collision.bUser.x, 2) + Math.pow(collision.aUser.y - collision.bUser.y, 2))*1.75) {

  3.             console.log('[DEBUG] Killing user: ' + collision.bUser.id);

  4.             console.log('[DEBUG] Collision info:');

  5.             console.log(collision);



  6.             var numUser = util.findIndex(users, collision.bUser.id);

  7.             if (numUser > -1) {

  8.                 if(users[numUser].cells.length > 1) {

  9.                     users[numUser].massTotal -= collision.bUser.mass;

  10.                     users[numUser].cells.splice(collision.bUser.num, 1);

  11.                 } else {

  12.                     users.splice(numUser, 1);

  13.                     io.emit('playerDied', { name: collision.bUser.name });

  14.                     sockets[collision.bUser.id].emit('RIP');

  15.                 }

  16.             }

  17.             currentPlayer.massTotal += collision.bUser.mass;

  18.             collision.aUser.mass += collision.bUser.mass;

  19.         }

  20.     }
複製程式碼

這裡是筆者看不懂還是四叉樹沒啥作用呢?在這裡用四叉樹和直接兩次迴圈有區別麼?check是固定返回true的啊!!!!!下面的四叉樹說明,可以證明這裡用四叉樹是無效的。


四叉樹

四叉樹空間索引原理及其實現 - 心如止水-GISer的成長之路 - CSDN部落格

四叉樹索引的基本思想是將地理空間遞迴劃分為不同層次的樹結構。它將已知範圍的空間等分成四個相等的子空間,如此遞迴下去,直至樹的層次達到一定深度或者滿足某種要求後停止分割。四叉樹的結構比較簡單,並且當空間資料物件分佈比較均勻時,具有比較高的空間資料插入和查詢效率,因此四叉樹是GIS中常用的空間索引之一。常規四叉樹的結構如圖所示,地理空間物件都儲存在葉子節點上,中間節點以及根節點不儲存地理空間物件。

《球球大作戰》原始碼解析(6):碰撞處理

四叉樹對於區域查詢,效率比較高。但如果空間物件分佈不均勻,隨著地理空間物件的不斷插入,四叉樹的層次會不斷地加深,將形成一棵嚴重不平衡的四叉樹,那麼每次查詢的深度將大大的增多,從而導致查詢效率的急劇下降。

nodejs的 simple-quadtree介紹

程式碼中的tree.get、tree.put等方法用到了nodejs的simple-quadtree庫,這裡做個簡單介紹。

simple-quadtree

simple-quadtree是一套小型的四叉樹實現,每棵樹支援 put、 get、remove 和 clear四種操作。四叉樹的節點物件必須包含x,y座標,以及長度寬度w、h。

Put方法

Put方法可以將節點放入四叉樹裡面,例如:

  1. qt.put({x: 5, y: 5, w: 0, h: 0, string: 'test'});
複製程式碼

Get方法

Get方法會迭代取出四叉樹節點,然後呼叫回撥函式,如下所示。

  1. qt.get({x:0, y: 0, w: 10, h: 10}, function(obj) {



  2. // obj == {x: 5, y: 5, w: 0, h: 0, string: 'test'}



  3. });

複製程式碼

如果回撥函式返回true,迭代會一直進行下去,如果回撥函式返回false,則迭代停止。由於原始碼中的check方法總是返回true,所以這裡使用四叉樹並沒能減少計算量,相反比for迴圈多了構建樹的計算。沒什麼用!

還是放個廣告吧,筆者出版的一本書《Unity3D網路遊戲實戰》充分的講解怎樣開發一款網路遊戲,特別對網路框架設計、網路協議、資料處理等方面都有詳細的描述,相信會是一本好書的。

《球球大作戰》原始碼解析(6):碰撞處理

作者:羅培羽
原地址:https://zhuanlan.zhihu.com/p/28107508





相關文章