游戏物体的力与运动:用unity实现磁体相互吸引和排斥的效果
作者:互联网
本文基于Unity 5.3版本和C#语言,使用前请注意本文未必适用于其他的版本。
前言
我们知道在诸如Unity此类的游戏引擎中,游戏内物体是已经有现成的力与运动的物理模型的。我们可以调用相关的API来对物体施力(比如Unity的AddForce),获取坐标等。因此在引入万有引力,磁场力等物体的相互作用时,只要了解力的结算公式和作用方式,即可通过在每帧用脚本进行力的结算和施加,来实现这些物理上的效果。
此外,作为一个游戏开发者,出于游戏需求经常会被要求模拟一些其他领域的现象与原理。我们应当始终保持一种学习的心态来获取其他领域的经验与知识,但也要记住我们的初衷-“在游戏中实现”。每当我们遇到这样的问题时,获取知识的同时也要在大脑中思考,如何在你所用的引擎中实现这些效果,以及你要追求的是什么,是正确,还是效率。
追求正确需要以最贴合公式的方式进行结算,并且需要考虑到各类极限情况。在为信息安全,航空航天等精密领域开发软件时,正确性无疑是非常关键的。
追求效率则是优先保证资源分配效率的情况下,允许牺牲一部分正确性。这个效率既可以是指开发成本,比如一些游戏使用静态模型动画替代布娃娃系统;也可以指运行效率,比如多数游戏物理所采用的牛顿力学而忽略相对论性效应和量子力学效应;两者都是的情况也有,比如WoW中投射物动画与游戏逻辑的分离。这都是在资源有限情况下所侧重的一种决策。
在游戏开发中,我们会经常遇到这样的抉择。由于绝大多数游戏是为用户打造的,我们更多时候会考虑实现效率。当然也有诸如Kerbal Space Program这样相对更加追求正确性的游戏存在,但毕竟是特例,是由游戏定位所决定的。
基本物理知识与思路
要实现磁体吸引与排斥的功能,先得补习一点物理知识。
“磁性是物质响应磁场作用的属性。每一种物质或多或少地会被磁场影响。铁磁性是最强烈、最为人知的一种磁性。由于具有铁磁性,磁石或磁铁会产生磁场。另外,顺磁性物质会趋向于朝着磁场较强的区域移动,即被磁场吸引;反磁性物质则会趋向于朝着磁场较弱的区域移动,即被磁场排斥;还有一些物质会与磁场有更复杂的关系,例如,自旋玻璃的性质、反铁磁性等等。外磁场对于某些物质的影响非常微弱。”
“磁单极子
磁单极子是一种假想的粒子(或粒子类),这粒子只拥有一个磁极(指北极或指南极)。换句话说,类似带电粒子的拥有电荷,磁单极子拥有磁荷。
现今,对于这概念的兴趣大多出自于粒子物理学,特别值得注意的是大统一理论和超弦理论,关于磁单极子的存在或可能性,它们做了很多预测,因而激发出许多物理学者寻找磁单极子的强烈兴趣。但尽管竭尽全力,物理学者至今仍旧无法观察到任何磁单极子的蛛丝马迹.”
“永久磁铁的磁场
永久磁铁的磁场比较复杂,特别是在磁铁附近。一个微小条形磁铁的磁场与其磁矩成正比,也会与磁铁的定向有关。当尺寸驱向无穷小极限时,磁铁可以理想化成为磁偶极子,以方程表示,这微小条形磁铁(磁偶极子)产生的磁场为
有时候,磁铁与磁铁之间感受到的磁力和力矩,可以采用“磁极模型”来计算,磁极与磁极之间会互相吸引或互相排斥,就好像电荷与电荷之间的库伦力。很可惜地,磁极模型不能正确地反映出磁铁内部的真实状况(请参阅铁磁性)。科学家尚未找到磁荷存在的实证。磁铁的指北极与指南极无法被分离;任何分离动作会造成两个子磁铁,各自拥有自己的指北极与指南极。磁极模型无法解释电流产生的磁场,也无法解释移动于磁场中的电荷所感受到的洛伦兹力。
更正确地描述磁性,涉及了计算广泛分布于磁铁内部的原子尺寸载流循环所产生的磁场。”
为什么我只关心这三项?因为作为一个物理苦手,哦不,游戏程序员(其实我是设计师啊设计师啊),我并不关心磁场强度是怎么计算的,也不关心磁单极子是否存在。就算把整个物理公式都照搬进来,有效率的在游戏中模拟也难度非常高,并不符合实际。
对于游戏需求而言,最常见的是顺磁性,反磁性和铁磁性。我这里只做铁磁性(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<Rigidbody> ();
- }
- }
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<Rigidbody> ();
- // 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.”
“每一个作用力都对应着一个相等反抗的反作用力:也就是说,两个物体彼此施加于对方的力总是大小相等、方向相反。”
有了这个脚本,目前场景内的磁铁应该已经可以吸引硬币了。
你可以加一点简单的控制脚本来帮助测试,手机游戏拍卖平台并且调整对应的常数值来观察物体相互作用的变化,以达到你自己的预期要求。
磁铁同磁场内其他磁铁交互的部分:
上一步的脚本只做了针对带有“Paramagnetic”标签的物体的相互作用,现在我们来做对于“Magnetic”的物体,也就是其他磁铁,的相互作用。把之前的if 部分改到下列代码的else if部分即可。
- if (other.gameObject.tag == "Magnetic") {
- MagneticField other_script = other.gameObject.GetComponent<MagneticField> ();
- 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<Rigidbody> ();
- }
- // Update is called once per frame
- void FixedUpdate () {
- }
- // Range of magnetic field
- void OnTriggerStay (Collider other) {
- Rigidbody other_rb = other.gameObject.GetComponent<Rigidbody> ();
- if (other.gameObject.tag == "Magnetic") {
- MagneticField other_script = other.gameObject.GetComponent<MagneticField> ();
- 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:天上随机掉下一个条形磁铁。
这个项目只是用来测试上述的磁场相互作用脚本的,基本已经和现实生活很接近了。
该脚本也基本适用于其他形状不太正常的磁铁,使用时最主要的还是两个磁极的位置以及强度的设定。公式是死的,人是活的,请结合实际情况使用和修改。
标签:磁铁,磁极,物体,磁体,transform,排斥,unity,other,position 来源: https://blog.csdn.net/wubaohu1314/article/details/120472902