天天看點

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動

        很多童鞋沒有系統的Unity3D遊戲開發基礎,也不知道從何開始學。為此我們精選了一套國外優秀的Unity3D遊戲開發教程,翻譯整理後放送給大家,教您從零開始一步一步掌握Unity3D遊戲開發。 本文不是廣告,不是推廣,是免費的純幹貨!本文全名:喵的Unity遊戲開發之路 - 移動 - 滑動球體 - 玩家控制下的球的滑動

将帶有軌迹的球體放在平面上。

根據玩家輸入來定位球體。

控制速度和加速度。

限制球體的位置,使其從邊緣反彈。

這是有關控制角色移動的教程系列的第一部分。具體來說,我們将根據玩家的輸入滑動一個球體。

本教程使用Unity 2019.2.9f1制作。假定您已經先閱讀了基礎教程。

最終效果之一

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動

球體卡在平面上。

控制位置

許多遊戲都是關于一個角色,角色必須四處移動以實作某些目标。玩家的任務是引導角色。動作遊戲通常通過按下鍵或轉動操縱杆來操縱角色,進而使您可以直接控制遊戲。點選遊戲可讓您訓示目标位置,角色會自動移動到該位置。程式設計遊戲可讓您編寫角色執行的指令。等等。

在本教程系列中,我們将重點介紹如何在3D動作遊戲中控制角色。我們從在一個小的平面矩形上滑動一個球體開始簡單。一旦我們牢牢掌握了這一點,将來我們就可以使其變得更加複雜。

設定場景

從一個新的預設3D項目開始。盡管您可以使用自己選擇的渲染管道,但此時包管理器不需要任何東西。

我一直使用線性色彩空間,您可以通過“Edit / Project Settings / Player / Other Settings”在項目設定中進行配置。

預設的SampleScene場景有一個攝像頭和一個定向燈,我們将保留它們。建立一個代表地面的平面以及一個球體,兩者均位于原點。預設球體的半徑為0.5,是以将其Y坐标設定為0.5,使其看起來像位于地平面的頂部。

我們将自己限制為在地面上進行2D運動,是以讓我們将錄影機向下放置在平面上方,以便在遊戲視窗中清晰地看到遊戲區域。還将其Projection 模式設定為“Orthographic”。這擺脫了透視,使我們能夠看到2D運動而不會變形。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動

剩下的唯一使我們困惑的是球體的陰影。通過将燈光的“ Shadow Type陰影類型 ”設定為“ None無 ”或“ No Shadows無陰影”來消除它,具體取決于Unity版本。

為地面和球體建立材質,并根據需要進行配置。我将球體設為黑色,将地面暗淡的顔色設為灰色。我們還将通過軌迹可視化運動,是以也要為此建立材質。我将使用一種淡紅色的材質。最後,我們需要一個

MovingSphere

腳本來實作運動。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動

該腳本可以以MonoBehaviour的空擴充名開頭。

using UnityEngine;              public class MovingSphere : MonoBehaviour { }
           

将一個

TrailRenderer

和我們的

MovingSphere

元件都添加到球體中。保持其他一切不變。

将跟蹤材質配置設定給元件的“ 材質”數組的第一個也是唯一的元素

TrailRenderer

。它并不需要投下陰影,盡管這并不是必須的,因為我們還是禁用了那些陰影。除此之外,将“ 寬度”從1.0 減小到更合理的值(例如0.1),這将生成細線。

盡管我們尚未編碼任何運動,但可以通過進入播放模式并在場景視窗中移動球體來預覽其外觀。

讀取玩家輸入

要移動球體,我們必須閱讀玩家的輸入指令。我們使用MovingSphere的

Update

方法來做到這一點。播放器輸入為2D,是以我們可以将其存儲在

Vector2

變量中。最初,我們将其X和Y分量都設定為零,然後使用它們将球體放置在XZ平面中。是以,輸入的Y分量成為位置的Z分量。Y位置保持零。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
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鍵。輸入值也經過調整,是以按鍵的行為有點像操縱杆。您可以根據需要調整這些設定,但我保留預設設定。

使用箭頭或WASD鍵。

兩個軸都有第二個定義,将它們連結到操縱杆或左操縱杆的輸入。這樣可以使輸入更加流暢,但是我将使用除下一個動畫之外的所有動畫的關鍵幀。

使用控制棒。

歸一化輸入向量

軸在靜止時傳回零,而在極限時傳回-1或1。當我們使用輸入來設定球體的位置時,它被限制為具有相同範圍的矩形。至少,鍵輸入就是這種情況,因為鍵是獨立的。如果是棍子,則尺寸是互相關聯的,通常我們在任何方向上都被限制為距原點的最大距離為1,進而将位置限制在一個圓内。

控制器輸入的優點是,無論方向如何,輸入向量的最大長度始終為1。是以,各個方向的移動速度都可以一樣快。按鍵不是這種情況,單個按鍵的最大值為1,而同時按下兩個按鍵的最大值為√2,這意味着對角線移動最快。

由于勾股定理,鍵的最大值為√2。軸值定義直角三角形兩側的長度,組合的矢量為斜邊。是以,輸入向量的大小為x2+y2" role="presentation"> sqrt(x ^ 2 + y ^ 2)。

通過将輸入矢量除以其大小,可以確定矢量的長度永遠不會超過1。結果始終是機關長度向量,除非其初始長度為零,在這種情況下,結果不确定。此過程稱為标準化向量。我們可以通過調用

Normalize

向量來做到這一點,向量将自行縮放并在結果不确定時變為零向量。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
playerInput.x = Input.GetAxis("Horizontal");              playerInput.y = Input.GetAxis("Vertical");              playerInput.Normalize();
           

标準化的按鍵輸入。

限制輸入向量

始終對輸入向量進行歸一化會将位置限制為始終位于圓上,除非輸入是中性的,在這種情況下,我們最終會到達原點。原點和圓之間的線表示一個架構,其中圓從中心跳到圓或向後跳。

這種全有或全無的輸入可能是理想的,但讓我們也使圓内的所有位置也有效。我們僅通過調整輸入矢量的大小(如果其大小超過1)來做到這一點。一種友善的方法是調用靜态

Vector2.ClampMagnitude

方法而不是

Normalize

,使用向量(最大為1)作為參數。結果是一個相同或縮小到所提供最大值的向量。

//playerInput.Normalize();              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”。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
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)。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
[SerializeField, Range(0f, 100f)]              float maxSpeed = 10f;
           

SerializeField是做什麼的?

它告訴Unity對字段進行序列化,這意味着它已儲存并在Unity編輯器中公開,是以可以通過檢查器進行調整。我們也可以建立該public字段,但是通過這種方式,該字段仍然不受MovingSphere類外部代碼的影響。

将輸入向量和最大速度相乘以找到所需的速度。

Vector3 velocity =              new Vector3(playerInput.x, 0f, playerInput.y)* maxSpeed;
           

最高速度設定為10。

加速

由于我們可以直接控制速度,是以可以立即進行更改。僅輸入系統應用的過濾會稍微減慢更改的速度。實際上,速度不能立即改變。更改職位需要一定的精力和時間,就像更改職位一樣。速度的變化率稱為加速度“ a”,導緻“ v_(n + 1)= v_n + at”,而“ v_0”為零向量。減速隻是與目前速度相反的加速度,是以不需要特殊處理。

讓我們看看如果使用輸入矢量直接控制加速度而不是速度來控制時會發生什麼。這需要我們跟蹤目前速度,是以将其存儲在一個字段中。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
Vector3 velocity;
           

現在,輸入向量在Update中定義了加速度,但讓我們暫時将其乘以maxSpeed,暫時将其重新解釋為最大加速度。然後将其添加到速度,然後計算位移。

Vector3 acceleration=              new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;              velocity += acceleration * Time.deltaTime;              Vector3 displacement = velocity * Time.deltaTime;
           

所需速度

控制加速度而不是速度會産生更平滑的運動,但同時也會削弱我們對球體的控制。就像我們開車而不是步行。在大多數遊戲中,需要對速度進行更直接的控制,是以讓我們回到這種方法。但是,施加加速度确實會産生更平滑的運動。

我們可以通過直接控制目标速度并将加速度應用于實際速度,直到與所需速度相比對,來結合這兩種方法。然後,我們可以通過調整球的最大加速度來調整球的響應速度。為此添加一個可序列化的字段。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
[SerializeField, Range(0f, 100f)]              float maxAcceleration = 10f;
           

現在,

Update

我們使用輸入矢量來定義所需的速度,而不再用舊的方式調整速度。

Vector3 desiredVelocity=              new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;              //velocity += acceleration * Time.deltaTime;
           

相反,我們首先通過将最大加速度乘以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 += maxSpeedChange;              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;              //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);              //}              velocity.x =              Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);              velocity.z =              Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);
           

最大速度和加速度均設定為10。

現在,我們可以調整最大加速度,以在平滑運動和響應性之間達成所需的權衡。

限制位置

除了控制角色的速度之外,遊戲的很大一部分還限制了角色的前進方向。我們的簡單場景包含一個代表地面的平面。讓我們做這個,使球體必須保留在平面上。

留在方格内

與其使用平面本身,不如簡單地使允許區域成為球體的可序列化字段。我們可以

Rect

為此使用結構值。通過調用其構造函數方法(其前兩個參數為-5和後兩個參數為10),為其提供與預設平面比對的預設值。這些定義了其左下角和大小。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
[SerializeField]              Rect allowedArea = new Rect(-5f, -5f, 10f, 10f);
           

在将新位置配置設定給之前,我們通過限制新位置來限制球體。是以,首先将其存儲在transform.localPosition變量中。

//transform.localPosition += displacement;              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

xMax

X屬性,為Z使用

yMin

yMax

屬性。

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
喵的Unity遊戲開發之路 - 玩家控制下的球的滑動
if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) {              //newPosition = transform.localPosition;              newPosition.x =              Mathf.Clamp(newPosition.x, allowedArea.xMin, allowedArea.xMax);              newPosition.z =              Mathf.Clamp(newPosition.z, allowedArea.yMin, allowedArea.yMax);              }
           

堅持邊緣。

消除速度

現在,球體似乎粘在邊緣上。到達邊緣後,我們沿着邊緣滑動,但是要過一段時間才能離開邊緣。發生這種情況是因為球體的速度仍然指向邊緣。我們必須通過遠離邊緣的加速度來改變方向,這需要一段時間,具體取決于最大加速度。

如果我們的球體是一個球,而該區域的邊緣是一堵牆,那麼如果它碰到牆,則應該停止。确實發生了。但是,如果牆壁突然消失,球将無法恢複之前的速度。動量消失了,它的能量在碰撞過程中轉移了,這可能已經造成了破壞。是以,當碰到邊緣時,我們必須擺脫速度。但是仍然可以沿着邊緣滑動,是以僅應消除指向該邊緣方向的速度分量。

為了将适當的速度分量設定為零,我們必須檢查兩個次元的兩個方向是否超出範圍。此時,我們最好自己定位位置,因為我們正在執行與

Mathf.Clamp

和Contains相同的檢查。

//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);              //}              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;              }
           

彈性設定為0.5。

這并不代表現實的實體,這要複雜得多。但是它開始看起來像它,對于大多數遊戲來說已經足夠了。另外,我們的動作也不是很精确。我們的計算僅在幀中運動結束時恰好到達邊緣時才是正确的。事實并非如此,這意味着我們應該立即将球體移離邊緣一點點。首先計算剩餘時間,然後将其與相關維中的新速度一起使用。但是,這可能會導緻第二次彈跳,使事情變得更加複雜。幸運的是,我們不需要如此精确的精度就能呈現出令人信服的球體反彈的錯覺。

下一個教程是實體。

Repository

https://bitbucket.org/catlikecodingunitytutorials/movement-01-sliding-a-sphere/

往期精選

Unity3D遊戲開發中100+效果的實作和源碼大全 - 收藏起來肯定用得着

Shader學習應該如何切入?

UE4 開發從入門到入土

聲明:釋出此文是出于傳遞更多知識以供交流學習之目的。若有來源标注錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯系,我們将及時更正、删除,謝謝。

原作者:Jasper Flick

原文:

https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/

翻譯、編輯、整理:MarsZhou

More:【微信公衆号】 u3dnotes

喵的Unity遊戲開發之路 - 玩家控制下的球的滑動