將帶有軌跡的球體放在平面上。
根據玩家輸入來定位球體。
控制速度和加速度。
限制球體的位置,使其從邊緣反彈。
這是有關控制角色移動的教程系列的第一部分。具體來說,我們將根據玩家的輸入滑動一個球體。
本教程使用Unity 2019.2.9f1製作。假定您已經先閱讀了基礎教程。
許多遊戲都是關於一個角色,角色必須四處移動以實現某些目標。玩家的任務是引導角色。動作遊戲通常通過按下鍵或轉動操縱杆來操縱角色,從而使您可以直接控制遊戲。點擊遊戲可讓您指示目標位置,角色會自動移動到該位置。編程遊戲可讓您編寫角色執行的指令。等等。
在本教程系列中,我們將重點介紹如何在3D動作遊戲中控制角色。我們從在一個小的平面矩形上滑動一個球體開始簡單。一旦我們牢牢掌握了這一點,將來我們就可以使其變得更加複雜。
從一個新的默認3D項目開始。儘管您可以使用自己選擇的渲染管道,但此時包管理器不需要任何東西。
我一直使用線性色彩空間,您可以通過「Edit / Project Settings / Player / Other Settings」在項目設置中進行配置。
默認的SampleScene場景有一個攝像頭和一個定向燈,我們將保留它們。創建一個代表地面的平面以及一個球體,兩者均位於原點。默認球體的半徑為0.5,因此將其Y坐標設置為0.5,使其看起來像位於地平面的頂部。
我們將自己限制為在地面上進行2D運動,因此讓我們將攝像機向下放置在平面上方,以便在遊戲窗口中清晰地看到遊戲區域。還將其Projection 模式設置為「Orthographic」。這擺脫了透視,使我們能夠看到2D運動而不會變形。
剩下的唯一使我們困惑的是球體的陰影。通過將燈光的「 Shadow Type陰影類型 」設置為「 None無 」或「 No Shadows無陰影」來消除它,具體取決於Unity版本。
為地面和球體創建材質,並根據需要進行配置。我將球體設為黑色,將地面暗淡的顏色設為灰色。我們還將通過軌跡可視化運動,因此也要為此創建材質。我將使用一種淡紅色的材質。最後,我們需要一個MovingSphere腳本來實現運動。
該腳本可以以MonoBehaviour的空擴展名開頭。
using UnityEngine;
public class MovingSphere : MonoBehaviour { }
將一個TrailRenderer和我們的MovingSphere組件都添加到球體中。保持其他一切不變。
將跟蹤材質分配給組件的「 材質」數組的第一個也是唯一的元素TrailRenderer。它並不需要投下陰影,儘管這並不是必須的,因為我們還是禁用了那些陰影。除此之外,將「 寬度」從1.0 減小到更合理的值(例如0.1),這將生成細線。
儘管我們尚未編碼任何運動,但可以通過進入播放模式並在場景窗口中移動球體來預覽其外觀。
要移動球體,我們必須閱讀玩家的輸入命令。我們使用MovingSphere的Update方法來做到這一點。播放器輸入為2D,因此我們可以將其存儲在Vector2變量中。最初,我們將其X和Y分量都設置為零,然後使用它們將球體放置在XZ平面中。因此,輸入的Y分量成為位置的Z分量。Y位置保持零。
using UnityEngine;
public class MovingSphere : MonoBehaviour {
void Update () { Vector2 playerInput; playerInput.x = 0f; playerInput.y = 0f; transform.localPosition = new Vector3(playerInput.x, 0f, playerInput.y); }}從播放器檢索方向輸入的最簡單方法是調用Input.GetAxis軸名稱。默認情況下,Unity 定義了水平和垂直輸入軸,您可以在項目設置的「 輸入」部分中進行檢查。我們將水平值用於X,將垂直值用於Y。
playerInput.x =Input.GetAxis("Horizontal"); playerInput.y =Input.GetAxis("Vertical");默認設置將這些軸連結到箭頭和WASD鍵。輸入值也經過調整,因此按鍵的行為有點像操縱杆。您可以根據需要調整這些設置,但我保留默認設置。
兩個軸都有第二個定義,將它們連結到操縱杆或左操縱杆的輸入。這樣可以使輸入更加流暢,但是我將使用除下一個動畫之外的所有動畫的關鍵幀。
您可以這樣做,但是原理是相同的。我們所需要做的就是檢索兩個軸值。另外,在撰寫本文時,該軟體包仍在預覽中,因此尚未正式發布並受支持。
軸在靜止時返回零,而在極限時返回-1或1。當我們使用輸入來設置球體的位置時,它被約束為具有相同範圍的矩形。至少,鍵輸入就是這種情況,因為鍵是獨立的。如果是棍子,則尺寸是相互關聯的,通常我們在任何方向上都被限制為距原點的最大距離為1,從而將位置限制在一個圓內。
控制器輸入的優點是,無論方向如何,輸入向量的最大長度始終為1。因此,各個方向的移動速度都可以一樣快。按鍵不是這種情況,單個按鍵的最大值為1,而同時按下兩個按鍵的最大值為√2,這意味著對角線移動最快。
由於勾股定理,鍵的最大值為√2。軸值定義直角三角形兩側的長度,組合的矢量為斜邊。因此,輸入向量的大小為 sqrt(x ^ 2 + y ^ 2)。
通過將輸入矢量除以其大小,可以確保矢量的長度永遠不會超過1。結果始終是單位長度向量,除非其初始長度為零,在這種情況下,結果不確定。此過程稱為標準化向量。我們可以通過調用Normalize向量來做到這一點,向量將自行縮放並在結果不確定時變為零向量。
playerInput.x = Input.GetAxis("Horizontal"); playerInput.y = Input.GetAxis("Vertical"); playerInput.Normalize();
始終對輸入向量進行歸一化會將位置限制為始終位於圓上,除非輸入是中性的,在這種情況下,我們最終會到達原點。原點和圓之間的線表示一個框架,其中圓從中心跳到圓或向後跳。
這種全有或全無的輸入可能是理想的,但讓我們也使圓內的所有位置也有效。我們僅通過調整輸入矢量的大小(如果其大小超過1)來做到這一點。一種方便的方法是調用靜態Vector2.ClampMagnitude方法而不是Normalize,使用向量(最大為1)作為參數。結果是一個相同或縮小到所提供最大值的向量。
playerInput = Vector2.ClampMagnitude(playerInput, 1f);
到目前為止,我們一直在直接使用輸入來設置球體的位置。這意味著,當輸入向量「 i」改變時,球體的位置「 p」立即改變為相同值。因此,「 p = i」。這不是適當的運動,是隱形傳態。一種更自然的控制球體的方法是通過將位移矢量d添加到其舊位置p_0來確定其下一個位置p_1,因此p_1 = p_0 + d。
通過使用d = i而不是p = i,我們使輸入和位置之間的關係不太直接。這樣就消除了位置上的約束,因為它現在相對於自身而不是第一次更新後的原點。因此,該位置由無限迭代序列「 p_(n + 1)= p_n + d」描述,其中「 p_0」定義為起始位置。
Vector3 displacement = new Vector3(playerInput.x, 0f, playerInput.y); transform.localPosition+= displacement;
我們的球體確實可以移動到任何地方,但是它是如此之快以至於難以控制。這是每次更新都添加輸入向量的結果。幀速率越高,速度越快。為了獲得一致的結果,我們不希望幀頻影響我們的輸入。如果我們使用恆定的輸入,則無論幀速率是否可能波動,我們都需要恆定的位移。
為了我們的目的,一個幀代表一個持續時間:從上一幀的開始到當前幀之間經過了t時間,我們可以通過訪問Time.deltaTime。因此,我們的位移實際上是「 d = it」,我們錯誤地認為「 t」是常數。
位移以Unity單位測量,假定代表一米。但是我們將輸入乘以持續時間,以秒表示。為了達到米,輸入必須以米/秒為單位。因此,輸入矢量表示速度:「 v = i」和「 d = vt」。
Vector3 velocity = new Vector3(playerInput.x, 0f, playerInput.y); Vector3 displacement =velocity * Time.deltaTime; transform.localPosition += displacement;
我們的最大輸入向量的大小為1,表示每秒一米的速度,等於每小時3.6公裡,大約每小時2.24英裡。那不是很快。
我們可以通過縮放輸入向量來提高最大速度。比例因子表示最大速度,即沒有方向的速度。添加一個具有SerializeField屬性的欄位maxSpeed(默認值為10),並為其賦予Range屬性(例如1–100)。
[SerializeField, Range(0f, 100f)] float maxSpeed = 10f;它告訴Unity對欄位進行序列化,這意味著它已保存並在Unity編輯器中公開,因此可以通過檢查器進行調整。我們也可以創建該public欄位,但是通過這種方式,該欄位仍然不受MovingSphere類外部代碼的影響。
將輸入向量和最大速度相乘以找到所需的速度。
Vector3 velocity = new Vector3(playerInput.x, 0f, playerInput.y)* maxSpeed;
由於我們可以直接控制速度,因此可以立即進行更改。僅輸入系統應用的過濾會稍微減慢更改的速度。實際上,速度不能立即改變。更改職位需要一定的精力和時間,就像更改職位一樣。速度的變化率稱為加速度「 a」,導致「 v_(n + 1)= v_n + at」,而「 v_0」為零向量。減速只是與當前速度相反的加速度,因此不需要特殊處理。
讓我們看看如果使用輸入矢量直接控制加速度而不是速度來控制時會發生什麼。這需要我們跟蹤當前速度,因此將其存儲在一個欄位中。
現在,輸入向量在Update中定義了加速度,但讓我們暫時將其乘以maxSpeed,暫時將其重新解釋為最大加速度。然後將其添加到速度,然後計算位移。
Vector3 acceleration= new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed; velocity += acceleration * Time.deltaTime; Vector3 displacement = velocity * Time.deltaTime;
控制加速度而不是速度會產生更平滑的運動,但同時也會削弱我們對球體的控制。就像我們開車而不是步行。在大多數遊戲中,需要對速度進行更直接的控制,因此讓我們回到這種方法。但是,施加加速度確實會產生更平滑的運動。
我們可以通過直接控制目標速度並將加速度應用於實際速度,直到與所需速度相匹配,來結合這兩種方法。然後,我們可以通過調整球的最大加速度來調整球的響應速度。為此添加一個可序列化的欄位。
[SerializeField, Range(0f, 100f)] float maxAcceleration = 10f;
現在,Update我們使用輸入矢量來定義所需的速度,而不再用舊的方式調整速度。
Vector3 desiredVelocity= new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
相反,我們首先通過將最大加速度乘以t來找到最大速度變化。這就是我們能夠更改此更新速度的程度。
Vector3 desiredVelocity = new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed; float maxSpeedChange = maxAcceleration * Time.deltaTime;
首先,我們僅考慮速度的X分量。如果小於期望值,則添加最大更改。
float maxSpeedChange = maxAcceleration * Time.deltaTime; if (velocity.x < desiredVelocity.x) { velocity.x += maxSpeedChange; }
這可能會導致過衝,我們可以通過選擇增加值和期望值中的最小值來防止。我們可以在這裡使用一種Mathf.Min方法。
if (velocity.x < desiredVelocity.x) { velocity.x = Mathf.Min(velocity.x + maxSpeedChange, desiredVelocity.x); }或者,速度可能大於所需速度。在那種情況下,我們減去最大變化,並通過Mathf.Max獲取最大值和所需值。
if (velocity.x < desiredVelocity.x) { velocity.x = Mathf.Min(velocity.x + maxSpeedChange, desiredVelocity.x); } else if (velocity.x > desiredVelocity.x) { velocity.x = Mathf.Max(velocity.x - maxSpeedChange, desiredVelocity.x); }
我們還可以通過便捷的Mathf.MoveTowards方法來完成所有這些工作,將當前和期望值以及允許的最大變化值傳遞給它。分別對X和Z組件執行此操作。
float maxSpeedChange = maxAcceleration * Time.deltaTime; velocity.x = Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange); velocity.z = Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);
現在,我們可以調整最大加速度,以在平滑運動和響應性之間達成所需的權衡。
除了控制角色的速度之外,遊戲的很大一部分還限制了角色的前進方向。我們的簡單場景包含一個代表地面的平面。讓我們做這個,使球體必須保留在平面上。
與其使用平面本身,不如簡單地使允許區域成為球體的可序列化欄位。我們可以Rect為此使用結構值。通過調用其構造函數方法(其前兩個參數為-5和後兩個參數為10),為其提供與默認平面匹配的默認值。這些定義了其左下角和大小。
[SerializeField] Rect allowedArea = new Rect(-5f, -5f, 10f, 10f);
在將新位置分配給之前,我們通過約束新位置來約束球體。因此,首先將其存儲在transform.localPosition變量中。
Vector3 newPosition = transform.localPosition + displacement; transform.localPosition = newPosition;
我們可以在允許區域上調用Contains以檢查點是否位於其內部或其邊緣。如果新位置不是這種情況,那麼我們將其設置為當前位置,並在此更新期間取消運動。
Vector3 newPosition = transform.localPosition + displacement; if (!allowedArea.Contains(newPosition)) { newPosition = transform.localPosition; } transform.localPosition = newPosition;當我們將Vector3傳遞給Contains它時,將檢查XY坐標,這在我們的情況下是不正確的。因此,將其Vector2與XZ坐標一起傳遞給新對象。
if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) { newPosition = transform.localPosition; }
我們的球體再也無法逃脫,它試圖停止時就停下來。結果很生澀,因為在某些幀中運動被忽略了,但是我們很快會處理。在此之前,球體可以一直移動直到它位於平面邊緣的頂部。那是因為我們限制了它的位置並且沒有考慮它的半徑。如果整個球體保持在允許區域內,則看起來會更好。我們可以更改代碼以考慮半徑,但是另一種方法是簡單地縮小允許區域。這對於我們簡單的場景就足夠了。
在兩個維度上,將區域的角向上移動0.5,並將其大小減小1。
我們可以通過將新位置鉗位到允許的區域而不是忽略它來擺脫劇烈的運動。我們可以通過調用Mathf.Clamp一個值及其允許的最小值和最大值來做到這一點。為X使用區域的xMin和xMaxX屬性,為Z使用yMin與yMax屬性。
if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) { newPosition.x = Mathf.Clamp(newPosition.x, allowedArea.xMin, allowedArea.xMax); newPosition.z = Mathf.Clamp(newPosition.z, allowedArea.yMin, allowedArea.yMax); }
現在,球體似乎粘在邊緣上。到達邊緣後,我們沿著邊緣滑動,但是要過一段時間才能離開邊緣。發生這種情況是因為球體的速度仍然指向邊緣。我們必須通過遠離邊緣的加速度來改變方向,這需要一段時間,具體取決於最大加速度。
如果我們的球體是一個球,而該區域的邊緣是一堵牆,那麼如果它碰到牆,則應該停止。確實發生了。但是,如果牆壁突然消失,球將無法恢復之前的速度。動量消失了,它的能量在碰撞過程中轉移了,這可能已經造成了破壞。因此,當碰到邊緣時,我們必須擺脫速度。但是仍然可以沿著邊緣滑動,因此僅應消除指向該邊緣方向的速度分量。
為了將適當的速度分量設置為零,我們必須檢查兩個維度的兩個方向是否超出範圍。此時,我們最好自己定位位置,因為我們正在執行與Mathf.Clamp和Contains相同的檢查。
if (newPosition.x < allowedArea.xMin) { newPosition.x = allowedArea.xMin; velocity.x = 0f; } else if (newPosition.x > allowedArea.xMax) { newPosition.x = allowedArea.xMax; velocity.x = 0f; } if (newPosition.z < allowedArea.yMin) { newPosition.z = allowedArea.yMin; velocity.z = 0f; } else if (newPosition.z > allowedArea.yMax) { newPosition.z = allowedArea.yMax; velocity.z = 0f; }
在碰撞過程中並非總是消除速度。如果我們的球體是一個完美的彈跳球,它將在相關尺寸上反轉方向。讓我們嘗試一下。
if (newPosition.x < allowedArea.xMin) { newPosition.x = allowedArea.xMin; velocity.x =-velocity.x; } else if (newPosition.x > allowedArea.xMax) { newPosition.x = allowedArea.xMax; velocity.x =-velocity.x; } if (newPosition.z < allowedArea.yMin) { newPosition.z = allowedArea.yMin; velocity.z =-velocity.z; } else if (newPosition.z > allowedArea.yMax) { newPosition.z = allowedArea.yMax; velocity.z =-velocity.z; }
現在,球體保持其動量,當它碰到牆時,它只是改變方向。它確實會放慢一點,因為彈跳後其速度將不再與所需速度匹配。為了獲得最佳反彈,玩家必須立即調整其輸入。
反轉時不需要保留整個速度。有些事情比其他事情反彈更多。因此,讓我們通過添加一個bounciness欄位來使其可配置,默認情況下將其設置為0.5,範圍為0–1。這使我們能夠使球體完全彈力或完全不彈跳,或介於兩者之間。
[SerializeField, Range(0f, 1f)] float bounciness = 0.5f;
碰到邊緣時,將跳動因素分解為新的速度值。
if (newPosition.x < allowedArea.xMin) { newPosition.x = allowedArea.xMin; velocity.x = -velocity.x *bounciness; } else if (newPosition.x > allowedArea.xMax) { newPosition.x = allowedArea.xMax; velocity.x = -velocity.x *bounciness; } if (newPosition.z < allowedArea.yMin) { newPosition.z = allowedArea.yMin; velocity.z = -velocity.z *bounciness; } else if (newPosition.z > allowedArea.yMax) { newPosition.z = allowedArea.yMax; velocity.z = -velocity.z *bounciness; } (視頻見文首)
這並不代表現實的物理,這要複雜得多。但是它開始看起來像它,對於大多數遊戲來說已經足夠了。另外,我們的動作也不是很精確。我們的計算僅在幀中運動結束時恰好到達邊緣時才是正確的。事實並非如此,這意味著我們應該立即將球體移離邊緣一點點。首先計算剩餘時間,然後將其與相關維中的新速度一起使用。但是,這可能會導致第二次彈跳,使事情變得更加複雜。幸運的是,我們不需要如此精確的精度就能呈現出令人信服的球體反彈的錯覺。
下一個教程是物理。
https://bitbucket.org/catlikecodingunitytutorials/movement-01-sliding-a-sphere/
聲明:發布此文是出於傳遞更多知識以供交流學習之目的。若有來源標註錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯繫,我們將及時更正、刪除,謝謝。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/
翻譯、編輯、整理:MarsZhou
More:【微信公眾號】 u3dnotes