前几天,同事在报告中提及检测角色是否在扇形攻击范围的方法。我觉得该方法的性能不是太好,提出另一个颇为直接的方法。此问题在游戏中十分常见,只涉及简单的数学,却又可以看出实现者是否细心,所以我觉得可当作一道简单的面试题。问题在微博发表后得到不少回应,故撰文提供一些解答。
问题定义:
在二维中,检测点\mathbf{p}是否在扇形(circular sector)内,设扇形的顶点为\mathbf{c},半径为r,从\mathbf{\hat{u}}方向两边展开\theta角度。
当中 \mathbf{p},\mathbf{c},\mathbf{\hat{u}} 以直角坐标(cartesian coordinates)表示,r>0,0 < \theta < \pi。\mathbf{p}在扇形区边界上当作不相交。实现时所有数值采用单精度浮点数类型。
问题分析
许多相交问题都可以分拆为较小的问题。在此问题中,扇形可表示为圆形和角度区间的交集。
换句话说,问题等同于检测 \mathbf{p} 和 \mathbf{c} 的距离小于 r,及 \mathbf{p-c} 的方向在 \mathbf{\hat{u}} 两边 \theta 的角度范围内。
距离
\mathbf{p} 和 \mathbf{c} 的距离小于 r, 用数学方式表示:
|\mathbf{p} - \mathbf{c}| < r
极坐标
这是比较麻烦的部分,也是本题的核心。
有人想到,可以把 \mathbf{p-c} 和 \mathbf{\hat{u}} 从直角坐标转换成极坐标(polar coordinates)。数学上,\mathbf{p-c} 和 \mathbf{\hat{u}} 分别与 \mathbf{x} 轴的夹角可用atan2()函数求得:
\begin{align*}
\phi &= \mathrm{atan2}(p_y - c_y, p_x - c_x)\\
\alpha &= \mathrm{atan2}(u_y, u_x)
\end{align*}
然后,检查 \phi 是否在 (\alpha - \theta, \alpha + \theta) 区间内。但这要非常小心,因为 (\alpha - \theta, \alpha + \theta) 区间可能超越 (-\pi, \pi] 的范围,所以要检测:
\begin{align*}
\alpha_1 &< \phi - 2\pi < \alpha_2 \text{ ; or}\\
\alpha_1 &< \phi < \alpha_2 \text{ ; or}\\
\alpha_1 &< \phi + 2\pi < \alpha_2
\end{align*}
这个方法是可行的,不过即使假设 \mathbf{\hat{u}} 和 \theta 是常数,可预计算 \alpha_1 和 \alpha_2 ,我们还是避免不了要计算一个atan2()。
点积
点积(dot product)可计算两个矢量的夹角,这非常适合本题的扇形表示方式。我们要检测 \mathbf{p-c} 和 \mathbf{\hat{u}} 的夹角是否小于 \theta :
\cos^{-1}\left (\mathbf{\frac{p-c}{|p-c|}} \cdot \mathbf{\hat{u}} \right\) < \theta
相比极坐标的方法,点积算出来的夹角必然在 [0, \pi] 区间里,无需作特别处理就可以和 \theta 比较。
这是比较直观的角度比较方式。本文将以此方法为主轴。
编码与优化
若直接实现以距离和点积的检测,可以得到:
// Naive
bool IsPointInCircularSector(
float cx, float cy, float ux, float uy, float r, float theta,
float px, float py)
{
assert(cosTheta > -1 && cosTheta < 1);
assert(squaredR > 0.0f);
// D = P - C
float dx = px - cx;
float dy = py - cy;
// |D| = (dx^2 + dy^2)^0.5
float length = sqrt(dx * dx + dy * dy);
// |D| > r
if (length > r)
return false;
// Normalize D
dx /= length;
dy /= length;
// acos(D dot U) < theta
ret