如圖片空幻、視頻或代碼格式等顯示異常憎茂,請(qǐng)查看原文:
https://mp.weixin.qq.com/s/Sv0FOxZCAHHUQPjT8rUeNw
????????很多童鞋沒有系統(tǒng)的Unity3D游戲開發(fā)基礎(chǔ)侨嘀,也不知道從何開始學(xué)榜跌。為此我們精選了一套國(guó)外優(yōu)秀的Unity3D游戲開發(fā)教程郭宝,翻譯整理后放送給大家重虑,教您從零開始一步一步掌握Unity3D游戲開發(fā)。?本文不是廣告廷支,不是推廣频鉴,是免費(fèi)的純干貨!本文全名:喵的Unity游戲開發(fā)之路 - 移動(dòng)?- 互動(dòng)環(huán)境?- 有影響的運(yùn)動(dòng)
通過加速區(qū)創(chuàng)建跳板和懸浮力。
制作一個(gè)多功能檢測(cè)區(qū)。
反應(yīng)性地交換材料并激活或停用對(duì)象攒暇。
通過事件觸發(fā)的簡(jiǎn)單插值移動(dòng)對(duì)象。
這是有關(guān)控制角色移動(dòng)的教程系列的第十期周荐。它使環(huán)境能夠以各種方式對(duì)運(yùn)動(dòng)做出反應(yīng)。
本教程使用Unity 2019.4.4f1制作悯姊。它還使用ProBuilder軟件包羡藐。
效果之一
修正
我改進(jìn)了軌道攝像機(jī)的1.4節(jié)“使焦點(diǎn)居中”,以便更好地實(shí)現(xiàn)焦點(diǎn)居中和焦點(diǎn)半徑限制的相互作用悯许。調(diào)整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;
}
}
我還更改了“移動(dòng)地面”部分2.3確定運(yùn)動(dòng),因此忽略了質(zhì)量較輕的連接物體辉阶。這樣可以防止球體自動(dòng)跟隨其推開的輕物體先壕。如下調(diào)整MovingSphere.UpdateState結(jié)尾:
if (connectedBody) {
if (connectedBody.isKinematic || connectedBody.mass >= body.mass) {
UpdateConnectionState();
}
}
最后,更改了“攀爬”第2.5節(jié)的“可選攀登”谆甜,以防止自動(dòng)粘在動(dòng)畫的可攀爬表面上垃僚。這是通過在EvaluateCollision中調(diào)節(jié)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;
}
…
}
(另一個(gè))效果
加速區(qū)
主動(dòng)環(huán)境比靜態(tài)環(huán)境有趣,尤其是當(dāng)它對(duì)正在發(fā)生的事情做出反應(yīng)時(shí)规辱。這種行為可以對(duì)任何事情做出反應(yīng)谆棺,也可以做任何事情,但是一個(gè)簡(jiǎn)單的例子就是跳墊:只要有東西落在墊上,它就會(huì)向上發(fā)射改淑。這可能是我們的運(yùn)動(dòng)球或碰巧掉落或推到墊子上的任何其他物體碍岔。因此,該行為在邏輯上屬于跳板朵夏。其他物體不必知道它的存在蔼啦,它們只是突然結(jié)束飛行。
區(qū)域組成
描述跳板行為的最通用方法是仰猖,該區(qū)域可加速進(jìn)入其的任何物體捏肢。因此,我們將創(chuàng)建一個(gè)AccelerationZone
組件類型饥侵,其可配置的速度不能為負(fù)鸵赫。
using UnityEngine;
public class AccelerationZone : MonoBehaviour {
[SerializeField, Min(0f)]
float speed = 10f;
}
可以通過將具有觸發(fā)對(duì)撞器的對(duì)象添加到場(chǎng)景,然后將區(qū)域行為附加到場(chǎng)景來創(chuàng)建區(qū)域躏升。您也可以添加可視化跳板的對(duì)象奉瘤,但是我只是用半透明的黃色材料使該區(qū)域可見。
當(dāng)帶Rigidbody的東西進(jìn)入?yún)^(qū)域時(shí)煮甥,我們應(yīng)該加速它盗温。為此在OnTriggerEnter添加一個(gè)方法Accelerate,該方法以觸發(fā)主體作為參數(shù)調(diào)用新方法成肘。進(jìn)入該區(qū)域的所有物體都會(huì)發(fā)生這種情況卖局,但是如果需要,您可以使用圖層來防止檢測(cè)到某些物體双霍。
void OnTriggerEnter (Collider other) {
Rigidbody body = other.attachedRigidbody;
if (body) {
Accelerate(body);
}
}
void Accelerate(Rigidbody body) {}
Accelerate中
只需使身體速度的Y分量等于配置的速度砚偶,除非它已經(jīng)更大。其他速度分量不受影響洒闸。
void Accelerate(Rigidbody body) {
Vector3 velocity = body.velocity;
if (velocity.y >= speed) {
return;
}
velocity.y = speed;
body.velocity = velocity;
}
防止猛然掉地
當(dāng)發(fā)射常規(guī)物體時(shí)染坯,這種簡(jiǎn)單的方法效果很好,但是我們的球體沒有正確發(fā)射丘逸。當(dāng)它進(jìn)入?yún)^(qū)域時(shí)单鹿,它似乎獲得了很大的前進(jìn)速度。發(fā)生這種情況是因?yàn)槲覀儗⑵淇ㄔ诹说厣仙罡佟T谶@種情況下仲锄,可以通過降低“ 最大捕捉速度”來解決,但不適用于設(shè)置為低速的加速區(qū)域湃鹊。為了防止接地儒喊,一般來說,我們必須指示MovingSphere
暫時(shí)不要執(zhí)行接地币呵。我們可以通過PreventSnapToGround
向其添加設(shè)置stepsSinceLastJump
為-1 的公共方法來做到這一點(diǎn)怀愧。
public void PreventSnapToGround () {
stepsSinceLastJump = -1;
}
現(xiàn)在AccelerationZone.Accelerate
可以在主體具有MovingSphere
組件的情況下調(diào)用此方法,我們可以通過調(diào)用TryGetComponent
球體作為輸出參數(shù)來進(jìn)行檢查和檢索。
void Accelerate(Rigidbody body) {
…
if (body.TryGetComponent(out MovingSphere sphere)) {
sphere.PreventSnapToGround();
}
}
請(qǐng)注意芯义,這種方法不會(huì)重置跳躍階段哈垢,因此在沒有降落的情況下彈跳跳板不會(huì)刷新空氣跳躍。
持續(xù)加速
瞬時(shí)速度變化對(duì)于跳板很合適毕贼,但是我們也可以使用該區(qū)域創(chuàng)建其他連續(xù)的加速度現(xiàn)象温赔,例如懸浮區(qū)域。我們可以通過簡(jiǎn)單地添加與OnTriggerStay相同的方法OnTriggerEnter來支持這一點(diǎn)鬼癣。
void OnTriggerStay (Collider other) {
Rigidbody body = other.attachedRigidbody;
if (body) {
Accelerate(body);
}
}
如果效果持續(xù)時(shí)間較長(zhǎng)陶贼,那么通過適當(dāng)?shù)募铀俣葋韺?shí)現(xiàn)速度變化會(huì)更好一些,因此讓我們向該區(qū)域添加一個(gè)可配置的加速度待秃,且最小值也應(yīng)為零拜秧。如果將其設(shè)置為零,我們將立即進(jìn)行更改章郁,否則將應(yīng)用加速枉氮。
[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;
}
…
}
也可以施加力,這樣質(zhì)量較大的物體最終的加速度會(huì)變慢暖庄,但是固定的加速度使水平設(shè)計(jì)更容易聊替,因此我使用了這一點(diǎn)。
任意方向
最后培廓,為了使其可以向任何方向加速惹悄,請(qǐng)?jiān)贏ccelerate開始時(shí)將人體速度轉(zhuǎn)換為區(qū)域的局部空間,并在應(yīng)用時(shí)將其轉(zhuǎn)換回世界空間肩钠。通過InverseTransformDirection和TransformDirection這樣做泣港,因此區(qū)域的比例不會(huì)對(duì)其產(chǎn)生影響。現(xiàn)在可以通過旋轉(zhuǎn)區(qū)域來控制加速方向价匠。
void Accelerate(Rigidbody body) {
Vector3 velocity =transform.InverseTransformDirection(body.velocity);
…
body.velocity =transform.TransformDirection(velocity);
…
}
對(duì)存在做出反應(yīng)
加速區(qū)只是如何創(chuàng)建具有特定行為的觸發(fā)區(qū)的一個(gè)示例当纱。如果您需要一個(gè)區(qū)域執(zhí)行其他操作,則必須為其編寫新代碼踩窖。但是檢測(cè)和響應(yīng)某處某物的存在的簡(jiǎn)單行為是如此普遍坡氯,以至于我們理想情況下只編寫一次。而且許多行為非常簡(jiǎn)單(例如激活對(duì)象)毙石,以至于無法為其創(chuàng)建專用的組件類型廉沮。而更復(fù)雜的行為通常只是一些簡(jiǎn)單動(dòng)作的組合。如果關(guān)卡設(shè)計(jì)師可以通過簡(jiǎn)單地配置游戲?qū)ο蟛⑻砑右恍┙M件來創(chuàng)建它徐矩,而不必一直創(chuàng)建專門的代碼,這將很方便叁幢。
檢測(cè)區(qū)
讓我們從創(chuàng)建一個(gè)DetectionZone
組件開始滤灯,該組件檢測(cè)在其區(qū)域中是否存在某些東西,并在有東西進(jìn)入或退出時(shí)通知感興趣的人。我們通過給它配置的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();
}
檢查器會(huì)將組件的事件作為名為On Enter()和On Exit()的列表公開,這些列表最初是空的豫尽。名稱后面的括號(hào)中沒有任何內(nèi)容篙梢,表示這些事件沒有參數(shù)。
材料選擇器
為了演示這是如何工作的美旧,我們將創(chuàng)建一個(gè)簡(jiǎn)單的MaterialSelector
組件類型渤滞,該組件類型具有可配置的材料和MeshRenderer
參考數(shù)組。它具有一個(gè)帶有index參數(shù)的Select公共方法榴嗅,該方法將有效的材質(zhì)分配給渲染器(如果有效)妄呕。
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];
}
}
}
創(chuàng)建一個(gè)帶有紅色非活動(dòng)區(qū)域和綠色活動(dòng)區(qū)域的材質(zhì)選擇器組件,這些組件將用于更改檢測(cè)區(qū)域的可視化嗽测。盡管不需要將其添加到受影響的游戲?qū)ο笾行骼@是最有意義的。
現(xiàn)在唠粥,通過按項(xiàng)目的+按鈕將其添加到檢測(cè)區(qū)域組件的輸入事件列表中疏魏。通過材質(zhì)選擇器的左下角字段將游戲?qū)ο箧溄拥皆擁?xiàng)目。之后晤愧,可以選擇MaterialSelector.Select
方法大莫。由于此方法具有整數(shù)參數(shù),因此其值將顯示在方法名稱下方养涮。默認(rèn)情況下葵硕,它設(shè)置為零,表示無效狀態(tài)贯吓,因此將其設(shè)置為1懈凹。然后對(duì)退出事件執(zhí)行相同的操作,這次將參數(shù)保留為零悄谐。
確保默認(rèn)情況下介评,區(qū)域?qū)ο笫褂貌换顒?dòng)的紅色材料。然后以這種方式開始爬舰,但是一旦有物體進(jìn)入?yún)^(qū)域们陆,它將切換為活動(dòng)的綠色材料。當(dāng)有東西離開該區(qū)域時(shí)情屹,它將再次變?yōu)榧t色坪仇。
首次進(jìn)入和最后退出
該檢測(cè)區(qū)域可以工作,但確實(shí)可以完成其編程的工作垃你,即每次進(jìn)入時(shí)調(diào)用一次進(jìn)入椅文,每次離開時(shí)調(diào)用一次退出喂很。因此,我們可以混合使用enter和exit事件(例如enter皆刺,enter少辣,exit,enter羡蛾,exit漓帅,exit),并且當(dāng)其中仍然有東西時(shí)痴怨,最終會(huì)出現(xiàn)視覺上無效的區(qū)域忙干。在區(qū)域中保持活動(dòng)狀態(tài)時(shí),使區(qū)域保持活動(dòng)狀態(tài)更加直觀腿箩。使用保證進(jìn)入和退出事件將嚴(yán)格交替的區(qū)域進(jìn)行設(shè)計(jì)也更加容易豪直。因此,它僅應(yīng)在第一件東西進(jìn)入時(shí)和最后一件東西離開時(shí)發(fā)出信號(hào)珠移。將事件重命名為onFirstEnter弓乙,并將onLastExit
其重命名以使其變得清晰,這將需要再次掛接事件钧惧。
為了使這種行為成為可能暇韧,我們必須跟蹤區(qū)域中當(dāng)前的對(duì)撞機(jī)。我們將通過將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>();
…
}
該列表如何工作懈玻?
請(qǐng)參閱“ 對(duì)象管理”系列的“ 持久對(duì)象”教程。
在OnTriggerEnter中
僅調(diào)用輸入事件如果列表為空乾颁,則始終對(duì)撞機(jī)添加到列表中涂乌,以保持它的軌道。
void OnTriggerEnter (Collider other) {
if (colliders.Count == 0) {
onFirstEnter.Invoke();
}
colliders.Add(other);
}
在這種情況下英岭,我們將在OnTriggerExit中從列表刪除對(duì)撞機(jī)湾盒,僅當(dāng)列表為空時(shí)才調(diào)用exit事件。列表的Remove
方法返回刪除是否成功诅妹。應(yīng)當(dāng)總是這樣罚勾,因?yàn)榉駝t我們將無法跟蹤對(duì)撞機(jī),但是我們?nèi)匀豢梢詫?duì)其進(jìn)行檢查吭狡。
void OnTriggerExit (Collider other) {
if (colliders.Remove(other) && colliders.Count == 0) {
onLastExit.Invoke();
}
}
檢測(cè)出現(xiàn)和消失的對(duì)象
不幸的是尖殃,OnTriggerExit它是不可靠的,因?yàn)樵谕S没螅没蜾N毀游戲?qū)ο蠡蚱鋵?duì)撞機(jī)時(shí)送丰,不會(huì)調(diào)用它。不應(yīng)該單獨(dú)禁用碰撞器弛秋,因?yàn)槟菢訒?huì)導(dǎo)致物體掉落到幾何體中蚪战,因此我們將不支持此功能牵现。但是我們應(yīng)該能夠處理整個(gè)游戲?qū)ο笤趨^(qū)域內(nèi)時(shí)被禁用或破壞的情況铐懊。
每個(gè)物理步驟邀桑,我們都必須檢查區(qū)域中的對(duì)撞機(jī)是否仍然有效。添加一個(gè)在對(duì)撞機(jī)列表中循環(huán)的FixedUpdate方法科乎。如果對(duì)撞機(jī)進(jìn)行評(píng)估壁畸,false
則意味著它或其游戲?qū)ο笠驯黄茐摹H绻皇沁@種情況茅茂,我們必須檢查其游戲?qū)ο笫欠褚淹S媚笃迹覀兛梢酝ㄟ^activeInHierarchy
其游戲?qū)ο蟮膶傩詠聿檎摇H绻麑?duì)撞機(jī)不再有效空闲,請(qǐng)從列表中將其刪除令杈,并減少循環(huán)迭代器。如果列表為空碴倾,則調(diào)用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();
}
}
}
}
大多數(shù)情況下,檢測(cè)區(qū)域中可能沒有物體跌榔。為了避免不必要的FixedUpdate
連續(xù)調(diào)用异雁,我們可以在喚醒組件時(shí)以及最后一個(gè)對(duì)撞機(jī)退出后禁用該組件趋箩。然后我們只有在有東西進(jìn)入后才啟用它莹捡。之所以有效,是因?yàn)闊o論是否啟用行為均蜜,總是會(huì)觸發(fā)觸發(fā)器方法担平。
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;
}
}
接下來示绊,我們還應(yīng)該處理區(qū)域游戲?qū)ο蟊旧肀煌S没蜾N毀的情況,因?yàn)楫?dāng)事件仍在區(qū)域中時(shí)發(fā)生時(shí)暂论,調(diào)用退出事件是有意義的面褐。我們都可以通過添加OnDisable
清除列表并在列表不為空時(shí)調(diào)用exit事件的方法來做到。
void OnDisable () {
if (colliders.Count > 0) {
colliders.Clear();
onLastExit.Invoke();
}
}
請(qǐng)注意空另,檢測(cè)區(qū)的組件不應(yīng)由其他代碼禁用盆耽,因?yàn)樗梢怨芾碜约旱臓顟B(tài)。一般規(guī)則是不要禁用檢測(cè)區(qū)域組件扼菠,也不要禁用任何可能影響該區(qū)域的對(duì)撞機(jī)摄杂。這些游戲?qū)ο髴?yīng)全部停用或銷毀。
熱裝
因?yàn)闊嶂剌d(在編輯器播放模式下重新編譯)OnDisable將被調(diào)用循榆,因此它違反了我們剛剛聲明的規(guī)則析恢。這將導(dǎo)致調(diào)用退出事件以響應(yīng)熱重載,此后已存在于該區(qū)域中的對(duì)象將被忽略秧饮。幸運(yùn)的是映挂,我們可以檢測(cè)到OnDisable中的熱重裝泽篮。如果同時(shí)啟用了該組件并且游戲?qū)ο筇幱诨顒?dòng)狀態(tài),則我們將進(jìn)行熱重載柑船,并且什么也不做帽撑。當(dāng)游戲?qū)ο鬀]有被銷毀而組件被銷毀時(shí),情況也是如此鞍时,但是我們裁定不應(yīng)該這樣做亏拉。
我們只需要在編輯器中播放時(shí)進(jìn)行檢查,就可以將代碼包裝在#if UNITY_EDITOR
和中#endif
逆巍。
void OnDisable () {
#if UNITY_EDITOR
if (enabled && gameObject.activeInHierarchy) {
return;
}
#endif
if (colliders.Count > 0) {
colliders.Clear();
onLastExit.Invoke();
}
}
OnDisable中相關(guān)的狀態(tài)組合是什么及塘?
如果禁用了該組件,則將其禁用或禁用游戲?qū)ο笕窦缓笪覀兝^續(xù)進(jìn)行笙僚。否則,如果游戲?qū)ο笪刺幱诨顒?dòng)狀態(tài)灵再,則該游戲?qū)ο髮⒈煌S没蜾N毀肋层,然后我們繼續(xù)進(jìn)行。否則檬嘀,它要么是熱裝槽驶,要么是僅組件被破壞,我們將其忽略鸳兽。
更復(fù)雜的行為
這只是通過事件可以完成的簡(jiǎn)單演示掂铐。您可以通過向事件列表中添加更多條目來創(chuàng)建更復(fù)雜的行為。您不必為此創(chuàng)建新方法揍异,您可以使用現(xiàn)有方法全陨。限制是它必須是與事件的參數(shù)列表匹配的void方法或?qū)傩栽O(shè)置器,或者最多具有一個(gè)可序列化的參數(shù)衷掷。例如辱姨,我進(jìn)行了一些設(shè)置,以便在檢測(cè)區(qū)域內(nèi)有東西的同時(shí)關(guān)閉懸浮區(qū)域戚嗅,除了更改區(qū)域本身的可視化效果之外雨涛。
您不必總是對(duì)所有事件都響應(yīng)。您可能只有在進(jìn)入或退出時(shí)才觸發(fā)某些事件懦胞。例如替久,在進(jìn)入?yún)^(qū)域時(shí)激活某些內(nèi)容。然后退出并不會(huì)取消激活它躏尉,而重新進(jìn)入則會(huì)再次激活它蚯根,這無濟(jì)于事。
這種基于事件的方法可以用于整個(gè)游戲嗎胀糜?
從理論上講颅拦,是的蒂誉,這對(duì)于快速制作原型非常有用,但是卻很麻煩距帅。一旦發(fā)現(xiàn)自己重復(fù)了一個(gè)復(fù)雜的模式右锨,就可以為其創(chuàng)建專用的方法或行為,這應(yīng)該更容易使用锥债,并在以后必要時(shí)進(jìn)行優(yōu)化陡蝇。
簡(jiǎn)單運(yùn)動(dòng)
我們將在本教程中介紹的最后一種情況是移動(dòng)環(huán)境對(duì)象。復(fù)雜的運(yùn)動(dòng)可以通過動(dòng)畫來完成哮肚,可以通過檢測(cè)區(qū)域觸發(fā)。但是通常兩點(diǎn)之間的簡(jiǎn)單線性插值就足夠了广匙,例如允趟,對(duì)于門,電梯或浮動(dòng)平臺(tái)鸦致。因此潮剪,讓我們添加對(duì)此的支持。
自動(dòng)滑塊
無論插值什么分唾,它在概念上都由從0到1的滑塊控制抗碰。如何更改值是與插值本身不同的問題。保持滑塊分離還可以將其用于多個(gè)插值绽乔。因此弧蝇,我們將創(chuàng)建一個(gè)AutomaticSlider
專用于此值的組件。它的可配置持續(xù)時(shí)間必須為正折砸。當(dāng)我們使用它為物理對(duì)象設(shè)置動(dòng)畫時(shí)看疗,我們將使其在FixedUpdate
方法中增加其值,并確保它不會(huì)過沖睦授。一旦值達(dá)到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事件來將行為附加到滑塊怖辆。在這種情況下,我們需要一個(gè)on-value-changed事件删顶,該事件將用于傳遞滑塊的當(dāng)前值竖螃。因此,我們的事件需要一個(gè)float
參數(shù)翼闹,我們可以為其使用UnityEvent<float>
類型斑鼻。在FixedUpdate結(jié)束時(shí)調(diào)用事件。
using UnityEngine;
using UnityEngine.Events;
public class AutomaticSlider : MonoBehaviour {
…
[SerializeField]
UnityEvent<float> onValueChanged = default;
float value;
void FixedUpdate () {
…
onValueChanged.Invoke(value);
}
}
但是猎荠,Unity無法序列化通用事件類型坚弱,因此該事件不會(huì)顯示在檢查器中蜀备。我們必須創(chuàng)建自己的具體可序列化事件類型,該事件類型可以簡(jiǎn)單地?cái)U(kuò)展UnityEvent<float>
荒叶。此類型特定于我們的滑塊碾阁,因此通過在類內(nèi)部以及事件字段本身進(jìn)行聲明將其設(shè)置為嵌套類型。
[System.Serializable]
public class OnValueChangedEvent : UnityEvent<float> { }
[SerializeField]
OnValueChangedEventonValueChanged = default;
進(jìn)入播放模式時(shí)些楣,滑塊將立即開始增加脂凶。如果您不希望這樣做,請(qǐng)?jiān)谀J(rèn)情況下將其禁用愁茁。然后蚕钦,您可以將其連接到檢測(cè)區(qū)域,以在以后啟用它鹅很。
請(qǐng)注意嘶居,在這種情況下,事件的名稱后跟(Single)促煮,表示它具有一個(gè)參數(shù)邮屁。Single是指float
類型,它是單精度浮點(diǎn)數(shù)菠齿。
位置插補(bǔ)器
接下來佑吝,創(chuàng)建一個(gè)PositionInterpolator
組件類型,該組件類型Rigidbody
通過帶有float
參數(shù)的公共方法Interpolate在兩個(gè)可配置位置之間插值可配置位置绳匀。請(qǐng)使用Vector3.LerpUnclamped
以便提供的值不會(huì)受到限制芋忿,而將其留給調(diào)用者。我們必須通過其MovePosition
方法更改身體的位置襟士,以便將其解釋為運(yùn)動(dòng)盗飒,否則將成為隱形傳送。
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都添加到同一平臺(tái)對(duì)象陋桂,我創(chuàng)建了一個(gè)簡(jiǎn)單的移動(dòng)平臺(tái)逆趣。內(nèi)插器方法Interpolate的動(dòng)態(tài)版本綁定到滑塊的事件,這就是為什么其值沒有字段的原因嗜历。然后宣渗,我將滑塊連接到檢測(cè)區(qū)域,以便在有物體進(jìn)入該區(qū)域時(shí)激活平臺(tái)梨州。請(qǐng)注意痕囱,插值點(diǎn)位于世界空間中。
自動(dòng)倒車
我們可以通過向添加一個(gè)可配置的自動(dòng)反向切換來使插值來回移動(dòng)AutomaticSlider
暴匠。這需要我們跟蹤它是否被反轉(zhuǎn)鞍恢,并將FixedUpdate中的代碼加倍,必須支持雙向。同樣帮掉,當(dāng)自動(dòng)反轉(zhuǎn)激活時(shí)弦悉,我們必須跳動(dòng)而不是鉗制該值。在持續(xù)時(shí)間極短的情況下蟆炊,這可能會(huì)導(dǎo)致過沖稽莉,因此反彈后我們?nèi)匀粫?huì)鉗住。
[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);
}
平穩(wěn)步伐
線性插值的運(yùn)動(dòng)是剛性的涩搓,反轉(zhuǎn)時(shí)速度會(huì)突然變化污秆。通過將值的平滑變體傳遞給事件,我們可以使其加速和減速昧甘。我們通過應(yīng)用smoothstep功能給它良拼,這是3V 2 - 2V 3。使它成為可配置的選項(xiàng)疾层。
[SerializeField]
bool autoReverse = false, smoothstep = false;
…
float SmoothedValue => 3f * value * value - 2f * value * value * value;
void FixedUpdate () {
…
onValueChanged.Invoke(smoothstep ? SmoothedValue :value);
}
更多控制
可以通過檢測(cè)區(qū)域事件禁用滑塊組件來暫停動(dòng)畫将饺,但是我們也可以控制其方向。最簡(jiǎn)單的方法是通過公共屬性提供其反轉(zhuǎn)狀態(tài)痛黎。用自動(dòng)Reversed
屬性替換該reversed字段,并調(diào)整其他代碼的大小寫以使其匹配刮吧。
//bool reversed;
public bool Reversed { get; set; }
讓我們對(duì)自動(dòng)反轉(zhuǎn)選項(xiàng)執(zhí)行相同的操作湖饱。在這種情況下,我們必須保留序列化字段杀捻,因此添加一個(gè)顯式屬性井厌。
public bool AutoReverse {
get => autoReverse;
set => autoReverse = value;
}
請(qǐng)注意,方向反轉(zhuǎn)是突然的致讥,因?yàn)樗匀皇呛?jiǎn)單的插值仅仆。如果要在任何時(shí)候平穩(wěn)停止和反轉(zhuǎn),則需要?jiǎng)?chuàng)建使用加速度和速度的更復(fù)雜的邏輯垢袱。
碰撞碰撞
移動(dòng)風(fēng)景的危險(xiǎn)是墓拜,身體最終可能會(huì)陷入兩個(gè)接近的對(duì)撞機(jī)之間。當(dāng)對(duì)撞機(jī)之間的縫隙關(guān)閉時(shí)请契,身體要么被彈出咳榜,要么最終被推入對(duì)撞機(jī)或通過對(duì)撞機(jī)。如果碰撞表面成一定角度爽锥,則存在清晰的逃生路徑涌韩,身體將朝該方向被推動(dòng)。如果不是這樣氯夷,或者如果沒有足夠的時(shí)間逃脫臣樱,則身體最終會(huì)被壓碎,從而穿透對(duì)撞機(jī)。如果一個(gè)物體卡在兩個(gè)足夠厚的簡(jiǎn)單對(duì)撞機(jī)之間雇毫,那么它可以留在它們內(nèi)部玄捕,一旦有一條清晰的道路就會(huì)彈出。否則會(huì)掉下去嘴拢。
如果碰撞表面成一定角度桩盲,則身體將被推到一邊,并且很有可能逃脫席吴。因此赌结,通過在表面之間留出足夠的空間或通過引入傾斜的對(duì)撞機(jī)(無論是否可見)來設(shè)計(jì)這樣的配置是一個(gè)好主意。此外孝冒,將盒子對(duì)撞機(jī)隱藏在地板上可以使它更牢固柬姚,以免物體被推過∽校或者量承,添加一個(gè)區(qū)域,在適當(dāng)?shù)臅r(shí)候觸發(fā)該區(qū)域的破壞穴店,表示它被壓碎了撕捍。
局部插值
世界空間中的配置可能會(huì)帶來不便,因?yàn)樗鼰o法在多個(gè)位置用于同一動(dòng)畫泣洞。因此忧风,我們通過給PositionInterpolator添加一個(gè)本地空間選項(xiàng)來包裝一下。為此球凰,我們添加了一個(gè)可選的可配置的相對(duì)于插值發(fā)生位置的Transform狮腿。通常用插值器引用對(duì)象,但這不是必需的呕诉。
[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);
}
想知道下一個(gè)教程何時(shí)發(fā)布嗎缘厢?關(guān)注微信公眾號(hào)(u3dnotes)吧!
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-10-reactive-environment/
往期精選
Unity3D游戲開發(fā)中100+效果的實(shí)現(xiàn)和源碼大全 - 收藏起來肯定用得著
Shader學(xué)習(xí)應(yīng)該如何切入甩挫?
聲明:發(fā)布此文是出于傳遞更多知識(shí)以供交流學(xué)習(xí)之目的贴硫。若有來源標(biāo)注錯(cuò)誤或侵犯了您的合法權(quán)益,請(qǐng)作者持權(quán)屬證明與我們聯(lián)系捶闸,我們將及時(shí)更正夜畴、刪除,謝謝删壮。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/reactive-environment/
翻譯贪绘、編輯、整理:MarsZhou
More:【微信公眾號(hào)】?u3dnotes