如何用unity做出磁體相互吸引和排斥的效果?


本文基於Unity 5.3版本的3D物理和C#語言,服用前請注意本文未必適用於其他的版本。

(對於2D的情形,Unity 5可以使用Point Effector實現)

========================================================================

前言

我們知道在諸如Unity此類的遊戲引擎中,遊戲內物體是已經有現成的力與運動的物理模型的。我們可以調用相關的API來對物體施力(比如Unity的AddForce),獲取坐標等。因此在引入萬有引力,磁場力等物體的相互作用時,只要了解力的結算公式和作用方式,即可通過在每幀用腳本進行力的結算和施加,來實現這些物理上的效果。

此外,作為一個遊戲開發者,出於遊戲需求經常會被要求模擬一些其他領域的現象與原理。我們應當始終保持一種學習的心態來獲取其他領域的經驗與知識,但也要記住我們的初衷-「在遊戲中實現」。每當我們遇到這樣的問題時,獲取知識的同時也要在大腦中思考,如何在你所用的引擎中實現這些效果,以及你要追求的是什麼,是正確,還是效率。

追求正確需要以最貼合公式的方式進行結算,並且需要考慮到各類極限情況。在為信息安全,航空航天等精密領域開發軟體時,正確性無疑是非常關鍵的。

追求效率則是優先保證資源分配效率的情況下,允許犧牲一部分正確性。這個效率既可以是指開發成本,比如一些遊戲使用靜態模型動畫替代布娃娃系統;也可以指運行效率,比如多數遊戲物理所採用的牛頓力學而忽略相對論性效應和量子力學效應;兩者都是的情況也有,比如WoW中投射物動畫與遊戲邏輯的分離。這都是在資源有限情況下所側重的一種決策。

在遊戲開發中,我們會經常遇到這樣的抉擇。由於絕大多數遊戲是為用戶打造的,我們更多時候會考慮實現效率。當然也有諸如Kerbal Space Program這樣相對更加追求正確性的遊戲存在,但畢竟是特例,是由遊戲定位所決定的。

基本物理知識與思路

要實現磁體吸引與排斥的功能,先得補習一點物理知識。

磁性是物質響應磁場作用的屬性。每一種物質或多或少地會被磁場影響。鐵磁性是最強烈、最為人知的一種磁性。由於具有鐵磁性,磁石或磁鐵會產生磁場。另外,順磁性物質會趨向於朝著磁場較強的區域移動,即被磁場吸引;反磁性物質則會趨向於朝著磁場較弱的區域移動,即被磁場排斥;還有一些物質會與磁場有更複雜的關係,例如,自旋玻璃的性質、反鐵磁性等等。外磁場對於某些物質的影響非常微弱。

來源:https://zh.wikipedia.org/wiki/%E7%A3%81

磁單極子

磁單極子是一種假想的粒子(或粒子類),這粒子只擁有一個磁極(指北極或指南極)。換句話說,類似帶電粒子的擁有電荷,磁單極子擁有磁荷。

現今,對於這概念的興趣大多出自於粒子物理學,特別值得注意的是大統一理論和超弦理論,關於磁單極子的存在或可能性,它們做了很多預測,因而激發出許多物理學者尋找磁單極子的強烈興趣。但儘管竭盡全力,物理學者至今仍舊無法觀察到任何磁單極子的蛛絲馬跡

永久磁鐵的磁場

永久磁鐵的磁場比較複雜,特別是在磁鐵附近。一個微小條形磁鐵的磁場與其磁矩成正比,也會與磁鐵的定向有關。當尺寸驅向無窮小極限時,磁鐵可以理想化成為磁偶極子,以方程表示,這微小條形磁鐵(磁偶極子)產生的磁場為

有時候,磁鐵與磁鐵之間感受到的磁力和力矩,可以採用「磁極模型」來計算,磁極與磁極之間會互相吸引或互相排斥,就好像電荷與電荷之間的庫倫力。很可惜地,磁極模型不能正確地反映出磁鐵內部的真實狀況(請參閱鐵磁性)。科學家尚未找到磁荷存在的實證。磁鐵的指北極與指南極無法被分離;任何分離動作會造成兩個子磁鐵,各自擁有自己的指北極與指南極。磁極模型無法解釋電流產生的磁場,也無法解釋移動於磁場中的電荷所感受到的洛倫茲力。

更正確地描述磁性,涉及了計算廣泛分布於磁鐵內部的原子尺寸載流循環所產生的磁場。

來源:https://zh.wikipedia.org/wiki/%E7%A3%81%E5%A0%B4

為什麼我只關心這三項?因為作為一個物理苦手,哦不,遊戲程序員(其實我是設計師啊設計師啊),我並不關心磁場強度是怎麼計算的,也不關心磁單極子是否存在。就算把整個物理公式都照搬進來,有效率的在遊戲中模擬也難度非常高,並不符合實際。

對於遊戲需求而言,最常見的是順磁性,反磁性和鐵磁性。我這裡只做鐵磁性(Magnetic)和順磁性(Paramagnetic),對應磁鐵和被磁鐵吸引的物體。如果你想加反磁性,只需要修改順磁性的代碼,把力的方向反過來就能做到了。

我的思路就是,類似於磁極模型,把磁場力的相互作用全部簡化為磁單極之間的作用。在一定範圍內,這樣模擬出來的效果常人是很難分辨的。

事實上,我只關心這裡出現在分母的r三次方項。這r^3告訴了我,磁場內某一點的磁感應強度和相對距離的3次方成反比。具體的常數可以根據遊戲內容和代碼相關進行微調。

========================================================================

場景準備工作

首先,我們先給這個Unity的物理世界加入兩種新成員:順磁性物體(Paramagnetic),鐵磁性物體(Magnetic)。為它們製作帶有標示性的標籤。

然後便是最簡單最容易想到的順磁性物體:硬幣

請原諒我沒有添加金屬材質,這並不是重點。

這個硬幣最重要的就是Rigidbody和Tag了

注意Continuous Dynamic可以防止因瞬時速度太快而導致的碰撞失效。

稀鬆平常的硬幣,由壓扁的圓柱體製成。加上Rigidbody和標籤,這樣就算完工了。

做成Prefab吧。

接下來做一個磁鐵吧。

注意我用的是紅色長方體(North Bar)加藍色長方體(South Bar)拼成的,為了區分N極與S極。

不要忘記標籤。

重點解釋一下磁鐵下的兩個Empty Object:North Pole South Pole

這兩個抽象的點,便是鐵磁性物體的磁極

這裡用了物理學終極大法:簡化為質點法,把所有磁性物體的相互作用都看作磁單極點的相互作用,一會的Script便可以用到它們(作為NP和SP)。

注意:一定要把磁極藏在物體裡面,否則會導致外部物體和裸磁極接觸後果自負!

其實也沒那麼嚴重,具體請參照下面腳本里,如果有像範例的腳本里一樣限制最小距離的話,也不用擔心和裸磁極接觸直接彈飛的問題。

這樣還沒完,還得做出「磁場」這個物體。

根據需要來做,我這裡用一個Capsule型的Trigger Collider代替了。

要避免bug的話,推薦大一點,要注重性能的話,推薦小一點。

注意勾上Trigger,這樣不會錯誤的被認為是物體本身的碰撞體。

完成以後,做成Prefab。

這裡「磁場」的意義是,規定相互作用的最大範圍。

i.e. 任何超出這個範圍的物體均不在相互作用的考量範圍內,有點類似希爾球的概念。

這樣場景編輯器里的工作就暫時完成了,我們得到了兩個Prefab:硬幣,以及條形磁鐵。

========================================================================

腳本編寫

在準備編寫的時候,先得理清楚思路。比如「我們現在有了什麼,我們要腳本實現的是什麼」。

我們已經有了鐵磁性物體和順磁性物體的Prefab,鐵磁性物體也有了兩個磁單極點和磁場。

我們現在需要一個鐵磁性物體的腳本,來對磁場內的物體施加作用力,即磁場力。

在磁場中的物體受到的磁場力應該取決於如下幾個因素:

-物體與磁鐵磁極的位移距離,的三次方,成反比(根據開頭的物理知識)

-物體的「磁導率」。(物體對磁鐵越友好,受到磁力越大)

-磁鐵的「磁場強度」。(磁鐵的強度越高,能施加的磁力越大)

-作為上帝,哦不,遊戲程序員所必需的,一個磁常數項,用於根據實際結果調整大小。

上述簡化做法來自生活經驗,同物理學公式存在出入,請勿當作物理學知識運用!

為此我們可以先做出一個吸引順磁性物體的腳本,然後進一步細化到鐵磁性物體的相互作用。

磁鐵腳本開頭聲明部分:

現在為條形磁鐵添加一個腳本吧(這個腳本也應該適用於所有帶有鐵磁性的物體):

using UnityEngine;
using System.Collections;

public class MagneticField : MonoBehaviour {
public GameObject NP;
public GameObject SP;
private static float mu0 = 10.0f;
public float strength;
private Rigidbody rb;
// Use this for initialization
void Awake () {
rb = GetComponent& ();
}
}

NP:該磁鐵的N極。

SP:該磁鐵的S極。

mu0:磁常數項。

strength:該磁鐵的「磁場強度」。

rb:讀取並記錄該物體的rigidbody,便於後續頻繁的使用。

回到場景編輯器中,將條形磁鐵附屬的兩個磁極分別拖拽連接到對應的位置,並設定磁化強度。

磁鐵同磁場內順磁性物體交互的部分:

我們之前設定了一個Trigger Collider作為磁場最大範圍,現在可以直接用OnTriggerStay來編寫每一幀的同磁場範圍內的物體的相互作用。

這裡我為了簡化操作避免給順磁性物體添加腳本,直接用質量代替了物體的「磁導率」如果你有實際應用的需求,可以更改代碼里的相關項目,並為物體加上對應的腳本和屬性來實現。

// Range of magnetic field
void OnTriggerStay (Collider other) {

Rigidbody other_rb = other.gameObject.GetComponent& ();

// check the tag of object
if (other.gameObject.tag == "Paramagnetic") {
Vector3 r_n = other.gameObject.transform.position - NP.transform.position; // displacement vector
Vector3 r_s = other.gameObject.transform.position - SP.transform.position; // displacement vector
float mu = Time.fixedDeltaTime * strength * mu0 * other_rb.mass;
// Calculate Force towards magnetic poles
Vector3 f_n =
(r_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n.magnitude, 0.2f), 3));
Vector3 f_s =
(r_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s.magnitude, 0.2f), 3));
// 截圖中此處debug請無視
// Apply attracting force to other object
other_rb.AddForce (
-1f * f_n);
other_rb.AddForce (
-1f * f_s);
// Apply attracting force to magnetic object
rb.AddForceAtPosition (
1f * f_n,
NP.transform.position);
rb.AddForceAtPosition (
1f * f_s,
SP.transform.position);
}
}

other_rb:同該磁鐵相互作用的目標物體的rigidbody。

r_n:從北磁極到目標物體的矢量位移。

r_s:從南磁極到目標物體的矢量位移。

mu:由每幀間隔時間,磁鐵的磁場強度,目標物體的磁導率(在這裡用了質量)以及磁常數共同決定的一個公式係數。

f_n:北磁極對物體施加的矢量力。

f_s:南磁極對物體施加的矢量力。

磁場力的大小最終為mu值除以距離的三次方。力的方向自然是位移的方向。

我這裡限定了位移距離的標量大小不小於一個特定值(如0.2),否則分母在接近0的時候會出現磁力過大彈飛物體的問題。電腦物理引擎每幀間隔都相對比較長,因此要考慮到這點。

此外要注意我不只為被吸引的物體加了吸引力,還為磁鐵的磁極加了一個相反方向的力

這是因為:

牛頓第三運動定律

To every action there is always opposed an equal reaction: or the mutual actions of two bodies upon each other are always equal, and directed to contrary parts.

每一個作用力都對應著一個相等反抗的反作用力:也就是說,兩個物體彼此施加於對方的力總是大小相等、方向相反。

https://zh.wikipedia.org/wiki/%E7%89%9B%E9%A1%BF%E7%AC%AC%E4%B8%89%E8%BF%90%E5%8A%A8%E5%AE%9A%E5%BE%8B

有了這個腳本,目前場景內的磁鐵應該已經可以吸引硬幣了。

你可以加一點簡單的控制腳本來幫助測試,並且調整對應的常數值來觀察物體相互作用的變化,以達到你自己的預期要求。

磁鐵同磁場內其他磁鐵交互的部分:

上一步的腳本只做了針對帶有「Paramagnetic」標籤的物體的相互作用,現在我們來做對於「Magnetic」的物體,也就是其他磁鐵,的相互作用。把之前的if 部分改到下列代碼的else if部分即可。

if (other.gameObject.tag == "Magnetic") {
MagneticField other_script = other.gameObject.GetComponent& ();
GameObject other_NP = other_script.NP;
GameObject other_SP = other_script.SP;
Vector3 r_n_n = other_NP.transform.position - NP.transform.position; // from this(N) to that(N)
Vector3 r_s_s = other_SP.transform.position - SP.transform.position; // from this(S) to that(S)
Vector3 r_n_s = other_SP.transform.position - NP.transform.position; // from this(N) to that(S)
Vector3 r_s_n = other_NP.transform.position - SP.transform.position; // from this(S) to that(N)
float other_strength = other_script.strength;

// Calculate Force towards magnetic poles
float mu = Time.fixedDeltaTime * strength * other_strength;
Vector3 f_n_n = // from this(N) to that(N)
(r_n_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n_n.magnitude, 0.2f), 3));
Vector3 f_s_s = // from this(S) to that(S)
(r_s_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s_s.magnitude, 0.2f), 3));
Vector3 f_n_s = // from this(N) to that(S)
(r_n_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n_s.magnitude, 0.2f), 3));
Vector3 f_s_n = // from this(S) to that(N)
(r_s_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s_n.magnitude, 0.2f), 3));
//截圖中此處的Debug請無視

// Apply Rejecting Force
other_rb.AddForceAtPosition (
1f * f_n_n,
other_NP.transform.position);
other_rb.AddForceAtPosition (
1f * f_s_s,
other_SP.transform.position);

// Apply Attracting Force
other_rb.AddForceAtPosition (
-1f * f_n_s,
other_SP.transform.position);
other_rb.AddForceAtPosition (
-1f * f_s_n,
other_NP.transform.position);

}

相比順磁性的物體來說,兩個鐵磁性物體的交互則稍許多了點變數和運算量。

other_rb:同該磁鐵相互作用的目標磁鐵的rigidbody。

other_script:目標磁鐵下屬的這個腳本。用於獲取該物體同腳本連接的public變數。

r_n_n:從本磁鐵北磁極到目標磁鐵北磁極的矢量位移。

r_s_s:從本磁鐵南磁極到目標磁鐵南磁極的矢量位移。

r_n_s:從本磁鐵北磁極到目標磁鐵南磁極的矢量位移。

r_s_n:從本磁鐵南磁極到目標磁鐵北磁極的矢量位移。

mu:由每幀間隔時間,磁鐵的磁場強度,目標磁鐵的磁場強度以及磁常數共同決定的一個公式係數。

f_n_n:本磁鐵北磁極對目標磁鐵北磁極施加的矢量力。

f_s_s:本磁鐵南磁極對目標磁鐵南磁極施加的矢量力。

f_n_s:本磁鐵北磁極對目標磁鐵南磁極施加的矢量力。

f_s_n:本磁鐵南磁極對目標磁鐵北磁極施加的矢量力。

這裡磁場力的計算同上一部分異曲同工。

此外,還要注意因為磁鐵(是直的,)存在「同性相斥,異性相吸」的規律。同名磁極之間的力應當是排斥力。

為什麼我這裡沒有向之前一樣施加反作用力?

因為同該磁鐵相互作用的另一磁鐵也有這個腳本!也會執行同樣的效果!

而且由於計算公式符合交換律,計算出的結果應當是一樣的。

我這裡默認物體的磁場範圍都足夠大。如果你有特殊需求,或者說你的磁場範圍不得不做的很小,那麼請考慮到一個情況:B磁鐵進了A磁鐵的磁場範圍,而A磁鐵沒有進入B磁鐵的磁場範圍。這可能會導致潛在的bug

如果你的磁場範圍都足夠大,磁場邊緣的相互作用已經可以忽略不計了,那麼就放心的省了反作用力吧。

這個問題,符合和我開頭所說的抉擇,即追求正確還是效率。

這樣以後就大工告成了。

完整代碼:

using UnityEngine;
using System.Collections;

public class MagneticField : MonoBehaviour {
public GameObject NP;
public GameObject SP;
private static float mu0 = 10.0f;
public float strength;
private Rigidbody rb;

// Use this for initialization
void Awake () {
rb = GetComponent& ();
}

// Update is called once per frame
void FixedUpdate () {

}

// Range of magnetic field
void OnTriggerStay (Collider other) {

Rigidbody other_rb = other.gameObject.GetComponent& ();

if (other.gameObject.tag == "Magnetic") {
MagneticField other_script = other.gameObject.GetComponent& ();
GameObject other_NP = other_script.NP;
GameObject other_SP = other_script.SP;
Vector3 r_n_n = other_NP.transform.position - NP.transform.position; // from this(N) to that(N)
Vector3 r_s_s = other_SP.transform.position - SP.transform.position; // from this(S) to that(S)
Vector3 r_n_s = other_SP.transform.position - NP.transform.position; // from this(N) to that(S)
Vector3 r_s_n = other_NP.transform.position - SP.transform.position; // from this(S) to that(N)
float other_strength = other_script.strength;

// Calculate Force towards magnetic poles
float mu = Time.fixedDeltaTime * strength * other_strength;
Vector3 f_n_n = // from this(N) to that(N)
(r_n_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n_n.magnitude, 0.2f), 3));
Vector3 f_s_s = // from this(S) to that(S)
(r_s_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s_s.magnitude, 0.2f), 3));
Vector3 f_n_s = // from this(N) to that(S)
(r_n_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n_s.magnitude, 0.2f), 3));
Vector3 f_s_n = // from this(S) to that(N)
(r_s_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s_n.magnitude, 0.2f), 3));
//Debug.Log ( f_n_n.magnitude + ", " + f_n_n.magnitude);

// Apply Rejecting Force
other_rb.AddForceAtPosition (
1f * f_n_n,
other_NP.transform.position);
other_rb.AddForceAtPosition (
1f * f_s_s,
other_SP.transform.position);

// Apply Attracting Force
other_rb.AddForceAtPosition (
-1f * f_n_s,
other_SP.transform.position);
other_rb.AddForceAtPosition (
-1f * f_s_n,
other_NP.transform.position);

}
else if (other.gameObject.tag == "Paramagnetic") {
Vector3 r_n = other.gameObject.transform.position - NP.transform.position; // displacement vector
Vector3 r_s = other.gameObject.transform.position - SP.transform.position; // displacement vector
float mu = Time.fixedDeltaTime * strength * mu0 * other_rb.mass;
// Calculate Force towards magnetic poles
Vector3 f_n =
(r_n.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_n.magnitude, 0.2f), 3));
Vector3 f_s =
(r_s.normalized) * mu /
(Mathf.Pow (Mathf.Max(r_s.magnitude, 0.2f), 3));
//Debug.Log ( nf.magnitude + ", " + sf.magnitude);

// Apply attracting force to other object
other_rb.AddForce (
-1f * f_n);
other_rb.AddForce (
-1f * f_s);
// Apply attracting force to magnetic object
rb.AddForceAtPosition (
1f * f_n,
NP.transform.position);
rb.AddForceAtPosition (
1f * f_s,
SP.transform.position);
}
}

}

再加點控制功能就可以進場景測試了。

========================================================================

實例體驗

硬幣,條形磁鐵。

接近中

成功吸附!

多丟點硬幣下來。不要吐槽硬板的陰影效果和藥片似的硬幣材質。。。

進去掃一圈。效果還不錯。

多放點硬幣,老mac電腦還算剛的住,掉幀不明顯。

這裡還是有瑕疵的,按理說被磁鐵吸引的物體自己也會被暫時磁化,所以在這裡應該是硬幣吸硬幣吸起一串來。(有興趣的同學可以自己嘗試實現,一個思路是把順磁性物體做成兩個磁極重合的磁化強度較低的鐵磁性物體,然後根據受磁化率提升自身的磁化強度。)

清空,再丟個條形磁鐵下來。

從左側平行接近目標。

十動然拒了。。

從上方靠近

合體!

再來丟下來點磁鐵。

測試項目的瀏覽器在線試玩(不支持Chrome,需下載Unity Web Player):

Magnet Simulation Test (海外網站)

操作:

上下左右:通過一個特定數值的力移動磁鐵。(磁鐵互相吸引後由於用於移動的力還是恆定值,因此會越來越難移動)

A/Z:抬起/下壓磁鐵。

空格:天上隨機掉下一個硬幣。

B:天上隨機掉下一個條形磁鐵。

這個項目只是用來測試上述的磁場相互作用腳本的,基本已經和現實生活很接近了。

該腳本也基本適用於其他形狀不太正常的磁鐵,使用時最主要的還是兩個磁極的位置以及強度的設定。公式是死的,人是活的,請結合實際情況使用和修改。

警告:本答案的代碼只為實現功能,均未經優化也未按設計規範編寫,直接搬運存在大幅度影響性能的風險,答主概不負責!對物理知識的錯誤應用和解析,作者也概不負責!

========================================================================

有人說不要自己造輪子,Asset Store上已經有了。

可也要看看別人的輪子隨著引擎的更新還能不能用了。

========================================================================

轉載請私信。

本文已授權遊資網http://GameRes.com轉載。


磁鐵之間的相互作用力可以用磁偶極的公式進行積分

mathbf{F}(mathbf{r}, mathbf{m}_1, mathbf{m}_2) = frac{3 mu_0}{4 pi r^5}left[(mathbf{m}_1cdotmathbf{r})mathbf{m}_2 + (mathbf{m}_2cdotmathbf{r})mathbf{m}_1 + (mathbf{m}_1cdotmathbf{m}_2)mathbf{r} - frac{5(mathbf{m}_1cdotmathbf{r})(mathbf{m}_2cdotmathbf{r})}{r^2}mathbf{r}
ight]

偶極的磁場mathbf{B}({mathbf{r}})=
abla	imes{mathbf{A}}=frac{mu_{0}}{4pi}left(frac{3mathbf{r}(mathbf{m}cdotmathbf{r})}{r^{5}}-frac{{mathbf{m}}}{r^{3}}
ight)

順磁性物質的總磁矩mathbf{M}可以由方程mu_0(1+chi_m^{-1})mathbf{M}=mathbf{B}算出。


Simple Physics Toolkit 這個插件效果還可以。在unity 2017.2可以使用。


asset store 有插件,

https://www.assetstore.unity3d.com/en/#!/content/3673

看看能用么,能在此基礎上修改么,盡量別造輪子


推薦閱讀:

為什麼手游現在普遍是996的工作了呢?
有沒有人用Unity3D不是做遊戲而是做App的?
無基礎製作獨立遊戲,應從哪裡入手?
如何防止Unity3D代碼被反編譯?
如何用unity實現switch噴射戰士2(Splatoon 2)中的效果 ?

TAG:Unity遊戲引擎 |