距離上次發布已經有了很長一段時間,期間由於各種原因沒有更新這方面的技術分享,在這里深表遺憾。在MMO或其他的游戲中,會有針對各種形狀的計算,通常在攻擊區域里不會很復雜,常見的為矩形、圓形、扇形。今天分享的是判斷一個目標點是否在扇形內的計算,用到的是比較簡單的運算,高效率的算法我很盡快更新。
數學知識
在這里需要大家回顧一下初中以及高中的代數和平面幾何的知識,想必大部分的朋友在這方面和我一樣幾乎忘記了或是對這些數學知識感覺有些頭痛。不過大家沒有必要擔心,在實際運用中,我們都不會涉及太復雜的計算,因為我們不需要追求的十分精確。
但是在以下內容中,大家需要知道一些基本的定理和公式:勾股定理、余弦定理。
需要了解弧度和角度的一些轉換:角度 = 弧度 * 180.0 / ∏
一些數學函數:atan2、acos等。
中心線:以中心線順、逆時針展開半角,形成一個完整的目標扇形區域。
極坐標概念:
如上圖坐標點A的平面坐標為(7.96, 5.43),對應的極坐標為(ρ, θ)
相對坐標的概念:
在扇形的計算中,我們的X軸與Y軸的方向與原點的的X軸和Y軸平行,在上圖中,如果以A點作為中心點,那么B的相對坐標為(xb - xa, yb - ya)。
扇形與點的關系
示例圖1:
必要的數據: 原點A(攻擊者坐標)、方向點B(direction)、目標點C(point)、扇形角度(β)、扇形的半徑(r)。
扇形的有效區域:
如上圖中的扇形的角度為180°,其有效面積為:以X軸為中心分布的可攻擊區域1(area1紫色區域)和可攻擊區域2(area2綠色區域)。
如果目標點的極坐標為(ρ,Θ),那么上述的目標點判斷為:目標點到原點A的距離小於扇形的半徑(ρ <= r)、在區域1(0 <= Θ <= α + β / 2)或區域2((a - β / 2) + 360 <= Θ <= 360)。
因為我們的極坐標的范圍為(0-360),所以如果算出來的扇形最小的角度為負,需要轉換為正來比較。
從上述的判斷中,並沒有將所有的情況考慮到,如果需要的扇形角度超過180或者方向點落在不同的象限內,計算的方式是不同的,接下來再看一張示意圖。
示例圖2:
如上圖所示,這樣的面積計算就不能使用第一種方式,方向的極坐標的角度為315°,而扇形的角度的不過270°,那么α、β、γ到底變成了怎樣的關系,我們求出了方向的極坐標是否能把這樣兩個面積的角度分別計算出來?
區域1(0 <= Θ <= α + β / 2),如果是上圖的數據則為0 <= Θ <= 315 + 270 / 2,范圍就是[0,450],如果用詞判斷則象限2中的空白部分的本不符合的點就符合條件了,其實我們可以看出區域1的實際范圍為[0,90],同理區域2的范圍為[180,360]。而超過了360°的角度只要減去360,這里的450轉換為正常角度則為90剛剛是我們的正確角度。而第一種方式中的區域二的區域為[a - β / 2,360],在上圖中剛剛為[180,360]是符合的。
如何判斷一個扇形是否跨域了X軸正方向?
因為我們的扇形是以中心線分成兩個部分,那么只需要判斷這兩個部分是否超過了就可以,當前如果中心線在X軸正方向時絕對是跨域了兩個方向。
三種跨域的方式:示例圖1中(a - β / 2)的值小於零、示例圖2中的(α + β / 2)大於360、方向點在X軸正方向上(極坐標的角度Θ為0或絕對坐標X大於0和Y等於0)。
同時兩個扇形的角度需要轉換:小於0的角度需要轉換為正(示例圖1中a - β / 2,加上360)、大於360的角度需要轉換為360以內的角度(示例圖中的α + β / 2,減去360)。
那么兩個區域就為[0, a + β / 2]、[a - β / 2, 360],需要注意這里的角度都需要進行上述的轉換。
示例圖3:
如果扇形的范圍沒有跨域X軸正方向,那么角度的范圍是[a - β / 2, a + β / 2]。
代碼實現
我們知道了點和面的關系后,就能夠對目標點進行判定了,編碼實現才有了依據。(上面三種情況是否會有遺漏,暫時沒有考慮過,歡迎大家指正)
基礎結構定義:
struct point_struct { double x; double y; point_struct() : x{.0}, y{.0} {} }; using point_t = point_struct;
相對坐標轉換(沒有方向):
/** * 沒有方向的相對坐標轉換,x、y軸的方向與原點相同 * @param origin 坐標系原點 * @param point 需要轉換的點 */ /** * Y * | * | CY * | | * | | .point * | | * | | * | origin----------------- CX * | * | * O--------------------------------------------------- X * */ point_t absolute_to_relative(const point_t &origin, const point_t &point) { point_t result; result.x = point.x - origin.x; result.y = point.y - origin.y; return result; }
極坐標轉換:
/** * 簡單極坐標轉換(轉換后的x為斜邊,y為角度), * 減少數學函數調用(可以忽略,因為通常情況下這幾種情況命中概率極低) * @param point 需要轉換的點 */ point_t to_spolar_coordinate(const point_t &point) { point_t result; result.x = 0; result.y = -1; if (0 == point.x == point.y) { result.y = 0; return result; } if (0 == point.y) { result.x = abs(point.x); result.y = point.x > 0 ? 0 : 180; return result; } if (0 == point.x) { result.x = abs(point.y); result.y = point.y > 0 ? 90 : 270; return result; } if (abs(point.x) == abs(point.y)) { result.x = 1.41421 * abs(point.x); if (point.x > 0 && point.y > 0) { result.y = 45; } else if (point.x < 0 && point.y > 0) { result.y = 135; } else if (point.x < 0 && point.y < 0) { result.y = 225; } else if (point.x > 0 && point.y < 0) { result.y = 315; } } return result; }
/** * 轉換為極坐標(轉換后的x為斜邊,y為角度) * @param point 需要轉換的點 */ inline point_t to_polar_coordinate(const point_t &point) { point_t result; result.x = sqrt(point.x * point.x + point.y * point.y); result.y = (180.0 / PI) * atan2(point.y , point.x); //弧度轉角度 result.y = result.y < .0 ? result.y + 360.0 : result.y; return result; }
判斷是否在扇形內:
/** * 判斷一個點是否在扇形內(相對中心點) * @param center 扇形的中心點 * @param direction 中心線的方向坐標 * @param r 半徑 * @param angle 角度(0 < angle < 360) * @param point 需要檢查的點 */ bool in_circular_sector(const point_t ¢er, const point_t &direction, double r, int angle, const point_t &point) { //實際使用中,我們會把方向點的極坐標放到外部進行計算 point_t d_rpoint = absolute_to_relative(center, direction); //方向相對坐標 point_t d_pc_point = to_spolar_coordinate(d_rpoint); //方向極坐標 if (-1 == d_pc_point.y) { //簡單的如果轉換不出,則需要調用角度函數計算 d_pc_point = to_polar_coordinate(d_rpoint); } point_t rpoint = absolute_to_relative(center, point); //目標相對坐標 point_t pc_point = to_polar_coordinate(rpoint); //目標極坐標 if (pc_point.x > r) return false; bool result = false; auto half_angle = angle / 2; auto angle_counter = d_pc_point.y - half_angle; //中心線順時針方向的范圍 auto angle_clockwise = d_pc_point.y + half_angle; //中心線逆時針方向的范圍 /* std::cout << "angle_counter: " << angle_counter << " angle_clockwise: " << angle_clockwise << " d_pc_point.y" << d_pc_point.y << std::endl; */ if (0 == d_pc_point.y || angle_counter < 0 || angle_clockwise > 360) { angle_counter = angle_counter < 0 ? angle_counter + 360 : angle_counter; angle_clockwise = angle_clockwise > 360 ? angle_counter - 360 : angle_counter; if (pc_point.y >= 0 && pc_point.y <= angle_counter) { result = true; } else if (pc_point.y >= angle_clockwise && pc_point.y <= 360) { result = true; } } else { result = angle_counter <= pc_point.y && angle_clockwise >= pc_point.y; } return result; }
實際使用的優化:
可以先求出圓范圍內的所有玩家對象,這樣可以減少atan2調用,其次方向點的極坐標放到循環之外,減少循環次數。
PF人員招募
開篇語
我們沒有大神,只有解決問題的人。
我們沒有強悍的技術,只有一顆向往簡單的心。
我們沒有驚人的理論,只有一堆不可思議的妄想。
我們不需要復雜,只需要夠簡潔。
我們不需要固定的思維,只需要你能想得到。
PF托管地址
https://github.com/viticm/plainframework1
PF安裝教程
http://www.cnblogs.com/lianyue/p/3974342.html
PF交流QQ群
348477824(同時歡迎技術人員加入進行交流)