最近参与的项目总是会涉及到物体的平面反弹问题,于是我就仔细研究了一下,得到一个适用于任何开发工具的通用性方法;下面我将这个方法分享给大家,欢迎大家提出问题、与我讨论
一、问题概述
首先明白任务的需求:对于在场景中任意位置任意角度生成的一根横杆,实现小球与横杆发生碰撞后能够实现平面反射,且要求使用的方法能够适用于任何引擎。
这个问题的需求其实非常简单,需要用到一些高中的数学和物理知识,主要涉及到向量的运算,只要明白其中的原理,实现起来并不是非常困难的事。这篇文章会先从理论上讨论通用的方法,再使用Unity引擎模拟这个过程。
二、理论分析
首先,我们的研究对象是在场景中生成的横杆,对于这根横杆,我们需要先得到它的方向向量,这是一件很容易做到的事情,因为横杆的生成也是由我们控制的,只要在生成时保留下来随机的相对于水平线的角度值,就可以根据这个角度值求得方向向量。我们设这个随机的角的弧度值为θ(三角函数的运算通常使用的是弧度值,设角度值为ω,那么弧度值θ=ω*π/180°,之后将不再赘述),横杆的方向向量为,那么
,这里我们使用1作为模长,是因为我们希望将我们所有的方向向量都转化为单位向量,方便我们进行之后的计算。

接下来我们来求一下横杆的法线的方向向量,因为我们知道一条直线的法线与该直线必然相差90°,所以可以求得
,同样我们知道法向量其实也可以是
,到目前为止并不能判断哪一条是我们需要的,不过这并没有关系,因为
,需要时可以随时转化,我们后面会看到这一点。所以这里我们就取
。
然后我们随便画出一条入射向量,再根据该入射向量画出反射向量
,然后根据平行四边形法则再分别画出
和
。根据平面反射可知,入射角等于反射角,易得向量
、
、
、
的模长均相等,即ABCD是一个菱形,如图2所示。

我们让就等于小球飞行过来时的方向向量,设为
,所以我们现在的任务就是根据
和法向量
要求出反射向量
。接下来就是求解的过程:
首先根据向量运算法则知道 =
=
+
,现在
是已知的,重点就是要求向量
的值,我们又知道
是法线的方向向量,但方向并不确定,在
与
同向的情况下,
= λ
,只要求得λ即|
|,问题就解决了;如果
与
方向相反,我们只要让
取反,问题一样可以得到解决。所以我们需要先判断
与
是否同向,这里可以计算一下
与
的夹角
(向量夹角计算公式:夹角的余弦值等于两个向量点乘除以两个向量的模的乘积,即
=
·
/ |
|*|
|)很显然在
与
同向的情况下,
必然大于π/2,由此就可以判断
的方向。
接下来我们要求解||的值,显然由于ABCD是菱形,所以
,幸运的是∠ABD正好与我们前面所求得的
相关,当
时,
,即
;当
时,
,即
,至此,反射相关的问题基本上已经得到解决,剩下的只是程序的实现,图3是完整的计算过程。

不过,除了反射以外,我们同样还需要考虑一下碰撞检测的通用方法。现在我们知道横杆的方向向量和横杆的位置坐标,那么可以很轻松的求出横杆所在的直线方程,设直线方程,接下来再通过点到直线距离公式就可以得到小球到横杆的最短距离d(设小球的位置
,那么
),这样,我们就可以每一帧都监听d的大小,当d小于一个极小值ε时,即为满足反射条件,如图4所示。

至此,小球反弹问题已经从理论上得到解决,接下来是我们尝试用Unity模拟这个过程。(注:Unity中已存在Vector2.Reflect()函数,传入入射向量和法向量即可实现核心的反射功能,我们在理论阶段的工作相当于实现了Reflect()的底层,为了保证方法在任何引擎中都适用,这是必须的,因此,我们在Unity的模拟工作中也不会使用其自带的Reflect()函数;此外,Unity中也存在一些自带的数学方法,能够实现求两个向量的夹角、角度值和弧度值的转化等等,因为这些问题比较简单,也不是我们这次讨论的核心问题,所以方便起见,我们会在程序模拟的时候使用)
三、代码实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallManager : MonoBehaviour
{
public Vector3 direction;
//小球移动的方向向量
public float flySpeed;
//小球移动速度
private void Update()
{
if (Mathf.Abs(this.transform.position.x) > 9 || Mathf.Abs(this.transform.position.y) > 5)
GameObject.Destroy(this.gameObject);
//越界销毁
}
private void FixedUpdate()
{
this.transform.Translate(direction * flySpeed * Time.fixedDeltaTime, Space.World);
//小球移动
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Wall"))
{
float normalAngle = (collision.GetComponent<Transform>().eulerAngles.z + 90) * Mathf.Deg2Rad;
//法线的角度值与横杆相差90°,Mathf.Deg2Rad是一个常数,可直接将角度值转化为弧度值
Vector3 normalDir = new Vector3(Mathf.Cos(normalAngle), Mathf.Sin(normalAngle), 0);
//求法线的方向向量
direction = Reflect(direction, normalDir);
//改变小球移动方向为反射向量
}
}
//偷个小懒,直接使用Unity的内置方法进行碰撞检测
private Vector3 Reflect(Vector3 inDir, Vector3 nDir)
{
float angleTemp = 0;
//定义一个变量保存θ2
if (Vector3.Angle(inDir, nDir) > 90)
{
angleTemp = 180 - Vector3.Angle(inDir, nDir);
//若向量夹角大于90°,则θ2为向量角的补角
//Vector3.Angle方法可以直接求得两向量的夹角
}
else
{
angleTemp = Vector3.Angle(inDir, nDir);
nDir = -nDir;
//若向量夹角小于90°,则θ2等于向量角,法向量取反
}
Vector3 outDir = Mathf.Cos(angleTemp * Mathf.Deg2Rad) * 2 * nDir + inDir;
//求反射向量
return outDir;
}
//反射函数
}
四、效果演示
五、扩展
上面讨论的问题均是在2D情景下实现的,如果扩展到3D情景下原理其实也基本相同,感兴趣的同学可以尝试实现。
