目錄
1 加速區域
1.1 Zone 組件
1.2 阻止檢測地面
1.3 持續加速
1.4 任意方向
2 意識到存在
2.1 檢測區域
2.2 材質選擇
2.3 最開始進入和最後退出
2.4 檢測突然出現和消失的物體
2.5 熱重載
2.6 更複雜的行為
3 簡單運動
3.1 自動滑動條
3.2 位置插值
3.3 自動倒置
3.4 平滑步長
3.5 更多控制
3.6 壓碎的碰撞體
3.7 局部插值
本文重點內容:
1、通過加速區域創建跳板和浮空
2、製作一個多功能區域
3、不同材質的交互以及關閉或者激活對象
4、通過事件觸發簡單對象插值運動
這是關於控制角色移動的教程系列的第十期。它讓環境可以以各種方式和對象運動產生交互。
本教程是CatLikeCoding系列的一部分,原文地址見文章底部。本教程使用Unity 2019.4.4f1製作。它還使用ProBuilder軟體包。
(和環境交互)
1 加速區域
一個活躍的環境比一個靜態的環境更有意思,特別是它們還能對正在發生的行為做出反應的時候。這個行為表示可以對任何事情做出反應,也可以做任何事情,但是一個簡單的例子是類似於跳板的東西:每當有東西落在跳板上時,它就會向上彈起。這可以是我們運動的球體,也可以是其他掉落或被推到跳板上的物體。因此,該行為在邏輯上屬於跳板。其他物體不需要意識到它的存在,它們只是突然被彈飛起來了。
1.1 Zone 組件
描述跳板行為的最通用方法是,它是一個區域,可加速進入區域的任何物體。因此,我們將創建AccelerationZone組件類型,其可配置的速度不能為負。
區域可以通過添加一個帶有觸發器碰撞器的對象到場景中來創建,然後將 zone behavior 附加到它上。你也可以添加可視化的跳板對象,但是我只是用半透明的黃色材質使區域可見。
(Acceleration zone 組件)
當具有剛體的物體進入區域時,我們應該對其進行加速。為此添加一個OnTriggerEnter方法,該方法將觸發並調用新的Accelerate方法。進入該區域的所有物體都被執行,但是如果需要的話,可以使用Layer來防止檢測到不需要的處理的物體。
(在區域中的物體被推開)
1.2 阻止檢測地面
這種簡單的方法在發射常規物體時效果很好,但是我們的球體卻沒有正確發射。相反,它進入該區域時似乎獲得了很大的前進速度。發生這種情況是因為我們將其壓在了地面上。在這種情況下,可以通過降低「Max Snap Speed 」來解決,但這種方法不適用於設置為低速的加速區域。通常,為了防止被地面捕捉,我們必須指示MovingSphere暫時不要執行捕捉。為此,我們可以向其添加一個公共的PreventSnapToGround方法,該方法將stepsSinceLastJump設置為-1。
現在,如果物體具有MovingSphere組件,則AccelerationZone.Accelerate可以調用此方法,我們可以通過使用Sphere作為輸出參數調用TryGetComponent來進行檢查和檢索。
(發射)
請注意,這種方法不會重置跳躍階段,因此在沒有著陸的情況下彈跳跳板不會刷新空氣跳躍。
1.3 持續加速
瞬時速度變化對於跳板很合適,但是我們也可以使用該區域創建其他連續的加速度現象,例如懸浮區域。我們可以通過簡單地添加一個與OnTriggerEnter相同的OnTriggerStay方法來支持這個特性。
如果效果持續時間較長,那麼通過適當的加速度來實現速度變化會更好一些,因此讓我們向該區域添加一個可配置的加速度,最小還是為零。如果將其設置為零,我們將立即進行更改,否則將應用加速。
(升空區域 air加速度為1)
也可以施加力,這樣質量較大的物體最終加速得較慢,但是固定的加速度使關卡設計變得更容易,因此我使用這個方式。
1.4 任意方向
最後,為了使其可以在任何方向上加速,請在「Accelerate」開始時將體速度轉換為區域的局部空間,並在應用時將其轉換回世界空間。使用InverseTransformDirection和TransformDirection進行此操作,以便區域的比例不會對其產生影響。現在可以通過旋轉區域來控制加速度方向。
(跳躍區域之間的彈跳)
2 意識到存在
加速區域只是如何創建具有特定行為的觸發區域的一個示例。如果你需要一個做其他事情的區域,你將不得不為它編寫新的代碼。但是,檢測和響應某個地方出現的某些東西的簡單行為是如此普遍,我們理想情況下只想編寫一次。有很多行為非常簡單,比如只是激活一個對象,就為它創建一個專用的組件類型可能就有些設計過渡了。更複雜的行為通常只是幾個簡單動作的組合。如果關卡設計師可以通過簡單的對象來創建它,那會是非常方便的。
2.1 檢測區域
讓我們首先創建一個DetectionZone組件,該組件檢測其區域中是否存在某些東西,並在有物體進入或退出時通知感興趣的模塊。我們通過從UnityEngine.Events命名空間為它提供類型為UnityEvent的onEnter和onExit欄位進行配置來實現。
只需讓它在OnTriggerEnter和OnTriggerExit中的適當事件上調用Invoke方法。這將觸發對事件註冊的所有內容的方法調用。
檢查器會將組件的事件作為名為On Enter()和On Exit()的列表公開,這些列表最初是空的。名稱後面的括號中沒有任何內容,表示這些事件沒有參數。
(沒有事件)
2.2 材質選擇
為了演示其工作原理,我們將創建一個簡單的MaterialSelector組件類型,該組件類型具有可配置的材質數組和MeshRenderer參考。它具有一個帶有索引參數的公共Select方法,該方法將有效的材質分配給渲染器(如果有效的話)。
創建一個帶有紅色非活動區域和綠色活動區域的材質選擇器組件,這將用於更改檢測區域的可視化。雖然不需要將其添加到受影響的遊戲對象中,但這仍然是有意義的。
(材質選擇器)
現在,通過按項目的+按鈕將其添加到檢測區域組件的輸入事件列表中。通過材質選擇器的左下角欄位將遊戲對象連結到該項目。之後,可以選擇MaterialSelector.Select方法。由於此方法具有整數參數,因此其值將顯示在方法名稱下方。默認情況下,它設置為零,表示不活動狀態,因此將其設置為1。然後對退出事件執行相同的操作,這次將參數保留為零。
(設置材質)
區域對象默認使用不活動的紅色材質。只要有物體進入區域,將切換材質到綠色。當有東西離開這個區域時,它又會變成紅色。
(和檢測區域的交互)
2.3 最開始進入和最後退出
該檢測區域可以工作,並確實可以完成其編程的目的,即每次進入時調用一次進入,每次離開時調用一次退出。因此,我們可以混合使用enter和exit事件(例如enter,enter,exit,enter,exit,exit),並且當其中仍然有東西時,最終會出現視覺上無效的區域。在區域中保持活動狀態時,使區域保持活動狀態更加直觀。使用保證進入和退出事件將嚴格交替的區域進行設計也更加容易。因此,它僅應在第一件東西進入時和最後一件東西離開時發出信號。重構事件重命名為onFirstEnter和onLastExit可以使這一點變得清晰,這將需要再次連接事件。
(重命名事件)
為了使這種行為成為可能,我們必須跟蹤區域中當前的碰撞體。通過為DetectionZone提供一個List 欄位(從System.Collections.Generic命名空間初始化為新列表)來完成此操作。
該列表如何工作?
請參閱「對象管理」系列的「持久對象」教程。
在OnTriggerEnter中,只有在列表為空時才調用enter事件,然後始終將碰撞器添加到列表中以跟蹤它。
在OnTriggerExit中,我們從列表中移除碰撞器,並且只有在列表為空時才調用退出事件 列表的Remove方法返回刪除是否成功 這應該總是這樣的,因為否則我們就無法追蹤碰撞器。
(只要有物體在區域就保持激活狀態)
2.4 檢測突然出現和消失的物體
不幸的是,OnTriggerExit不可靠,因為在停用,禁用或銷毀遊戲對象或其碰撞器時便不會再調用它。不應該單獨禁用碰撞器,因為那樣會導致物體掉落到幾何體中,因此我們將不支持這種方法。但是我們應該能夠處理整個遊戲對象在區域內時被禁用或銷毀的情況。
在每一個物理步長中,我們都要檢查區域內的碰撞器是否仍然有效。添加一個在碰撞器列表中循環的FixedUpdate方法。如果一個碰撞器計算為false,這意味著它或它的遊戲對象已經被銷毀。如果不是的話,我們就需要檢查它的遊戲對象是否被禁用了,這一點我們可以通過它的遊戲對象的active屬性來發現。如果碰撞器不再有效,則將其從列表中刪除並遞減循環迭代器。如果列表為空,則調用退出事件。
大多數情況下,檢測區域中沒有物體。為了避免不必要地連續調用FixedUpdate,我們可以在組件喚醒時和最後一個碰撞器退出後禁用該組件。然後我們只有在有東西進入後才啟用它。之所以這樣有效,是因為無論是否啟用行為,總是會觸發觸發器方法。
接下來,我們還應該處理區域對象自身被停用或銷毀的情況,因為當事件仍在區域中時發生時,調用退出事件是有意義的。我們都可以通過添加一個OnDisable方法來完成這兩項工作,該方法清除列表並在列表不為空時調用exit事件。
請注意,檢測區的組件不應由其他代碼禁用,因為它可以管理自己的狀態。一般規則是不要禁用檢測區域組件,也不要禁用任何可能影響該區域的碰撞器。這些遊戲對象應全部停用或銷毀。
2.5 熱重載
因為熱重載(在編輯器播放模式下重新編譯)將調用OnDisable,所以它違反了我們剛剛聲明的規則。這將導致退出事件被調用以響應熱重載,此後已經在區域中的對象會被忽略。幸運的是,我們可以在OnDisable中檢測到熱重載。如果同時啟用了該組件並且遊戲對象處於活動狀態,則我們將進行熱重載,並且什麼也不做。當遊戲對象沒有被銷毀而組件被銷毀時,情況也是如此,但是我們仍然什麼都不做。
我們只需要在編輯器中播放時進行檢查,就可以將代碼包裝在#if UNITY_EDITOR和#endif中。
OnDisable中有哪些相關狀態組合?
如果禁用了該組件,僅僅是禁用或反激活遊戲對象,則應該繼續進行。否則,如果遊戲對象未處於活動狀態,則該遊戲對象將被停用或銷毀,應該繼續。否則,要麼是熱重載,要麼是僅組件被銷毀,則將其忽略。
2.6 更複雜的行為
這只是通過事件可以完成的簡單演示。你可以通過將更多條目添加到事件列表來創建更複雜的行為。甚至不必為此創建新方法,直接使用現有方法。而限制則是它必須是與事件的參數列表匹配的無效方法或屬性設置器,或者最多具有一個可序列化的參數。例如,我進行了一些設置,以便在更改檢測區域本身的可視化效果的同時,在檢測區域內有東西時關閉懸浮區域。
(切換懸浮區域)
您必總是對所有事件都響應。有時候可能只有在進入或退出時才觸發某些事件。例如,在進入區域時激活某些內容。然後退出並不會取消激活它,而重新進入則會再次激活它,雖然二級激活實際上沒有任何用處。
這種基於事件的方法可以用於整個遊戲嗎?
從理論上講,是的,它對於快速原型製作非常有用,但是卻很麻煩。一旦發現自己重複了複雜的模式,便有必要為其創建專用的方法或行為,這種方法或方法應該更容易使用,並在以後必要時進行優化。
3 簡單運動
我們將在本教程中介紹的最後一種情況是移動環境對象。複雜的運動可以通過動畫來完成,可以通過檢測區域觸發。但是通常兩點之間的簡單線性插值就足夠了,例如,對於門,電梯或浮動平臺。現在,讓我們添加對此的支持。
3.1 自動滑動條
無論插值什麼,它在概念上都由從0到1的滑塊控制。如何更改值是與插值本身不同的問題。保持滑塊分離還可以將其用於多個插值。因此,我們將創建一個專用於該值的AutomaticSlider組件。它的可配置持續時間必須為正。當我們使用它為物理對象設置動畫時,我們將使其在FixedUpdate方法中增加其值,並確保它不會溢出。一旦值達到1,我們就可以完成並可以禁用滑塊。
再一次,我們將使用Unity事件使它能夠附加行為到滑動條。在本例中,我們需要一個隨值變化的事件,我們將使用它來傳遞滑塊的當前值。所以我們的事件需要一個浮點參數,可以使用UnityEvent類型。在FixedUpdate結束時調用事件。
但是,Unity無法序列化通用事件類型,因此該事件不會顯示在檢查器中。我們必須創建自己的具體可序列化事件類型,該事件類型只是擴展UnityEvent。此類型特定於我們的滑塊,因此可以通過在類內部以及事件欄位本身進行聲明來使其成為嵌套類型。
進入播放模式時,滑塊將立即開始增加。如果你不希望這樣做,請在默認情況下將其禁用。然後,你可以將其連接到檢??測區域,以在以後啟用它。
(禁用具有值更改事件的滑塊)
請注意,在這種情況下,事件的名稱後跟(Single),表示它具有一個參數。單精度是指浮點類型,它是單精度浮點數。
3.2 位置插值
接下來,創建一個PositionInterpolator組件類型,該類型通過帶有float參數的公共Interpolate方法在兩個可配置位置之間插值可配置剛體的位置。使用Vector3.LerpUnclamped,以使提供的值不會被鉗位,而是由調用者決定。我們需要通過其MovePosition方法更改身體的位置,以便將其解釋為運動,否則將成為閃現。
(位置插值和滑塊相連接)
通過將sider和interpolator都添加到同一平臺對象,我創建了一個簡單的移動平臺。插值器的Interpolate方法的動態版本綁定到滑塊的事件,這就是為什麼其值沒有欄位的原因。然後,我將滑塊連接到檢測區域,以便在有物體進入該區域時激活平臺。請注意,插值點在世界空間中。
(激活移動的平臺)
3.3 自動倒置
我們可以通過向AutomaticSlider添加可配置的自動反向切換來使插值來回移動。這需要我們跟蹤它是否反轉,並在FixedUpdate中加倍代碼,同時必須支持雙向。同樣,當自動反轉激活時,我們必須跳動而不是鉗制該值。在持續時間極短的情況下,這可能會導致溢出,因此反彈後我們仍然會鉗住。
(自動升降的平臺)
3.4 平滑步長
線性插值的運動是剛性的,反轉時速度會突然變化。通過將值的平滑變體傳遞給事件,可以使其加速和減速。通過對其應用smoothstep函數來實現。並使它成為可配置的選項。
(線性VS平滑)
(開啟了平滑步長的平臺)
3.5 更多控制
可以通過檢測區域事件,並禁用滑塊組件來暫停動畫,但讓我們也可以控制其方向。最簡單的方法是通過公共屬性提供其反轉狀態。將反向欄位替換為自動反向屬性,調整其他代碼的大小寫以使其匹配。
讓我們對自動反轉選項執行相同的操作。在這種情況下,我們必須保留序列化欄位,因此添加一個顯式屬性。
(更複雜的平臺控制)
請注意,方向反轉是突然的,因為它仍然是簡單的插值。如果要在任何時候平穩停止和反轉,則需要創建使用加速度和速度的更複雜的邏輯。
3.6 壓碎的碰撞體
移動場景的危險在於,物體最終可能會陷入兩個接近的碰撞器之間。當碰撞器之間的縫隙關閉時,身體要麼被彈出,要麼最終被壓入碰撞器或穿過碰撞器。如果碰撞表面成一定角度,則存在清晰的逃生路徑,物體將朝該方向被推動。如果不是這樣,或者如果沒有足夠的時間逃脫,則物體最終會被壓碎,穿透碰撞體。如果一個物體卡在兩個足夠厚的簡單碰撞器之間,那麼它可以留在它們內部,一旦有一條清晰的道路就彈出。否則會掉下去。
(物體被壓入地表內了)
如果碰撞表面成一定角度,則物體會被推到一邊,並且很有可能逃脫。因此,通過在表面之間留出足夠的空間或通過引入傾斜的碰撞器(無論是否可見)來設計這樣的配置是一個好主意。此外,將box碰撞器隱藏在地板上可以使它更牢固,以免物體被推入。或者,添加一個區域,在適當的時候觸發該區域的銷毀,表示它被壓碎了。
(帶有角度的碰撞器,並且地表下面隱藏了盒碰撞器)
3.7 局部插值
世界空間中的配置可能會帶來不便,因為它無法在多個位置用於同一動畫。因此,讓我們通過在PositionInterpolator中添加一個局部空間選項進行總結。為此,我們添加了一個可選的可配置的Transform,該插值相對於應該發生的插值。通常用插值器引用對象,但這不是必需的。
(相對插值讓復用成為可能)
下一章節,滾動。
本文翻譯自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials