3. 使用反正切来围绕某个位置转向

本文探讨了如何利用三角学中的正切函数及其反函数计算游戏角色朝向目标移动的角度,并介绍了在程序设计中如何正确使用反正切函数 atan2 来避免除零错误和其他常见问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

3.They’ve got atan, You Want atan2: using inverse tangent to turn towards a location

This post is about turning towards a point, or: more uses for trigonometry (sine, cosine, tangent, etc).

这篇帖子讨论如何定点转向:三角学的进一步应用(正弦、余弦、正切等等)。

A common feature in games is the capability to gradually move towards a given position. You may want some AI enemies that chase the player, or a character that moves towards the mouse cursor. Today we’re going to build a quick game with a cat that chases a laser pointer (which will be controlled by the mouse — the computer peripheral, that is!). How do you accomplish this? The two steps are firstly, calculating the angle at which to move (the subject of this post) and, secondly, moving progressively at that angle (covered in our previous post).

游戏中的一个常见特点是逐渐地朝着某个给定的点移动。你或许希望一些具有智能的敌人追逐玩家,或者某个角色朝着鼠标光标所在的位置移动。今天我们准备创建小猫追逐激光发射点的简单游戏(使用鼠标进行操作)。那么怎样完成呢?这包含两个步骤,首先是计算移动方向的角度,其次是朝着该角度逐步地移动。

So we want to calculate the direction needed to aim at a given point. A good place to start with any geometry problems like this is to draw a sketch. Here’s the problem:

因此我们希望针对一个给定点来计算所需的方向。开始着手处理类似这样的一些几何问题时可以考虑绘制一个草图。该问题如图:

 

As noted in our previous example, the X-Y axes form a right-angle, so we always have a right-angled triangle in these problems. Last time, we knew the angle and one side, and we wanted to find the other two sides, so we used sine and cosine. This time, we know two of the sides (the horizontal and vertical sides, which are the X and Y distances between us and our target), but we want to know the angle. To do this, we can use the tangent function.Wikipedia tells us:

正如我们前一节的例子所提到的,x轴和y轴形成一个直角,因此在这样的问题中我们始终可以得到一个直角三角形。上一次我们已知角度值和一条边的值,希望得到其他两边的值,于是我们使用sin和cos。这次我们已知两条边的值(水平边和垂直边的值,即鼠标光标处与目标距离的x轴和y轴的分量值),但是我们希望得到角度值。为了达到目的,我们可以使用tan函数,公式如下:

\tan \alpha = \displaystyle\frac{\textrm{opposite}}{\textrm{adjacent}}

Recall from last time that adjacent and opposite refer to the lengths of the sides that are adjacent and opposite to the angle of interest. So the adjacent is the horizontal distance, and the opposite is the vertical distance. In our example, we have:

回顾上次提到的内容,邻边和对边的值分别表示与斜角相邻以及相对的那条边的长度。于是邻边代表水平距离,而对边代表垂直距离。在我们的例子中,有如下公式:

\tan(\textrm{angle}) = \displaystyle\frac{\textrm{pointerY} - \textrm{catY}}{\textrm{pointerX} - \textrm{catX}}

We need to do something to both sides of the equation (standard mathematical operating procedure!) to help us work out the angle: we need to take the inverse tangent of both sides. This is called inverse tangent, or arctangent, and we’ll write it as arctan:

我们需要在公式两边做一些调整(标准的数学操作过程)以便于求出角度值:我们在两边对正切进行反向操作。这被称作反正切,我们将使用arctan书写公式:

\arctan(\tan(\textrm{angle})) = \arctan\left(\displaystyle\frac{\textrm{pointerY} - \textrm{catY}}{\textrm{pointerX} - \textrm{catX}}\right)

The left-hand side simplifies, because taking the arctangent of a tangent cancels out (by definition). So we have:

公式左边被简化了,因为执行arctan抵消了tan(由定义可知)。于是得到公式:

\textrm{angle} = \arctan\left(\displaystyle\frac{\textrm{pointerY} - \textrm{catY}}{\textrm{pointerX} - \textrm{catX}}\right)

So that tells us how to get the angle. But there’s a complication once we come to program this. You might be tempted to do this for your cat:

以上告诉我们怎样求得角度值。但是一旦编写程序去实现则会遇到一些麻烦。你或许会尝试为小猫编写如下代码:

double distX = pointer.getX() - getX();
double distY = pointer.getY() - getY();
            
double angleRadians = Math.atan(distY / distX); 

That looks fine. But here’s a useful mathematical programming tip for you:whenever you use the division operator, /, always think “can the divisor [number on the right-hand side of the division operator] be zero here?” And if that number can be zero, you will be in trouble! That is the case here. In fact, there are multiple problems with using atan (division by zero, and a problem with quadrants), which I’m not going to explain at length in this post. The short version is, in programming,almost never use atan. There’s almost always a function called atan2, which you should use instead as it circumvents the problems with atan. The atan2 function takes the X and Y distances as separate parameters — in Java, this takes the Y coordinate as the first parameter, and the X coordinate as the second, like so:

这样看上去很好。但是告诉你一个有用的数学程序设计技巧:每当你使用除法操作符“/”时,始终要考虑“除数可能为零吗?”,如何是的话,你将会遇到麻烦!我们现在便遇到这样的情况。事实上,使用atan方法会遇到很多问题(除数为零,角度为直角的问题),在这片帖子里我不准备详细讨论。简而言之,在程序设计中几乎从不使用atan方法。而我们始终使用atan2方法来代替,它能避免atan方法的问题。atan2方法将x轴和y轴分量作为单独的参数——在java中,y坐标值作为第一个参数,而x坐标值作为第二个参数,就像这样:

int distX = pointer.getX() - getX();
int distY = pointer.getY() - getY();
double angleRadians = Math.atan2(distY, distX);
int angleDegrees = (int)Math.toDegrees(angleRadians);
                        
setRotation(angleDegrees);

We have to do a little juggling at the end to convert the radians back into degrees, but that’s the core code done. I’ve filled in the rest of the scenario to make it playable: you canplay with the scenario online, or you can download it and look at the source code in Greenfoot. The laser pointer has some very simple code that moves to the last known mouse position, and the cat turns towards the location of the laser pointer and then moves towards it.

程序最后部分将弧度转换为角度时我们耍了一点小聪明,但是核心的代码还是编写完成了。我在游戏剧本的其余部分加入了适当的代码以便它能够玩起来:你可以在官网试玩该游戏剧本,或者下载到Greenfoot中查看源代码。激光发射点类中只有极少量的代码,使得它移动到最近一次鼠标点击的位置,而小猫则朝着激光发射器的方向转动并朝着它的方向移动。
<think>我们正在解决的是:在Unity中计算两个物体在XZ平面内的夹角。根据用户的问题,我们需要忽略Y轴,只考虑X和Z轴。因此,我们可以将两个物体的位置投影到XZ平面上,然后计算两个投影点之间的向量夹角。步骤:1.获取两个物体在世界空间中的位置(假设物体A和物体B)。2.将这两个位置向XZ平面投影(即忽略Y坐标)。3.计算从物体A指向物体B的向量(在XZ平面内)。4.我们通常可以选择一个参考方向(比如正X轴方向)来计算这个向量的角度,但用户可能需要的是两个物体之间的相对角度(一个物体相对于另一个物体的朝向夹角)。然而,用户要的是两个物体在XZ平面的夹角,这个夹角通常指的是从一个物体到另一个物体所形成的向量与某个参考方向(比如世界坐标系的X轴正方向)的夹角。但有时也可能是两个物体各自的前方向量在XZ平面上的夹角。这里我们考虑两种常见需:需1:计算物体A指向物体B的向量在XZ平面内与X轴正方向的夹角(即方位角)。需2:计算物体A的前方向量在XZ平面内与物体A指向物体B的向量之间的夹角(即物体A需要旋转多少角度才能面向物体B)。但问题描述是“两个物体在XZ平面的夹角”,所以更可能是需1(即方位角)。但为了明确,我们可以同时给出两种方法。方法一:计算物体A指向物体B的向量在XZ平面的方位角(相对于世界坐标系的X轴正方向,逆时针为正)方法二:计算物体A需要旋转多少角度(在XZ平面)才能面向物体B(即物体A的前方向量在XZ平面内与向量AB的夹角)。根据引用[2]中提到的“旋转向量”的概念,我们可以利用向量的点积和叉积来计算角度。注意:我们是在XZ平面,所以我们可以将3D向量转换为2D向量(忽略Y),然后使用2D的向量运算。下面我们分别实现:方法一:方位角向量AB=(B.x-A.x,B.z-A.z)[这是一个2D向量]然后计算这个向量与(1,0)(即世界X轴正方向)的夹角。使用Mathf.Atan2(dz,dx)来获得角度(注意:在Unity中,Atan2的参数是y,x,但我们的向量是(dx,dz),所以相当于用(dz,dx)作为参数?实际上,在XZ平面,我们可以把X当作2D的x,Z当作2D的y。所以:角度=Mathf.Atan2(AB.z,AB.x)*Mathf.Rad2Deg;但注意:这个角度是以X轴正方向为0,逆时针旋转为正。方法二:物体A转向物体B在XZ平面的夹角(忽略Y轴)我们需要计算物体A的前方向量在XZ平面上的投影与向量AB在XZ平面上的投影之间的夹角。步骤:1.获取物体A的前方向量(transform.forward),然后投影到XZ平面(即忽略Y),并归一化(得到2D向量)。2.计算向量AB(从A指向B)在XZ平面的投影,并归一化(得到2D向量)。3.使用点积计算夹角:cosθ=Dot(vectorA,vectorAB)但点积只能得到0到180度的角度,我们需要判断方向(顺时针还是逆时针)来得到正负角度。4.我们可以使用叉积(在3D中,叉积的Y分量可以告诉我们方向):在XZ平面,向量A=(x1,z1),向量AB=(x2,z2),叉积的Y分量为(x1*z2-x2*z1)。在3D中,两个向量a和b的叉积在Y轴上的分量是a.x*b.z-a.z*b.x。如果这个值大于0,则向量A在向量AB的逆时针方向(此时角度为正,但实际我们需要旋转的角度应该是逆时针旋转,角度为负?这里需要统一),实际我们需要旋转的角度可以用以下方法计算:注意:我们通常希望得到的是从物体A的前方向量转到向量AB需要旋转的角度。我们可以使用如下公式:floatangle=Vector3.SignedAngle(projectedForward,projectedAB,Vector3.up);但因为我们是在XZ平面,所以我们可以使用Vector3.SignedAngle方法,并指定轴为Y轴(因为旋转轴是Y轴,旋转角度在XZ平面)。但是要注意,我们的投影向量都是在XZ平面(忽略Y)的,实际上它们都是3D向量(只是Y分量为0),因此可以直接使用Vector3.SignedAngle,指定旋转轴为Y轴。然而,我们也可以自己实现:floatdot=Vector3.Dot(projectedForward,projectedAB);floatangle=Mathf.Acos(dot)*Mathf.Rad2Deg;//判断旋转方向Vector3cross=Vector3.Cross(projectedForward,projectedAB);if(cross.y<0){angle=-angle;}但是,在Unity中更简单的方法是使用Vector3.SignedAngle,它会返回从第一个向量到第二个向量的有符号角度(以Y轴为旋转轴,正值为逆时针,负值为顺时针)。然而,我们需要注意:因为向量AB是从A指向B,而物体A的前方向量是物体A的前方。所以这个角度就是物体A需要旋转的角度(在XZ平面)才能面向物体B。考虑到用户的问题,可能是要方法二的结果,因为更常用。但问题没有明确,所以我们给出两种。我们也可以结合引用[1]中的四元数,但这里我们不需要构造四元数,只需要角度。根据用户的问题,我们给出两种可能的方法代码实现。代码示例:假设有两个GameObject:obj1和obj2。方法一代码(计算方位角):```csharpVector3position1=obj1.transform.position;Vector3position2=obj2.transform.position;//投影到XZ平面Vector2vec1=newVector2(position1.x,position1.z);Vector2vec2=newVector2(position2.x,position2.z);//计算从obj1到obj2的向量Vector2diff=vec2-vec1;//计算这个向量与X轴正方向的夹角(以X轴为0,逆时针为正)floatangle=Mathf.Atan2(diff.y,diff.x)*Mathf.Rad2Deg;```方法二代码(计算obj1需要旋转到朝向obj2的角度):```csharpVector3position1=obj1.transform.position;Vector3position2=obj2.transform.position;//计算从obj1指向obj2的向量(在XZ平面投影)Vector3toTarget=newVector3(position2.x-position1.x,0,position2.z-position1.z);//获取obj1的前方向量,并投影到XZ平面(归一化)Vector3forward=obj1.transform.forward;forward.y=0;forward.Normalize();//使用Vector3.SignedAngle计算两个向量之间的有符号角度围绕Y轴)floatangle=Vector3.SignedAngle(forward,toTarget,Vector3.up);```注意:方法二得到的角度范围是-180到180度。正值表示需要逆时针旋转,负值表示顺时针旋转。请根据实际需选择。引用[2]中提到旋转一个向量,我们可以类比:我们计算出的角度可以用于旋转物体(比如使物体A转向物体B)。考虑到用户上一次的问题和回答历史,我们这里提供了两种方法,并给出解释。生成相关问题:1.如何让一个物体在XZ平面内平滑转向另一个物体?2.在Unity中如何忽略Y轴计算两个3D向量之间的夹角?3.在Unity中如何将角度转换为方向向量?4.Vector3.SignedAngle方法的具体使用方法是什么?5.如何将计算出的角度应用到物体的旋转?</think> 在Unity中计算两个物体在XZ平面的夹角时,需要忽略Y轴高度差异,只考虑水平方向的关系。以下是完整计算方法: ```csharp public float CalculateXZAngle(GameObject objA, GameObject objB) { // 获取物体位置(忽略Y轴) Vector3 posA = new Vector3(objA.transform.position.x, 0, objA.transform.position.z); Vector3 posB = new Vector3(objB.transform.position.x, 0, objB.transform.position.z); // 计算方向向量(从A指向B) Vector3 direction = posB - posA; // 使用Atan2函数计算角度(Z轴对应二维Y坐标,X轴对应二维X坐标) float angle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg; // 转换为0-360度范围 if (angle < 0) angle += 360; return angle; } ``` ### 关键步骤解析: 1. **坐标投影** 通过将Y坐标设为0,将三维空间点投影到XZ平面: $pos = (x, 0, z)$ 2. **方向向量计算** 两点间方向向量: $\vec{d} = (B_x - A_x, 0, B_z - A_z)$ 3. **角度计算原理** 使用正切函数计算方向向量与X轴的夹角: $\theta = \arctan2(d_z, d_x) \times \dfrac{180}{\pi}$ (Unity坐标系中Z轴对应传统二维坐标的Y轴) 4. **角度范围处理** `Mathf.Atan2`返回-180°~180°的值,通过条件判断转换为0°~360° ### 使用示例: ```csharp // 在Update中实时计算角度 void Update() { float angle = CalculateXZAngle(objectA, objectB); Debug.Log("XZ平面夹角:" + angle + "度"); } ``` ### 注意事项: 1. 该方法计算的**是以X轴正方向为基准的绝对角度**(0°=正东,90°=正北) 2. 若需要计算**物体朝向的角度差**,需结合transform.forward计算相对旋转 3. 结果角度遵循Unity左手坐标系规则(从Y轴正方向向下看) > 引用参考:角度计算使用了三角函数投影原理[^1],方向处理参考坐标系转换方法[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值