最近參與的項目總是會涉及到物體的平面反彈問題,于是我就仔細研究了一下,得到一個适用于任何開發工具的通用性方法;下面我将這個方法分享給大家,歡迎大家提出問題、與我讨論
一、問題概述
首先明白任務的需求:對于在場景中任意位置任意角度生成的一根橫杆,實作小球與橫杆發生碰撞後能夠實作平面反射,且要求使用的方法能夠适用于任何引擎。
這個問題的需求其實非常簡單,需要用到一些高中的數學和實體知識,主要涉及到向量的運算,隻要明白其中的原理,實作起來并不是非常困難的事。這篇文章會先從理論上讨論通用的方法,再使用Unity引擎模拟這個過程。
二、理論分析
首先,我們的研究對象是在場景中生成的橫杆,對于這根橫杆,我們需要先得到它的方向向量,這是一件很容易做到的事情,因為橫杆的生成也是由我們控制的,隻要在生成時保留下來随機的相對于水準線的角度值,就可以根據這個角度值求得方向向量。我們設這個随機的角的弧度值為θ(三角函數的運算通常使用的是弧度值,設角度值為ω,那麼弧度值θ=ω*π/180°,之後将不再贅述),橫杆的方向向量為
,那麼
,這裡我們使用1作為模長,是因為我們希望将我們所有的方向向量都轉化為機關向量,友善我們進行之後的計算。
圖1
接下來我們來求一下橫杆的法線的方向向量
,因為我們知道一條直線的法線與該直線必然相差90°,是以可以求得
,同樣我們知道法向量其實也可以是
,到目前為止并不能判斷哪一條是我們需要的,不過這并沒有關系,因為
,需要時可以随時轉化,我們後面會看到這一點。是以這裡我們就取
。
然後我們随便畫出一條入射向量
,再根據該入射向量畫出反射向量
,然後根據平行四邊形法則再分别畫出
和
。根據平面反射可知,入射角等于反射角,易得向量
、
、
、
的模長均相等,即ABCD是一個菱形,如圖2所示。
圖2
我們讓
就等于小球飛行過來時的方向向量,設為
,是以我們現在的任務就是根據
和法向量
要求出反射向量
。接下來就是求解的過程:
首先根據向量運算法則知道
=
=
+
,現在
是已知的,重點就是要求向量
的值,我們又知道
是法線的方向向量,但方向并不确定,在
與
同向的情況下,
= λ
,隻要求得λ即|
|,問題就解決了;如果
與
方向相反,我們隻要讓
取反,問題一樣可以得到解決。是以我們需要先判斷
與
是否同向,這裡可以計算一下
與
的夾角
(向量夾角計算公式:夾角的餘弦值等于兩個向量點乘除以兩個向量的模的乘積,即
=
·
/ |
|*|
|)很顯然在
與
同向的情況下,
必然大于π/2,由此就可以判斷
的方向。
接下來我們要求解|
|的值,顯然由于ABCD是菱形,是以
,幸運的是∠ABD正好與我們前面所求得的
相關,當
時,
,即
;當
時,
,即
,至此,反射相關的問題基本上已經得到解決,剩下的隻是程式的實作,圖3是完整的計算過程。
圖3
不過,除了反射以外,我們同樣還需要考慮一下碰撞檢測的通用方法。現在我們知道橫杆的方向向量和橫杆的位置坐标,那麼可以很輕松的求出橫杆所在的直線方程,設直線方程
,接下來再通過點到直線距離公式就可以得到小球到橫杆的最短距離d(設小球的位置
,那麼
),這樣,我們就可以每一幀都監聽d的大小,當d小于一個極小值ε時,即為滿足反射條件,如圖4所示。
圖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情景下原理其實也基本相同,感興趣的同學可以嘗試實作。
圖5