碰撞檢測的向量實現

吳冠禧javascript

注:一、本文只討論2d圖形碰撞檢測。二、本文討論圓形與圓形,矩形與矩形、圓形與矩形碰撞檢測的向量實現html

前言

2D遊戲中,一般使用矩形、圓形等來代替複雜圖形的相交檢測。由於這兩種形狀的碰撞檢測速度是最快的。其中矩形包圍盒又能夠分爲軸對齊包圍盒(AABB, Axis Aligned Bounding Box)與轉向包圍盒(OBB, Oriented Bounding Box)。AABB與OBB的區別在於,AABB中的矩形的其中一條邊和座標軸平行,OBB的計算複雜度要高於AABB。根據不一樣的使用場景,能夠用不一樣的方案。java

rect_circle

如上圖,明顯皮卡超適合用包圍盒,精靈球適合用包圍球。git

向量

向量做爲一種數學工具,在碰撞檢測中發揮很大做用,後面的計算都是經過向量來完成,因此先來複習一下向量。github

向量的代數表示

向量的代數表示指在指定了一個座標系以後,用一個向量在該座標系下的座標來表示該向量,兼具了符號的抽象性和幾何形象性,於是具備最高的實用性,被普遍採用於須要定量分析的情形。 對於自由向量,將向量的起點平移到座標原點後,向量就能夠用一個座標系下的一個點來表示,該點的座標值即向量的終點座標。數組

// 二維平面向量
class Vector2d{
  constructor(vx=1,vy=1){
    this.vx = vx;
    this.vy = vy;
  }
}
const vecA = new Vector2d(1,2);
const vecB = new Vector2d(3,1);
複製代碼

act1

向量運算

加法:向量的加法知足平行四邊形法則和三角形法則。具體的,兩向量相加仍是一個向量,分別是x與y兩個份量的相加。工具

act2

// 向量的加法運算
static add(vec,vec2){
  const vx = vec.vx + vec2.vx;
  const vy = vec.vy + vec2.vy;
  return new Vector2d(vx,vy);
}
複製代碼

減法:兩個向量a和b的相減獲得的向量能夠表示爲a和b的起點重合後,從b的終點指向a的終點的向量:測試

act3

// 向量的減法運算
static sub(vec,vec2){
  const vx = vec.vx - vec2.vx;
  const vy = vec.vy - vec2.vy;
  return new Vector2d(vx,vy);
}
複製代碼

大小:向量的大小,是其各個份量的平方和開方。ui

// 獲取向量長度
length(){
  return Math.sqrt(this.vx * this.vx + this.vy * this.vy);
}
複製代碼

點積:從代數角度看,先對兩個數字序列中的每組對應元素求積,再對全部積求和,結果即爲點積。this

// 向量的數量積
static dot(vec,vec2){
  return vec.vx * vec2.vx + vec.vy * vec2.vy;
}
複製代碼

旋轉:向量的旋轉能夠用旋轉矩陣求解

act4

act5

act6

//向量的旋轉 
static rotate(vec,angle){
  const cosVal = Math.cos(angle);
  const sinVal = Math.sin(angle);
  const vx = vec.vx * cosVal - vec.vy * sinVal;
  const vy = vec.vx * sinVal + vec.vy * cosVal;
  return new Vector2d(vx,vy);
}
複製代碼

圓形比較簡單,只要確認圓心x,y和半徑r就好了,而後推導出圓心向量。

class Circle{
  // x,y是圓的圓心 r是半徑
  constructor(x=0,y=0,r=1){
    this.x = x;
    this.y = y;
    this.r = r;
  }
  get P(){ return new Vector2d(this.x,this.y) } // 圓心向量
}
複製代碼

矩形

矩形就較爲複雜,定義一個矩形須要中心座標的x,y、兩邊長w和h,還有根據中心的旋轉角度rotation

export class Rect{
  // x,y是矩形中心的座標 w是寬 h是高 rotation是角度單位deg
  constructor(x=0,y=0,w=1,h=1,rotation=0){
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
}
複製代碼

兩圓相交

cb1

兩圓相交比較簡單,只需判斷兩圓心之間的距離小於兩圓的半徑之和。

兩圓心距離能夠用圓心向量相減,而後求相減向量的長度。

act7

circleCircleIntersect(circle1,circle2){
  const P1 = circle1.P;
  const P2 = circle2.P;
  const r1 = circle1.r;
  const r2 = circle2.r;
  const u = Vector2d.sub(P1,P2);
  return u.length() <= r1  + r2 ;
}
複製代碼

圓和矩形相交

涉及到矩形的相交問題都先要判斷是否軸對稱。

矩形軸對稱

cb2

先看軸對稱的狀況,下面是來自知乎問題怎樣判斷平面上一個矩形和一個圓形是否有重疊?「Milo Yip」的回答搬運:

設c爲矩形中心,h爲矩形半長,p爲圓心,r爲半徑。

act8

方法是計算圓心與矩形的最短距離 u,若 u 的長度小於 r 則二者相交。

  1. 首先利用絕對值把 p - c 轉移到第一象限,下圖顯示不一樣象限的圓心也能映射至第一象限,這不影響相交測試的結果:

act9

  1. 而後,把 v 減去 h,負數的份量設置爲0,就獲得圓心與矩形最短距離的矢量 u。下圖展現了4種狀況,紅色的u是結果。

act10

  1. 最後要比較 u 和 r 的長度,若距離少於 r,則二者相交。能夠只求 u 的長度平方是否小於 r 的平方。

下面我用js實現一下:

其中矩形的四個頂點命名爲A1,A2,A3,A4,矩形在第一象限的半長h等於CA3

class Rect{
  // x,y是矩形中心的座標 w是寬 h是高 rotation是角度單位deg
  constructor(x=0,y=0,w=1,h=1,rotation=0){
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
  get C(){ return new Vector2d(this.x,this.y); } // 矩形中心向量
  get A3(){ return new Vector2d(this.x+this.w/2,this.y+this.h/2); } // 頂點A3向量
}

rectCircleIntersect(rect,circle){
  const C = rect.C;
  const r = circle.r;
  const A3 = rect.A3;
  const P = circle.P;
  const h = Vector2d.sub(A3,C); // 矩形半長
  const v = new Vector2d(Math.abs(P.vx - C.vx),Math.abs(P.vy - C.vy));
  const u = new Vector2d(Math.max(v.vx - h.vx,0),Math.max(v.vy - h.vy,0));
  return u.lengthSquared() <= r * r;
}
複製代碼

矩形非軸對稱

cb3

這個問題其實也很好解決,將矩形中心視爲旋轉中心,將矩形和圓形一塊兒反向旋轉將矩形轉爲軸對稱,而後就能夠套用上面的解法。

act11

矩形中心到圓心向量爲是CP

反向旋轉θ度得向量CP'

而後根據向量得三角形定律得OP' = OC + CP'

後面就代入矩形是軸對稱的公式進行計算

class Rect{
  // x,y是矩形中心的座標 w是寬 h是高 rotation是角度單位deg
  constructor(x=0,y=0,w=1,h=1,rotation=0){
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
  get C(){ return new Vector2d(this.x,this.y); } // 矩形中心向量
  get A3(){ return new Vector2d(this.x+this.w/2,this.y+this.h/2); } // 頂點A3向量
  get _rotation(){ return this.rotation / 180 * Math.PI; }  // 角度單位轉換
}

p(rect,circle){
  const rotation = rect.rotation;
  const C = rect.C;
  let P;
  if (rotation % 360 === 0) {
    P = circle.P; // 軸對稱直接輸出P
  } else {
    P = Vector2d.add(C,Vector2d.rotate(Vector2d.sub(circle.P,C),rect._rotation*-1)); // 非軸對稱,計算P‘
  }
  return P;
}

rectCircleIntersect(rect,circle){
  const rotation = rect.rotation;
  const C = rect.C;
  const r = circle.r;
  const A3 = rect.A3;
  const P = p(rect,circle);
  const h = Vector2d.sub(A3,C);
  const v = new Vector2d(Math.abs(P.vx - C.vx),Math.abs(P.vy - C.vy));
  const u = new Vector2d(Math.max(v.vx - h.vx,0),Math.max(v.vy - h.vy,0));
  return u.lengthSquared() <= r * r;
}
複製代碼

查看Demo1 rococolate.github.io/blog/gom/te…

demo1

兩矩形相交

兩矩形都軸對稱AABB

cb4

想象一下兩個矩形A和B,B貼着A的邊走了一圈,B的矩形中心的軌跡是一個新的矩形,這樣就簡化成新矩形與B中心點這一點的相交問題,又由於點能夠當作是半徑爲0的圓,因此問題又轉換爲圓形和矩形相交。

act12

class Rect{
  // x,y是矩形中心的座標 w是寬 h是高 rotation是角度單位deg
  constructor(x=0,y=0,w=1,h=1,rotation=0){
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
  get C(){ return new Vector2d(this.x,this.y); } // 矩形中心向量
  get A3(){ return new Vector2d(this.x+this.w/2,this.y+this.h/2); } // 頂點A3向量
  get _rotation(){ return this.rotation / 180 * Math.PI; }  // 角度單位轉換
}

AABBrectRectIntersect(rect1,rect2){
  const P = rect2.C;
  const w2 = rect2.w; 
  const h2 = rect2.h; 
  const {w,h,x,y} = rect1;
  const C = rect1.C;
  const A3 = new Vector2d(x+w/2+w2/2,y+h/2+h2/2); // 新矩形的半長
  const H = Vector2d.sub(A3,C);
  const v = new Vector2d(Math.abs(P.vx - C.vx),Math.abs(P.vy - C.vy));
  const u = new Vector2d(Math.max(v.vx - H.vx,0),Math.max(v.vy - H.vy,0));
  return u.lengthSquared() === 0; // 點能夠當作是半徑爲0的圓
} 
複製代碼

兩矩形相交非軸對稱OBB

cb5

兩個矩形的OBB檢測使用分離軸定理(Separating Axis Theorem)

分離軸定理:經過判斷任意兩個矩形 在任意角度下的投影是否均存在重疊,來判斷是否發生碰撞。若在某一角度光源下,兩物體的投影存在間隙,則爲不碰撞,不然爲發生碰撞。

由於矩形的對邊平行,因此只要判斷四條對稱軸上的投影便可。

act13

如何投影?這裏補充一下向量點積的幾何意義。

act15

在歐幾里得空間中,點積能夠直觀地定義爲 A·B = |A||B|cosθ ,其中|A|cosθ是A到B的投影,若是B是單位向量,那麼A·B就是A到單位向量B的投影

回到矩形,將矩形4個頂點都投影到對稱軸上,咱們分別將其點乘便可。

act14

class Rect{
  // x,y是矩形中心的座標 w是寬 h是高 rotation是角度單位deg
  constructor(x=0,y=0,w=1,h=1,rotation=0){
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
  get C(){ return new Vector2d(this.x,this.y); }
  get _A1(){ return new Vector2d(this.x-this.w/2,this.y-this.h/2); }  // 4角頂點
  get _A2(){ return new Vector2d(this.x+this.w/2,this.y-this.h/2); }
  get _A3(){ return new Vector2d(this.x+this.w/2,this.y+this.h/2); }
  get _A4(){ return new Vector2d(this.x-this.w/2,this.y+this.h/2); }
  get _axisX(){ return new Vector2d(1,0); } // 未旋轉時的對稱軸X
  get _axisY(){ return new Vector2d(0,1); } // 未旋轉時的對稱軸Y
  get _CA1(){ return Vector2d.sub(this._A1,this.C); }
  get _CA2(){ return Vector2d.sub(this._A2,this.C); }
  get _CA3(){ return Vector2d.sub(this._A3,this.C); }
  get _CA4(){ return Vector2d.sub(this._A4,this.C); }
  get _rotation(){ return this.rotation / 180 * Math.PI; }
  get A1(){ return this.rotation % 360 === 0 ?  this._A1 :  Vector2d.add(this.C,Vector2d.rotate(this._CA1,this._rotation)); } // 計算上旋轉後4角頂點
  get A2(){ return this.rotation % 360 === 0 ?  this._A2 :  Vector2d.add(this.C,Vector2d.rotate(this._CA2,this._rotation)); }
  get A3(){ return this.rotation % 360 === 0 ?  this._A3 :  Vector2d.add(this.C,Vector2d.rotate(this._CA3,this._rotation)); }
  get A4(){ return this.rotation % 360 === 0 ?  this._A4 :  Vector2d.add(this.C,Vector2d.rotate(this._CA4,this._rotation)); }
  get axisX(){ return this.rotation % 360 === 0 ?  this._axisX :  Vector2d.rotate(this._axisX,this._rotation); } // 計算上旋轉後的對稱軸X
  get axisY(){ return this.rotation % 360 === 0 ?  this._axisY :  Vector2d.rotate(this._axisY,this._rotation); } // 計算上旋轉後的對稱軸Y
  get _vertexs(){ return [this._A1,this._A2,this._A3,this._A4]; } 
  get vertexs(){ return [this.A1,this.A2,this.A3,this.A4]; } // 4角頂點數組
}

OBBrectRectIntersect(rect1,rect2){
  const rect1AxisX = rect1.axisX;
  const rect1AxisY = rect1.axisY;
  const rect2AxisX = rect2.axisX;
  const rect2AxisY = rect2.axisY;
  if (!cross(rect1,rect2,rect1AxisX)) return false;  // 一旦有不相交的軸就能夠return false
  if (!cross(rect1,rect2,rect1AxisY)) return false;
  if (!cross(rect1,rect2,rect2AxisX)) return false;
  if (!cross(rect1,rect2,rect2AxisY)) return false;
  return true;  // 4軸投影都相交 return true
}
cross(rect1,rect2,axis){
  const vertexs1ScalarProjection = rect1.vertexs.map(vex => Vector2d.dot(vex,axis)).sort((a,b)=>a-b); // 矩形1的4個頂點投影並排序
  const vertexs2ScalarProjection = rect2.vertexs.map(vex => Vector2d.dot(vex,axis)).sort((a,b)=>a-b); // 矩形2的4個頂點投影並排序
  const rect1Min = vertexs1ScalarProjection[0]; // 矩形1最小長度
  const rect1Max = vertexs1ScalarProjection[vertexs1ScalarProjection.length - 1]; // 矩形1最大長度
  const rect2Min = vertexs2ScalarProjection[0]; // 矩形2最小長度
  const rect2Max = vertexs2ScalarProjection[vertexs1ScalarProjection.length - 1]; // 矩形2最大長度
  return rect1Max >= rect2Min && rect2Max >= rect1Min;  // 相交判斷 
}
複製代碼

最後放上一個相交的應用Demo rococolate.github.io/blog/gom/te…,Demo裏的形狀均可以拖拽,當碰到其餘形狀時會變透明。

demo2

參考文章

第十五章:碰撞檢測

方塊的戰爭:淺談格鬥遊戲的精髓

怎樣判斷平面上一個矩形和一個圓形是否有重疊?

「等一下,我碰!」——常見的2D碰撞檢測

碼農乾貨系列【1】--方向包圍盒(OBB)碰撞檢測

Rotation matrix

數量積

向量

相關文章
相關標籤/搜索