您好,登錄后才能下訂單哦!
今天小編給大家分享一下Unity怎么實現有影響的球體運動的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
本教程使用Unity 2019.4.4f1制作。它還使用ProBuilder軟件包。
效果之一
我改進了軌道攝像機的1.4節“使焦點居中”,以便更好地實現焦點居中和焦點半徑限制的相互作用。調整OrbitCamera.UpdateFocusPoint如下:
void UpdateFocusPoint () { previousFocusPoint = focusPoint; Vector3 targetPoint = focus.position; if (focusRadius > 0f) { float distance = Vector3.Distance(targetPoint, focusPoint); float t = 1f; if (distance > 0.01f && focusCentering > 0f) { t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime); } if (distance > focusRadius) { t = Mathf.Min€(t, focusRadius / distance); } focusPoint = Vector3.Lerp(targetPoint, focusPoint, t); } else { focusPoint = targetPoint; } }
我還更改了“移動地面”部分2.3確定運動,因此忽略了質量較輕的連接物體。這樣可以防止球體自動跟隨其推開的輕物體。如下調整MovingSphere.UpdateState結尾:
if (connectedBody) { if (connectedBody.isKinematic || connectedBody.mass >= body.mass) { UpdateConnectionState(); } }
最后,更改了“攀爬”第2.5節的“可選攀登”,以防止自動粘在動畫的可攀爬表面上。這是通過在EvaluateCollision中調節desiresClimbing
而不是在Climbing
屬性中完成的:
bool Climbing =>climbContactCount > 0 && stepsSinceLastJump > 2;
…
void EvaluateCollision (Collision collision) {
…
if (
desiresClimbing &&upDot >= minClimbDotProduct &&
(climbMask & (1 << layer)) != 0
) {
climbContactCount += 1;
climbNormal += normal;
lastClimbNormal = normal;
connectedBody = collision.rigidbody;
}
…
}
(另一個)效果
加速區
主動環境比靜態環境有趣,尤其是當它對正在發生的事情做出反應時。這種行為可以對任何事情做出反應,也可以做任何事情,但是一個簡單的例子就是跳墊:只要有東西落在墊上,它就會向上發射。這可能是我們的運動球或碰巧掉落或推到墊子上的任何其他物體。因此,該行為在邏輯上屬于跳板。其他物體不必知道它的存在,它們只是突然結束飛行。
區域組成
描述跳板行為的最通用方法是,該區域可加速進入其的任何物體。因此,我們將創建一個AccelerationZone
組件類型,其可配置的速度不能為負。
using UnityEngine;
public class AccelerationZone : MonoBehaviour {
[SerializeField, Min(0f)]
float speed = 10f;
}
可以通過將具有觸發對撞器的對象添加到場景,然后將區域行為附加到場景來創建區域。您也可以添加可視化跳板的對象,但是我只是用半透明的黃色材料使該區域可見。
當帶Rigidbody的東西進入區域時,我們應該加速它。為此在OnTriggerEnter添加一個方法Accelerate,該方法以觸發主體作為參數調用新方法。進入該區域的所有物體都會發生這種情況,但是如果需要,您可以使用圖層來防止檢測到某些物體。
void OnTriggerEnter (Collider other) {
Rigidbody body = other.attachedRigidbody;
if (body) {
Accelerate(body);
}
}
void Accelerate(Rigidbody body) {}
Accelerate中
只需使身體速度的Y分量等于配置的速度,除非它已經更大。其他速度分量不受影響。
void Accelerate(Rigidbody body) {
Vector3 velocity = body.velocity;
if (velocity.y >= speed) {
return;
}
velocity.y = speed;
body.velocity = velocity;
}
防止猛然掉地
當發射常規物體時,這種簡單的方法效果很好,但是我們的球體沒有正確發射。當它進入區域時,它似乎獲得了很大的前進速度。發生這種情況是因為我們將其卡在了地上。在這種情況下,可以通過降低“ 最大捕捉速度”來解決,但不適用于設置為低速的加速區域。為了防止接地,一般來說,我們必須指示MovingSphere
暫時不要執行接地。我們可以通過PreventSnapToGround
向其添加設置stepsSinceLastJump
為-1 的公共方法來做到這一點。
public void PreventSnapToGround () { stepsSinceLastJump = -1; }
現在AccelerationZone.Accelerate
可以在主體具有MovingSphere
組件的情況下調用此方法,我們可以通過調用TryGetComponent
球體作為輸出參數來進行檢查和檢索。
void Accelerate(Rigidbody body) { … if (body.TryGetComponent(out MovingSphere sphere)) { sphere.PreventSnapToGround(); } }
請注意,這種方法不會重置跳躍階段,因此在沒有降落的情況下彈跳跳板不會刷新空氣跳躍。
持續加速
瞬時速度變化對于跳板很合適,但是我們也可以使用該區域創建其他連續的加速度現象,例如懸浮區域。我們可以通過簡單地添加與OnTriggerStay相同的方法OnTriggerEnter來支持這一點。
void OnTriggerStay (Collider other) { Rigidbody body = other.attachedRigidbody; if (body) { Accelerate(body); } }
如果效果持續時間較長,那么通過適當的加速度來實現速度變化會更好一些,因此讓我們向該區域添加一個可配置的加速度,且最小值也應為零。如果將其設置為零,我們將立即進行更改,否則將應用加速。
[SerializeField, Min(0f)]
floatacceleration = 10f,speed = 10f;
…
void Accelerate(Rigidbody body) {
…
if (acceleration > 0f) {
velocity.y = Mathf.MoveTowards(
velocity.y, speed, acceleration * Time.deltaTime
);
}
else {
velocity.y = speed;
}
…
}
也可以施加力,這樣質量較大的物體最終的加速度會變慢,但是固定的加速度使水平設計更容易,因此我使用了這一點。
任意方向
最后,為了使其可以向任何方向加速,請在Accelerate開始時將人體速度轉換為區域的局部空間,并在應用時將其轉換回世界空間。通過InverseTransformDirection和TransformDirection這樣做,因此區域的比例不會對其產生影響。現在可以通過旋轉區域來控制加速方向。
void Accelerate(Rigidbody body) {
Vector3 velocity =transform.InverseTransformDirection(body.velocity);
…
body.velocity =transform.TransformDirection(velocity);
…
}
對存在做出反應
加速區只是如何創建具有特定行為的觸發區的一個示例。如果您需要一個區域執行其他操作,則必須為其編寫新代碼。但是檢測和響應某處某物的存在的簡單行為是如此普遍,以至于我們理想情況下只編寫一次。而且許多行為非常簡單(例如激活對象),以至于無法為其創建專用的組件類型。而更復雜的行為通常只是一些簡單動作的組合。如果關卡設計師可以通過簡單地配置游戲對象并添加一些組件來創建它,而不必一直創建專門的代碼,這將很方便。
檢測區
讓我們從創建一個DetectionZone
組件開始,該組件檢測在其區域中是否存在某些東西,并在有東西進入或退出時通知感興趣的人。我們通過給它配置的UnityEvent
類型的字段onEnter及
onExit
,從UnityEngine.Events
命名空間。
using UnityEngine;
using UnityEngine.Events;
public class DetectionZone : MonoBehaviour {
[SerializeField]
UnityEvent onEnter = default, onExit = default;
}
void OnTriggerEnter (Collider other) {
onEnter.Invoke();
}
void OnTriggerExit (Collider other) {
onExit.Invoke();
}
檢查器會將組件的事件作為名為On Enter()和On Exit()的列表公開,這些列表最初是空的。名稱后面的括號中沒有任何內容,表示這些事件沒有參數。
材料選擇器
為了演示這是如何工作的,我們將創建一個簡單的MaterialSelector
組件類型,該組件類型具有可配置的材料和MeshRenderer
參考數組。它具有一個帶有index參數的Select公共方法,該方法將有效的材質分配給渲染器(如果有效)。
using UnityEngine;
public class MaterialSelector : MonoBehaviour {
[SerializeField]
Material[] materials = default;
[SerializeField]
MeshRenderer meshRenderer = default;
public void Select (int index) {
if (
meshRenderer && materials != null &&
index >= 0 && index < materials.Length
) {
meshRenderer.material = materials[index];
}
}
}
創建一個帶有紅色非活動區域和綠色活動區域的材質選擇器組件,這些組件將用于更改檢測區域的可視化。盡管不需要將其添加到受影響的游戲對象中,但這是最有意義的。
現在,通過按項目的+按鈕將其添加到檢測區域組件的輸入事件列表中。通過材質選擇器的左下角字段將游戲對象鏈接到該項目。之后,可以選擇MaterialSelector.Select
方法。由于此方法具有整數參數,因此其值將顯示在方法名稱下方。默認情況下,它設置為零,表示無效狀態,因此將其設置為1。然后對退出事件執行相同的操作,這次將參數保留為零。
確保默認情況下,區域對象使用不活動的紅色材料。然后以這種方式開始,但是一旦有物體進入區域,它將切換為活動的綠色材料。當有東西離開該區域時,它將再次變為紅色。
首次進入和最后退出
該檢測區域可以工作,但確實可以完成其編程的工作,即每次進入時調用一次進入,每次離開時調用一次退出。因此,我們可以混合使用enter和exit事件(例如enter,enter,exit,enter,exit,exit),并且當其中仍然有東西時,最終會出現視覺上無效的區域。在區域中保持活動狀態時,使區域保持活動狀態更加直觀。使用保證進入和退出事件將嚴格交替的區域進行設計也更加容易。因此,它僅應在第一件東西進入時和最后一件東西離開時發出信號。將事件重命名為onFirstEnter,并將onLastExit
其重命名以使其變得清晰,這將需要再次掛接事件。
為了使這種行為成為可能,我們必須跟蹤區域中當前的對撞機。我們將通過將DetectionZone命名空間中的List<Collider>
字段初始化為System.Collections.Generic新列表來完成此操作。
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
public class DetectionZone : MonoBehaviour {
[SerializeField]
UnityEvent onFirstEnter = default, onLastExit = default;
List<Collider> colliders = new List<Collider>();
…
}
請參閱“ 對象管理”系列的“ 持久對象”教程。
在OnTriggerEnter中
僅調用輸入事件如果列表為空,則始終對撞機添加到列表中,以保持它的軌道。
void OnTriggerEnter (Collider other) { if (colliders.Count == 0) { onFirstEnter.Invoke(); } colliders.Add(other); }
在這種情況下,我們將在OnTriggerExit中從列表刪除對撞機,僅當列表為空時才調用exit事件。列表的Remove
方法返回刪除是否成功。應當總是這樣,因為否則我們將無法跟蹤對撞機,但是我們仍然可以對其進行檢查。
void OnTriggerExit (Collider other) { if (colliders.Remove(other) && colliders.Count == 0) { onLastExit.Invoke(); } }
檢測出現和消失的對象
不幸的是,OnTriggerExit它是不可靠的,因為在停用,禁用或銷毀游戲對象或其對撞機時,不會調用它。不應該單獨禁用碰撞器,因為那樣會導致物體掉落到幾何體中,因此我們將不支持此功能。但是我們應該能夠處理整個游戲對象在區域內時被禁用或破壞的情況。
每個物理步驟,我們都必須檢查區域中的對撞機是否仍然有效。添加一個在對撞機列表中循環的FixedUpdate方法。如果對撞機進行評估,false
則意味著它或其游戲對象已被破壞。如果不是這種情況,我們必須檢查其游戲對象是否已停用,我們可以通過activeInHierarchy
其游戲對象的屬性來查找。如果對撞機不再有效,請從列表中將其刪除,并減少循環迭代器。如果列表為空,則調用exit事件。
void FixedUpdate () { for (int i = 0; i < colliders.Count; i++) { Collider collider = colliders[i]; if (!collider || !collider.gameObject.activeInHierarchy) { colliders.RemoveAt(i--); if (colliders.Count == 0) { onLastExit.Invoke(); } } } }
大多數情況下,檢測區域中可能沒有物體。為了避免不必要的FixedUpdate
連續調用,我們可以在喚醒組件時以及最后一個對撞機退出后禁用該組件。然后我們只有在有東西進入后才啟用它。之所以有效,是因為無論是否啟用行為,總是會觸發觸發器方法。
void Awake () {
enabled = false;
}
void FixedUpdate () {
for (int i = 0; i < colliders.Count; i++) {
Collider collider = colliders[i];
if (!collider || !collider.gameObject.activeInHierarchy) {
colliders.RemoveAt(i--);
if (colliders.Count == 0) {
onLastExit.Invoke();
enabled = false;
}
}
}
}
void OnTriggerEnter (Collider other) {
if (colliders.Count == 0) {
onFirstEnter.Invoke();
enabled = true;
}
colliders.Add(other);
}
void OnTriggerExit (Collider other) {
if (colliders.Remove(other) && colliders.Count == 0) {
onLastExit.Invoke();
enabled = false;
}
}
接下來,我們還應該處理區域游戲對象本身被停用或銷毀的情況,因為當事件仍在區域中時發生時,調用退出事件是有意義的。我們都可以通過添加OnDisable
清除列表并在列表不為空時調用exit事件的方法來做到。
void OnDisable () { if (colliders.Count > 0) { colliders.Clear(); onLastExit.Invoke(); } }
請注意,檢測區的組件不應由其他代碼禁用,因為它可以管理自己的狀態。一般規則是不要禁用檢測區域組件,也不要禁用任何可能影響該區域的對撞機。這些游戲對象應全部停用或銷毀。
熱裝
因為熱重載(在編輯器播放模式下重新編譯)OnDisable將被調用,因此它違反了我們剛剛聲明的規則。這將導致調用退出事件以響應熱重載,此后已存在于該區域中的對象將被忽略。幸運的是,我們可以檢測到OnDisable中的熱重裝。如果同時啟用了該組件并且游戲對象處于活動狀態,則我們將進行熱重載,并且什么也不做。當游戲對象沒有被銷毀而組件被銷毀時,情況也是如此,但是我們裁定不應該這樣做。
我們只需要在編輯器中播放時進行檢查,就可以將代碼包裝在#if UNITY_EDITOR
和中#endif
。
void OnDisable () {#if UNITY_EDITOR if (enabled && gameObject.activeInHierarchy) { return; }#endif if (colliders.Count > 0) { colliders.Clear(); onLastExit.Invoke(); } }
如果禁用了該組件,則將其禁用或禁用游戲對象,然后我們繼續進行。否則,如果游戲對象未處于活動狀態,則該游戲對象將被停用或銷毀,然后我們繼續進行。否則,它要么是熱裝,要么是僅組件被破壞,我們將其忽略。
更復雜的行為
這只是通過事件可以完成的簡單演示。您可以通過向事件列表中添加更多條目來創建更復雜的行為。您不必為此創建新方法,您可以使用現有方法。限制是它必須是與事件的參數列表匹配的void方法或屬性設置器,或者最多具有一個可序列化的參數。例如,我進行了一些設置,以便在檢測區域內有東西的同時關閉懸浮區域,除了更改區域本身的可視化效果之外。
您不必總是對所有事件都響應。您可能只有在進入或退出時才觸發某些事件。例如,在進入區域時激活某些內容。然后退出并不會取消激活它,而重新進入則會再次激活它,這無濟于事。
從理論上講,是的,這對于快速制作原型非常有用,但是卻很麻煩。一旦發現自己重復了一個復雜的模式,就可以為其創建專用的方法或行為,這應該更容易使用,并在以后必要時進行優化。
簡單運動
我們將在本教程中介紹的最后一種情況是移動環境對象。復雜的運動可以通過動畫來完成,可以通過檢測區域觸發。但是通常兩點之間的簡單線性插值就足夠了,例如,對于門,電梯或浮動平臺。因此,讓我們添加對此的支持。
自動滑塊
無論插值什么,它在概念上都由從0到1的滑塊控制。如何更改值是與插值本身不同的問題。保持滑塊分離還可以將其用于多個插值。因此,我們將創建一個AutomaticSlider
專用于此值的組件。它的可配置持續時間必須為正。當我們使用它為物理對象設置動畫時,我們將使其在FixedUpdate
方法中增加其值,并確保它不會過沖。一旦值達到1,我們就可以完成并可以禁用滑塊。
using UnityEngine;
public class AutomaticSlider : MonoBehaviour {
[SerializeField, Min(0.01f)]
float duration = 1f;
float value;
void FixedUpdate () {
value += Time.deltaTime / duration;
if (value >= 1f) {
value = 1f;
enabled = false;
}
}
}
再一次,我們將使用Unity事件來將行為附加到滑塊。在這種情況下,我們需要一個on-value-changed事件,該事件將用于傳遞滑塊的當前值。因此,我們的事件需要一個float
參數,我們可以為其使用UnityEvent<float>
類型。在FixedUpdate結束時調用事件。
using UnityEngine;
using UnityEngine.Events;
public class AutomaticSlider : MonoBehaviour {
…
[SerializeField]
UnityEvent<float> onValueChanged = default;
float value;
void FixedUpdate () {
…
onValueChanged.Invoke(value);
}
}
但是,Unity無法序列化通用事件類型,因此該事件不會顯示在檢查器中。我們必須創建自己的具體可序列化事件類型,該事件類型可以簡單地擴展UnityEvent<float>
。此類型特定于我們的滑塊,因此通過在類內部以及事件字段本身進行聲明將其設置為嵌套類型。
[System.Serializable]
public class OnValueChangedEvent : UnityEvent<float> { }
[SerializeField]
OnValueChangedEventonValueChanged = default;
進入播放模式時,滑塊將立即開始增加。如果您不希望這樣做,請在默認情況下將其禁用。然后,您可以將其連接到檢測區域,以在以后啟用它。
請注意,在這種情況下,事件的名稱后跟(Single),表示它具有一個參數。Single是指float
類型,它是單精度浮點數。
位置插補器
接下來,創建一個PositionInterpolator
組件類型,該組件類型Rigidbody
通過帶有float
參數的公共方法Interpolate在兩個可配置位置之間插值可配置位置。請使用Vector3.LerpUnclamped
以便提供的值不會受到限制,而將其留給調用者。我們必須通過其MovePosition
方法更改身體的位置,以便將其解釋為運動,否則將成為隱形傳送。
using UnityEngine;
public class PositionInterpolator : MonoBehaviour {
[SerializeField]
Rigidbody body = default;
[SerializeField]
Vector3 from = default, to = default;
public void Interpolate (float t) {
body.MovePosition(Vector3.LerpUnclamped(from, to, t));
}
}
通過將sider和interpolator都添加到同一平臺對象,我創建了一個簡單的移動平臺。內插器方法Interpolate的動態版本綁定到滑塊的事件,這就是為什么其值沒有字段的原因。然后,我將滑塊連接到檢測區域,以便在有物體進入該區域時激活平臺。請注意,插值點位于世界空間中。
自動倒車
我們可以通過向添加一個可配置的自動反向切換來使插值來回移動AutomaticSlider
。這需要我們跟蹤它是否被反轉,并將FixedUpdate中的代碼加倍,必須支持雙向。同樣,當自動反轉激活時,我們必須跳動而不是鉗制該值。在持續時間極短的情況下,這可能會導致過沖,因此反彈后我們仍然會鉗住。
[SerializeField]
bool autoReverse = false;
…
bool reversed;
void FixedUpdate () {
float delta = Time.deltaTime / duration;
if (reversed) {
value -= delta;
if (value <= 0f) {
if (autoReverse) {
value = Mathf.Min€(1f, -value);
reversed = false;
}
else {
value = 0f;
enabled = false;
}
}
}
else {
value +=delta;
if (value >= 1f) {
if (autoReverse) {
value = Mathf.Max(0f, 2f - value);
reversed = true;
}
else {
value = 1f;
enabled = false;
}
}
}
onValueChanged.Invoke(value);
}
平穩步伐
線性插值的運動是剛性的,反轉時速度會突然變化。通過將值的平滑變體傳遞給事件,我們可以使其加速和減速。我們通過應用smoothstep功能給它,這是3V 2 - 2V 3。使它成為可配置的選項。
[SerializeField]
bool autoReverse = false, smoothstep = false;
…
float SmoothedValue => 3f * value * value - 2f * value * value * value;
void FixedUpdate () {
…
onValueChanged.Invoke(smoothstep ? SmoothedValue :value);
}
更多控制
可以通過檢測區域事件禁用滑塊組件來暫停動畫,但是我們也可以控制其方向。最簡單的方法是通過公共屬性提供其反轉狀態。用自動Reversed
屬性替換該reversed字段,并調整其他代碼的大小寫以使其匹配。
//bool reversed; public bool Reversed { get; set; }
讓我們對自動反轉選項執行相同的操作。在這種情況下,我們必須保留序列化字段,因此添加一個顯式屬性。
public bool AutoReverse { get => autoReverse; set => autoReverse = value; }
請注意,方向反轉是突然的,因為它仍然是簡單的插值。如果要在任何時候平穩停止和反轉,則需要創建使用加速度和速度的更復雜的邏輯。
碰撞碰撞
移動風景的危險是,身體最終可能會陷入兩個接近的對撞機之間。當對撞機之間的縫隙關閉時,身體要么被彈出,要么最終被推入對撞機或通過對撞機。如果碰撞表面成一定角度,則存在清晰的逃生路徑,身體將朝該方向被推動。如果不是這樣,或者如果沒有足夠的時間逃脫,則身體最終會被壓碎,從而穿透對撞機。如果一個物體卡在兩個足夠厚的簡單對撞機之間,那么它可以留在它們內部,一旦有一條清晰的道路就會彈出。否則會掉下去。
如果碰撞表面成一定角度,則身體將被推到一邊,并且很有可能逃脫。因此,通過在表面之間留出足夠的空間或通過引入傾斜的對撞機(無論是否可見)來設計這樣的配置是一個好主意。此外,將盒子對撞機隱藏在地板上可以使它更牢固,以免物體被推過。或者,添加一個區域,在適當的時候觸發該區域的破壞,表示它被壓碎了。
局部插值
世界空間中的配置可能會帶來不便,因為它無法在多個位置用于同一動畫。因此,我們通過給PositionInterpolator添加一個本地空間選項來包裝一下。為此,我們添加了一個可選的可配置的相對于插值發生位置的Transform。通常用插值器引用對象,但這不是必需的。
[SerializeField]
Transform relativeTo = default;
public void Interpolate (float t) {
Vector3 p;
if (relativeTo) {
p = Vector3.LerpUnclamped(
relativeTo.TransformPoint(from), relativeTo.TransformPoint(to), t
);
}
else {
p = Vector3.LerpUnclamped(from, to, t);
}
body.MovePosition(p);
}
以上就是“Unity怎么實現有影響的球體運動”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。