如圖檔、視訊或代碼格式等顯示異常,請檢視原文:
https://mp.weixin.qq.com/s/Sv0FOxZCAHHUQPjT8rUeNw
很多童鞋沒有系統的Unity3D遊戲開發基礎,也不知道從何開始學。為此我們精選了一套國外優秀的Unity3D遊戲開發教程,翻譯整理後放送給大家,教您從零開始一步一步掌握Unity3D遊戲開發。 本文不是廣告,不是推廣,是免費的純幹貨!本文全名:喵的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,該方法以觸發主體作為參數調用新方法。進入該區域的所有物體都會發生這種情況,但是如果需要,您可以使用圖層來防止檢測到某些物體。喵的Unity遊戲開發之路 - 互動環境(有影響的運動) -
void OnTriggerEnter (Collider other) { Rigidbody body = other.attachedRigidbody; if (body) { Accelerate(body); } } void Accelerate(Rigidbody body) {}
隻需使身體速度的Y分量等于配置的速度,除非它已經更大。其他速度分量不受影響。Accelerate中
-
防止猛然掉地 當發射正常物體時,這種簡單的方法效果很好,但是我們的球體沒有正确發射。當它進入區域時,它似乎獲得了很大的前進速度。發生這種情況是因為我們将其卡在了地上。在這種情況下,可以通過降低“ 最大捕捉速度”來解決,但不适用于設定為低速的加速區域。為了防止接地,一般來說,我們必須訓示void Accelerate(Rigidbody body) { Vector3 velocity = body.velocity; if (velocity.y >= speed) { return; } velocity.y = speed; body.velocity = velocity; }
暫時不要執行接地。我們可以通過MovingSphere
向其添加設定PreventSnapToGround
為-1 的公共方法來做到這一點。stepsSinceLastJump
- 現在
可以在主體具有AccelerationZone.Accelerate
元件的情況下調用此方法,我們可以通過調用MovingSphere
球體作為輸出參數來進行檢查和檢索。TryGetComponent
- 請注意,這種方法不會重置跳躍階段,是以在沒有降落的情況下彈跳跳闆不會重新整理空氣跳躍。 持續加速 瞬時速度變化對于跳闆很合适,但是我們也可以使用該區域建立其他連續的加速度現象,例如懸浮區域。我們可以通過簡單地添加與OnTriggerStay相同的方法OnTriggerEnter來支援這一點。
- 如果效果持續時間較長,那麼通過适當的加速度來實作速度變化會更好一些,是以讓我們向該區域添加一個可配置的加速度,且最小值也應為零。如果将其設定為零,我們将立即進行更改,否則将應用加速。
-
也可以施加力,這樣品質較大的物體最終的加速度會變慢,但是固定的加速度使水準設計更容易,是以我使用了這一點。 任意方向 最後,為了使其可以向任何方向加速,請在Accelerate開始時将人體速度轉換為區域的局部空間,并在應用時将其轉換回世界空間。通過InverseTransformDirection和TransformDirection這樣做,是以區域的比例不會對其産生影響。現在可以通過旋轉區域來控制加速方向。[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; } … }
-
對存在做出反應 加速區隻是如何建立具有特定行為的觸發區的一個示例。如果您需要一個區域執行其他操作,則必須為其編寫新代碼。但是檢測和響應某處某物的存在的簡單行為是如此普遍,以至于我們理想情況下隻編寫一次。而且許多行為非常簡單(例如激活對象),以至于無法為其建立專用的元件類型。而更複雜的行為通常隻是一些簡單動作的組合。如果關卡設計師可以通過簡單地配置遊戲對象并添加一些元件來建立它,而不必一直建立專門的代碼,這将很友善。 檢測區 讓我們從建立一個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; }
檢查器會将元件的事件作為名為On Enter()和On Exit()的清單公開,這些清單最初是空的。名稱後面的括号中沒有任何内容,表示這些事件沒有參數。 材料選擇器 為了示範這是如何工作的,我們将建立一個簡單的void OnTriggerEnter (Collider other) { onEnter.Invoke(); } void OnTriggerExit (Collider other) { onExit.Invoke(); }
元件類型,該元件類型具有可配置的材料和MaterialSelector
參考數組。它具有一個帶有index參數的Select公共方法,該方法将有效的材質配置設定給渲染器(如果有效)。MeshRenderer
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命名空間中的
字段初始化為System.Collections.Generic新清單來完成此操作。List<Collider>
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中
方法傳回删除是否成功。應當總是這樣,因為否則我們将無法跟蹤對撞機,但是我們仍然可以對其進行檢查。Remove
不幸的是,OnTriggerExit它是不可靠的,因為在停用,禁用或銷毀遊戲對象或其對撞機時,不會調用它。不應該單獨禁用碰撞器,因為那樣會導緻物體掉落到幾何體中,是以我們将不支援此功能。但是我們應該能夠處理整個遊戲對象在區域内時被禁用或破壞的情況。
每個實體步驟,我們都必須檢查區域中的對撞機是否仍然有效。添加一個在對撞機清單中循環的FixedUpdate方法。如果對撞機進行評估,
則意味着它或其遊戲對象已被破壞。如果不是這種情況,我們必須檢查其遊戲對象是否已停用,我們可以通過false
其遊戲對象的屬性來查找。如果對撞機不再有效,請從清單中将其删除,并減少循環疊代器。如果清單為空,則調用exit事件。activeInHierarchy
連續調用,我們可以在喚醒元件時以及最後一個對撞機退出後禁用該元件。然後我們隻有在有東西進入後才啟用它。之是以有效,是因為無論是否啟用行為,總是會觸發觸發器方法。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; } }
清除清單并在清單不為空時調用exit事件的方法來做到。OnDisable
因為熱重載(在編輯器播放模式下重新編譯)OnDisable将被調用,是以它違反了我們剛剛聲明的規則。這将導緻調用退出事件以響應熱重載,此後已存在于該區域中的對象将被忽略。幸運的是,我們可以檢測到OnDisable中的熱重裝。如果同時啟用了該元件并且遊戲對象處于活動狀态,則我們将進行熱重載,并且什麼也不做。當遊戲對象沒有被銷毀而元件被銷毀時,情況也是如此,但是我們裁定不應該這樣做。
我們隻需要在編輯器中播放時進行檢查,就可以将代碼包裝在
和中#if UNITY_EDITOR
。#endif
OnDisable中相關的狀态組合是什麼?
如果禁用了該元件,則将其禁用或禁用遊戲對象,然後我們繼續進行。否則,如果遊戲對象未處于活動狀态,則該遊戲對象将被停用或銷毀,然後我們繼續進行。否則,它要麼是熱裝,要麼是僅元件被破壞,我們将其忽略。
更複雜的行為這隻是通過事件可以完成的簡單示範。您可以通過向事件清單中添加更多條目來建立更複雜的行為。您不必為此建立新方法,您可以使用現有方法。限制是它必須是與事件的參數清單比對的void方法或屬性設定器,或者最多具有一個可序列化的參數。例如,我進行了一些設定,以便在檢測區域内有東西的同時關閉懸浮區域,除了更改區域本身的可視化效果之外。
您不必總是對所有事件都響應。您可能隻有在進入或退出時才觸發某些事件。例如,在進入區域時激活某些内容。然後退出并不會取消激活它,而重新進入則會再次激活它,這無濟于事。
這種基于事件的方法可以用于整個遊戲嗎?
從理論上講,是的,這對于快速制作原型非常有用,但是卻很麻煩。一旦發現自己重複了一個複雜的模式,就可以為其建立專用的方法或行為,這應該更容易使用,并在以後必要時進行優化。
簡單運動 我們将在本教程中介紹的最後一種情況是移動環境對象。複雜的運動可以通過動畫來完成,可以通過檢測區域觸發。但是通常兩點之間的簡單線性插值就足夠了,例如,對于門,電梯或浮動平台。是以,讓我們添加對此的支援。 自動滑塊 無論插值什麼,它在概念上都由從0到1的滑塊控制。如何更改值是與插值本身不同的問題。保持滑塊分離還可以将其用于多個插值。是以,我們将建立一個
專用于此值的元件。它的可配置持續時間必須為正。當我們使用它為實體對象設定動畫時,我們将使其在AutomaticSlider
方法中增加其值,并確定它不會過沖。一旦值達到1,我們就可以完成并可以禁用滑塊。FixedUpdate
再一次,我們将使用Unity事件來将行為附加到滑塊。在這種情況下,我們需要一個on-value-changed事件,該事件将用于傳遞滑塊的目前值。是以,我們的事件需要一個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; } } }
參數,我們可以為其使用float
類型。在FixedUpdate結束時調用事件。UnityEvent<float>
但是,Unity無法序列化通用事件類型,是以該事件不會顯示在檢查器中。我們必須建立自己的具體可序列化事件類型,該事件類型可以簡單地擴充using UnityEngine; using UnityEngine.Events; public class AutomaticSlider : MonoBehaviour { … [SerializeField] UnityEvent<float> onValueChanged = default; float value; void FixedUpdate () { … onValueChanged.Invoke(value); } }
。此類型特定于我們的滑塊,是以通過在類内部以及事件字段本身進行聲明将其設定為嵌套類型。UnityEvent<float>
[System.Serializable] public class OnValueChangedEvent : UnityEvent<float> { } [SerializeField] OnValueChangedEventonValueChanged = default;
進入播放模式時,滑塊将立即開始增加。如果您不希望這樣做,請在預設情況下将其禁用。然後,您可以将其連接配接到檢測區域,以在以後啟用它。
請注意,在這種情況下,事件的名稱後跟(Single),表示它具有一個參數。Single是指
類型,它是單精度浮點數。 位置插補器 接下來,建立一個float
元件類型,該元件類型PositionInterpolator
通過帶有Rigidbody
參數的公共方法Interpolate在兩個可配置位置之間插值可配置位置。請使用float
以便提供的值不會受到限制,而将其留給調用者。我們必須通過其Vector3.LerpUnclamped
方法更改身體的位置,以便将其解釋為運動,否則将成為隐形傳送。MovePosition
通過将sider和interpolator都添加到同一平台對象,我建立了一個簡單的移動平台。内插器方法Interpolate的動态版本綁定到滑塊的事件,這就是為什麼其值沒有字段的原因。然後,我将滑塊連接配接到檢測區域,以便在有物體進入該區域時激活平台。請注意,插值點位于世界空間中。 自動倒車 我們可以通過向添加一個可配置的自動反向切換來使插值來回移動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)); } }
。這需要我們跟蹤它是否被反轉,并将FixedUpdate中的代碼加倍,必須支援雙向。同樣,當自動反轉激活時,我們必須跳動而不是鉗制該值。在持續時間極短的情況下,這可能會導緻過沖,是以反彈後我們仍然會鉗住。AutomaticSlider
平穩步伐 線性插值的運動是剛性的,反轉時速度會突然變化。通過将值的平滑變體傳遞給事件,我們可以使其加速和減速。我們通過應用smoothstep功能給它,這是3V 2 - 2V 3。使它成為可配置的選項。[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); }
更多控制 可以通過檢測區域事件禁用滑塊元件來暫停動畫,但是我們也可以控制其方向。最簡單的方法是通過公共屬性提供其反轉狀态。用自動[SerializeField] bool autoReverse = false, smoothstep = false; … float SmoothedValue => 3f * value * value - 2f * value * value * value; void FixedUpdate () { … onValueChanged.Invoke(smoothstep ? SmoothedValue :value); }
屬性替換該reversed字段,并調整其他代碼的大小寫以使其比對。Reversed
移動風景的危險是,身體最終可能會陷入兩個接近的對撞機之間。當對撞機之間的縫隙關閉時,身體要麼被彈出,要麼最終被推入對撞機或通過對撞機。如果碰撞表面成一定角度,則存在清晰的逃生路徑,身體将朝該方向被推動。如果不是這樣,或者如果沒有足夠的時間逃脫,則身體最終會被壓碎,進而穿透對撞機。如果一個物體卡在兩個足夠厚的簡單對撞機之間,那麼它可以留在它們内部,一旦有一條清晰的道路就會彈出。否則會掉下去。
如果碰撞表面成一定角度,則身體将被推到一邊,并且很有可能逃脫。是以,通過在表面之間留出足夠的空間或通過引入傾斜的對撞機(無論是否可見)來設計這樣的配置是一個好主意。此外,将盒子對撞機隐藏在地闆上可以使它更牢固,以免物體被推過。或者,添加一個區域,在适當的時候觸發該區域的破壞,表示它被壓碎了。
局部插值 世界空間中的配置可能會帶來不便,因為它無法在多個位置用于同一動畫。是以,我們通過給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); }
想知道下一個教程何時釋出嗎?關注微信公衆号(u3dnotes)吧!
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-10-reactive-environment/
往期精選
Unity3D遊戲開發中100+效果的實作和源碼大全 - 收藏起來肯定用得着
Shader學習應該如何切入?
UE4 開發從入門到入土
聲明:釋出此文是出于傳遞更多知識以供交流學習之目的。若有來源标注錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯系,我們将及時更正、删除,謝謝。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/reactive-environment/
翻譯、編輯、整理:MarsZhou
More:【微信公衆号】 u3dnotes