不少童鞋沒有系統的Unity3D遊戲開發基礎,也不知道從何開始學。爲此咱們精選了一套國外優秀的Unity3D遊戲開發教程,翻譯整理後放送給你們,教您從零開始一步一步掌握Unity3D遊戲開發。 本文不是廣告,不是推廣,是免費的純乾貨!本文全名:喵的Unity遊戲開發之路 - 移動 - 推球:遊戲中的物理php
控制剛體球體的速度。css
經過跳躍支持垂直運動。nginx
檢測地面及其角度。apache
使用ProBuilder建立測試場景。編程
沿斜坡移動。json
這是有關控制角色移動的教程系列的第二部分。此次,咱們將使用物理引擎建立更逼真的運動並支持更復雜的環境。bash
本教程使用Unity 2019.2.11f1製做。它還使用ProBuilder軟件包。微信
最終效果之一框架
在不公平的賽道上不受約束的球體。less
剛體
在上一教程中,咱們將球體約束爲保留在矩形區域內。顯式地編程這樣的限制頗有意義,由於它很簡單。可是,若是咱們但願球體在複雜的3D環境中移動,則必須支持與任意幾何圖形的交互。咱們將使用Unity現有的物理引擎,即NVIDIA的PhysX,而不是本身實現。
與物理引擎結合使用,有兩種通用的方法來控制角色。首先是剛體方法,即經過施加力或改變其速度,使角色的行爲像常規物理對象同樣,而間接控制它。第二種是運動學方法,即在僅查詢物理引擎執行自定義碰撞檢測的同時進行直接控制。
剛體組件
咱們將使用第一種方法來控制球體,這意味着咱們必須向其中添加一個Rigidbody
組件。咱們可使用剛體的默認配置。
添加該份量足以將咱們的球體變成一個物理對象,只要它仍然具備其SphereCollider
份量便可。從如今開始,咱們推遲到物理引擎進行碰撞,所以從中刪除區號Update
。
Vector3 newPosition = transform.localPosition + displacement;//if (newPosition.x < allowedArea.xMin) {// newPosition.x = allowedArea.xMin;// velocity.x = -velocity.x * bounciness;//}//…transform.localPosition = newPosition;
消除了咱們本身的約束後,球體再次能夠自由移動通過平面的邊緣,在此點,球體因爲重力而直線降低。發生這種狀況是由於咱們從不覆蓋球體的Y位置。
咱們再也不須要容許區域的配置選項。咱們的自定義跳動也再也不須要。
//[SerializeField, Range(0f, 1f)]//float bounciness = 0.5f;//[SerializeField]//Rect allowedArea = new Rect(-5f, -5f, 10f, 10f);
若是咱們仍然想約束球體保留在平面上,則能夠經過添加其餘對象來阻止其路徑來實現。例如,建立四個立方體,對其進行縮放和定位,以便它們圍繞平面造成一堵牆。這將防止球體掉落,儘管它在與牆壁碰撞時表現得很怪異。因爲此時咱們具備3D幾何形狀,所以再次啓用陰影以更好地瞭解深度也是一個好主意。
物理怪異。
當試圖移動到一個角落時,因爲物理引擎和咱們本身的代碼爭奪球形的位置,所以球形變得不穩定。咱們將其移入牆壁,而後PhysX經過將其向後推來解決碰撞。若是咱們中止將其推入牆壁,則PhysX將使球因爲動量而保持運動。
控制剛體速度
若是要使用物理引擎,則應讓它控制球體的位置。直接調整位置將有效地傳送,這不是咱們想要的。相反,咱們必須經過對球施加力或調整其速度來間接控制球。
咱們已經對位置進行了間接控制,由於咱們會影響速度。咱們要作的就是更改代碼,使其覆蓋Rigidbody
組件的速度,而不是本身調整位置。咱們須要爲此訪問組件,所以經過body
在Awake
方法中初始化的字段來跟蹤它。
Rigidbody body;void Awake () {body = GetComponent<Rigidbody>();}
從Update中刪除位移代碼,而是將咱們的速度分配給body的速度。
//Vector3 displacement = velocity * Time.deltaTime;//Vector3 newPosition = transform.localPosition + displacement;//transform.localPosition = newPosition;body.velocity = velocity;
可是物理碰撞等也會影響速度,所以請先將其從body中檢索出來,而後再對其進行調整以匹配所需的速度。
velocity = body.velocity;float maxSpeedChange = maxAcceleration * Time.deltaTime;velocity.x =Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);velocity.z =Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);body.velocity = velocity;
控制body的速度。
無摩擦運動
如今,咱們調整球體的速度,PhysX用來移動它。而後解決衝突,能夠調整速度,而後再次調整速度,依此類推。儘管球體更加緩慢而且沒有達到其最大速度,但最終的運動看起來像咱們之前的運動。那是由於PhysX會產生摩擦。儘管這更現實,但它使配置球體變得更加困難,所以讓咱們消除摩擦和反彈。這是經過「 資產/建立/物理材質」建立新的物理材質(是的,在菜單中拼寫爲「 Physic」),而後將全部值設置爲零,將「 合併」模式設置爲「 最小」。
將此物理材質分配給球體的對撞機。
如今,它再也不受到任何摩擦或反彈。
不建議不要直接調節速度嗎?
這是基於速度瞬時變化是不現實的想法的通用建議。咱們正在作的是有效地施加加速度,只是以一種受控的方式來達到目標速度。若是您知道本身在作什麼,直接調整速度就能夠了。
無摩擦運動。
與球體碰撞時,球體彷佛仍會反彈一點。發生這種狀況是由於PhysX不會阻止碰撞,而是會在碰撞發生後檢測到它們,而後移動剛體以使它們再也不相交。在快速運動的狀況下,這可能須要一個以上的物理模擬步驟,所以咱們能夠看到這種穿透現象的發生。
若是運動確實很是快,那麼球體可能最終會徹底穿過壁或朝另外一側穿透,這對於較薄的壁來講更可能發生。您能夠經過更改的Rigidbody碰撞檢測模式來避免這種狀況,但這一般僅在移動很是快時才須要。
並且,球體如今能夠滑動而不是滾動,所以咱們也能夠凍結其在全部尺寸上的旋轉,這能夠經過組件的「 約束」複選框來完成Rigidbody
。
固定更新
物理引擎使用固定的時間步長,而無論幀速率如何。儘管咱們已經將球的控制權交給了PhysX,但咱們仍然會影響其速度。爲了得到最佳結果,咱們應該以固定的時間步長同步調整速度。爲此,咱們將Update
方法分爲兩部分。咱們檢查輸入並設置所需速度的部分能夠保留在Update中,而速度的調整應移至新FixedUpdate
方法。爲了完成這項工做,咱們必須將所需的速度存儲在一個場中。
Vector3 velocity, desiredVelocity;
void Update () {Vector2 playerInput;playerInput.x = Input.GetAxis("Horizontal");playerInput.y = Input.GetAxis("Vertical");playerInput = Vector2.ClampMagnitude(playerInput, 1f);//Vector3 desiredVelocity =desiredVelocity =new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;}void FixedUpdate () {velocity = body.velocity;float maxSpeedChange = maxAcceleration * Time.deltaTime;velocity.x =Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);velocity.z =Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);body.velocity = velocity;}
FixedUpdate在每一個物理模擬步驟的開始都調用該方法。發生的頻率取決於時間步長,默認爲0.02(每秒50次),可是您能夠經過「 時間」項目設置或經過更改時間步長Time.fixedDeltaTime
。
根據您的幀速率FixedUpdate
,每次調用能夠調用0次,一次或屢次Update
。每一個框架都會發生一系列FixedUpdate
調用,而後Update
被調用,而後呈現框架。當物理時間步長相對於幀時間太大時,這可使物理仿真的離散性質變得明顯。
0.2物理時間步。
您能夠經過減小固定時間步長或啓用的Rigidbody插值模式來解決此問題。將其設置爲Interpolate可以使它在其最後位置和當前位置之間線性插值,所以根據PhysX,它會稍微落後於其實際位置。另外一個選項是Extrapolate,它根據其速度插值到其猜想的位置,這僅對於速度基本恆定的對象才真正可接受。
帶插值的0.2物理時間步長。
請注意,增長時間步長意味着球體在每次物理更新時覆蓋的距離更大,這可能致使使用離散碰撞檢測時球體穿過壁隧穿。
跳躍
因爲咱們的球體如今能夠在3D物理世界中導航,所以咱們可使其具備跳躍的能力。
根據指令跳躍
咱們能夠用Input.GetButtonDown("Jump")來檢測玩家是否按下了該幀的跳轉按鈕,默認狀況下是空格鍵。咱們在Update中這樣作,可是就像調整速度同樣,咱們會將實際的跳躍延遲到FixedUpdate的下次調用。所以,請經過布爾字段desiredJump跟蹤是否須要跳轉。
bool desiredJump;…void Update () {…desiredJump = Input.GetButtonDown("Jump");}
可是,咱們可能最終不調用FixedUpdate下一幀,在這種狀況下desiredJump將其調回false原定位置,而desiredJump 將被遺忘。咱們能夠經過布爾「或」運算或「或」分配將檢查與其先前的值相結合來防止這種狀況。這樣,它將保持true啓用狀態,直到咱們將其顯式設置回false。
desiredJump|=Input.GetButtonDown("Jump");
在調整速度以後和在FixedUpdate中應用速度以前,請檢查是否須要跳躍。若是是這樣,請重置desiredJump
並調用一個新Jump
方法,該方法最初僅將5添加到速度的Y份量,以模擬忽然的向上加速度。
void FixedUpdate () {…if (desiredJump) {desiredJump = false;Jump();}body.velocity = velocity;}void Jump() {velocity.y += 5f;}
這將使球體向上移動,直到因爲重力不可避免地回落。
跳。
跳躍高度
讓咱們對其範圍進行配置是可配置的。咱們能夠經過直接控制跳躍速度來作到這一點,但這並不直觀,由於初始跳躍速度和跳躍高度之間的關係並不微不足道。直接控制跳躍高度更方便,因此讓咱們開始吧。
[SerializeField, Range(0f, 10f)]float jumpHeight = 2f;
跳躍須要克服重力,所以所需的垂直速度取決於重力。特別,vÿ=--2GHv_y = sqrt(-2gh) 那裏 GG 是重力, HH是所需的高度。負號在那裏,由於GG假定爲負。咱們能夠經過檢索它Physics.gravity.y
,也能夠經過Physics項目設置進行配置。咱們正在使用默認的重力矢量,該矢量向下垂直爲9.81,與地球的平均重力匹配。
void Jump () {velocity.y +=Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}
如何得出所需的速度?
咱們從初始跳躍速度開始 Ĵ,它會因重力而減少,直到達到零,而後咱們開始回落。重力G是一個持續不斷的加速度,將咱們拉倒,爲此咱們在此推導中使用正數,由於這使咱們免於編寫大量負號。因此在任什麼時候候Ť 由於跳躍的垂直速度是 v = jg t。何時v達到零,咱們位於跳躍的頂部,所以正好位於所需的高度。這發生在jg t = 0,因此何時 j = gt。所以,當t = j /克。
由於 G 恆定,任什麼時候候的平均速度爲 v_(av)= j-(gt)/ 2,所以隨時的高度爲 h = v_(av)t = jt-(gt ^ 2)/ 2。這意味着在跳躍的頂端h = j(j / g)-(g(j / g)^ 2)/ 2,咱們能夠重寫爲 h = j ^ 2 / g-(j ^ 2 / g)/ 2 = j ^ 2 / g-j ^ 2 /(2g)= j ^ 2 /(2g)
如今咱們知道 h = j ^ 2 /(2g) 在頂部,所以 j ^ 2 = 2gh 和 j = sqrt(2gh)。何時G 是負數而不是 j = sqrt(-2gh)。
請注意,因爲物理模擬的離散性,咱們極可能沒法達到所需的高度。在時間步長之間的某個地方將達到最大值。
在地面的跳躍
目前,咱們能夠隨時跳下,即便已經在空中,也能夠永遠保持空中飛行。僅當球體在地面上時才能啓動適當的跳躍。咱們沒法直接詢問Rigidbody
它當前是否正在接觸地面,可是當它與某些物體碰撞時咱們會獲得通知,所以咱們將使用它。
若是MovingSphere
有一個OnCollisionEnter
方法,那麼它將在PhysX檢測到新的碰撞後被調用。只要物體保持彼此接觸,碰撞就仍然存在。以後,OnCollisionExit
將調用一個方法(若是存在)。將兩種方法都添加到MovingSphere中,將第一個 onGround
boolean字段設置爲true
,並將後者 boolean字段設置爲false
。
bool onGround;…void OnCollisionEnter () {onGround = true;}void OnCollisionExit () {onGround = false;}
如今咱們只能在地面上跳躍,如今咱們假設在觸摸某物時就是這種狀況。若是咱們不接觸任何東西,則應忽略指望的跳躍。
void Jump () {if (onGround) {velocity.y += Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}}
當球體僅接觸地面時,此方法有效,但若是它也短暫接觸牆,則跳躍將變得不可能。之因此發生這種狀況,是由於OnCollisionExit
在咱們仍與地面保持接觸的同時,它被做爲牆壁使用。解決方案是不依賴OnCollisionExit
而是添加一種OnCollisionStay
方法,只要碰撞仍然存在,就能夠在每一個物理步驟中調用該方法。設置onGround
於true
在該方法中。
//void OnCollisionExit () {// onGround = false;//}void OnCollisionStay () {onGround = true;}
每一個物理步驟都從調用全部FixedUpdate
方法開始,而後PhysX完成其工做,最後調用碰撞方法。所以,若是存在任何活動衝突,則在最後一步FixedUpdate期間將設置什麼時候調用gets 。爲了保持onGround有效,咱們要作的就是在FixedUpdate末尾將其onGround設置爲false。
void FixedUpdate () {…onGround = false;}
如今,只要咱們接觸到某物,咱們就能夠跳躍。
無牆跳躍
當觸摸任何東西時都容許跳躍意味着咱們也能夠在空中但觸摸牆壁而不是地面時跳躍。若是要防止這種狀況,咱們必須可以區分地面和其餘東西。
將地面定義爲主要是水平面是有意義的。咱們能夠經過檢查碰撞接觸點的法線向量來檢查咱們所碰撞的物體是否知足此條件。
什麼是法向量?
它是指示方向的單位長度向量。一般是遠離某物的方向。所以,一個平面只有一個法向量,而球體上的每一個點都有一個指向其中心的不一樣法線向量。
一個簡單的碰撞只有兩個形狀接觸的單個點,例如,當咱們的球體接觸地面時。一般,球體會稍微穿透平面,而PhysX經過將球體直接推離平面而解決了。推進的方向是接觸點的法線向量。由於咱們使用的是球體,因此矢量始終從球體表面上的接觸點指向其中心。
實際上,它可能比這更混亂,由於可能存在多個碰撞,而且穿透可能會持續一個以上的仿真步驟,可是咱們如今沒必要真正擔憂這一點。咱們確實須要認識到的是,一次碰撞能夠包含多個接觸。對於平面-球體碰撞,這是不可能的,可是當涉及到凹形網格對撞機時,這是可能的。
咱們能夠經過向和Collision
都添加一個參數來獲取碰撞信息。與其直接設置onGround 爲true,咱們不如將責任轉交給一種新方法EvaluateCollision ,並將數據給它。
void OnCollisionEnter (Collision collision) {//onGround = true;EvaluateCollision(collision);}void OnCollisionStay (Collision collision) {//onGround = true;EvaluateCollision(collision);}void EvaluateCollision (Collision collision) {}
能夠經過Collision的contactCount
屬性找到接觸點的數量。咱們可使用它經過該GetContact
方法遍歷全部點,併爲其傳遞索引。而後,咱們能夠訪問該點的normal
屬性。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;}}
法線是球應被推進的方向,該方向直接遠離碰撞表面。假設它是一個平面,則矢量與平面的法向矢量匹配。若是平面是水平的,則其法線將指向垂直,所以其Y份量應正好爲1。若是是這種狀況,則咱們正在接觸地面。可是,咱們要寬容一些,接受0.9或更大的Y份量。
Vector3 normal = collision.GetContact(i).normal;onGround |= normal.y >= 0.9f;
空中跳躍
在這一點上,咱們只能在地面上跳,可是遊戲一般容許空中跳兩次甚至三跳。讓咱們對此進行支持,並使其可配置爲容許多少次空氣跳躍。
[SerializeField, Range(0, 5)]int maxAirJumps = 0;
如今,咱們必須跟蹤跳轉階段,以便知道是否容許再次跳轉。若是咱們在地面上,咱們能夠經過在FixedUpdate開始時將其設置爲零的整數字段來執行此操做。可是,讓咱們將代碼與速度檢索一塊兒移動到單獨的UpdateState
方法中,以保持FixedUpdate
簡短。
int jumpPhase;…void FixedUpdate () {//velocity = body.velocity;UpdateState();…}void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;}}
從如今開始,每次跳躍時,咱們都會增長跳躍階段。咱們能夠在地面上或還沒有達到容許的最大空中跳躍時跳躍。
void Jump () {if (onGround|| jumpPhase < maxAirJumps) {jumpPhase += 1;velocity.y += Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}}
應該<= maxAirJumps不是嗎?
跳轉後,跳轉階段當即設置回零。在下一個教程中,咱們將找到緣由。
限制向上速度
快速連續跳躍的空氣使向上的速度比單次跳躍的速度高得多。咱們將進行更改,以使咱們不能超過單跳便可達到所需高度的跳速。第一步是隔離計算出的跳躍速度Jump
。
jumpPhase += 1;float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);velocity.y +=jumpSpeed;
若是咱們已經有向上的速度,則在將其添加到速度的Y份量以前,將其從跳躍速度中減去。這樣,咱們將永遠不會超過跳躍速度。
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);if (velocity.y > 0f) {jumpSpeed = jumpSpeed - velocity.y;}velocity.y += jumpSpeed;
可是,若是咱們已經快於跳躍速度,那麼咱們不但願跳躍使咱們減速。咱們能夠經過確保修改後的跳轉速度永遠不會變爲負值來避免這種狀況。經過採用修改後的最大跳躍速度和零來實現。
if (velocity.y > 0f) {jumpSpeed =Mathf.Max(jumpSpeed - velocity.y, 0f);}
空中運動
目前,咱們在控制球體時不在意球體是在地面上仍是在空中,但能夠理解,空中球體更難控制。控制的數量能夠在徹底控制和徹底控制之間變化。這取決於遊戲。所以,經過添加單獨的最大空氣加速度(默認設置爲1),使它可配置。這樣能夠大大減小空中控制,但不能徹底將其刪除。
[SerializeField, Range(0f, 100f)]float maxAcceleration = 10f, maxAirAcceleration = 1f;
如今,咱們在FixedUpdate計算最大速度變化時使用哪一種加速度取決於咱們是否在地面上。
float acceleration = onGround ? maxAcceleration : maxAirAcceleration;float maxSpeedChange =acceleration* Time.deltaTime;
連續下坡
咱們正在使用物理學在一個小的平面上移動球體,與牆碰撞並四處跳躍。一切都很好,所以是時候考慮更復雜的環境了。在本教程的其他部分中,咱們將研究涉及坡度時的基本運動。
ProBuilder測試場景
您能夠經過旋轉平面或立方體來建立坡度,但這是建立關卡的不便方法。所以,咱們將導入ProBuilder程序包,並使用該程序包建立一些坡度。該ProGrids包也駕輕就熟柵格捕捉,但若是你碰巧使用,它不是在統一2019.3須要。ProBuilder使用起來至關簡單,可是可能須要一些時間來適應。我不會解釋如何使用它,只是要記住,它主要是關於臉的,而邊緣和頂點是次要的。
我從ProBuilder立方體開始建立了一個坡度,將其拉伸到10×5×3,在X維度上將其拉伸了10個單位,而後將X面折疊到其底部邊緣。這將產生一個三角形的雙斜面,其兩側的斜率長爲10個單位,高爲5個單位。
我將其中十個放置在一個平面上,並將它們的高度從一單位更改成十個單位。包括平坦的地面在內,咱們得到的傾斜角度大約爲0.0°,5.7°,11.3°,16.7°,21.8°,26.6°,31.0°,35.0°,38.7°,42.0°和45.0°。
以後,我又放置了十個斜坡,此次是從45°版本開始,而後將筆尖向每一個傾斜的角度向左拉一個單位,直到最後獲得一面垂直牆。這給咱們提供了大約48.0°,51.3°,55.0°,59.0°,63.4°,68.2°,73.3°,78.7°,84.3°和90.0°的角度。
經過將球體變成預製件並添加21個實例(從每一個水平到徹底垂直),每一個坡度一個實例,我完成了測試場景。
若是您不想本身設計關卡,能夠從本教程的資源庫中獲取它。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-02-physics/
斜率測試
由於全部球體實例都響應用戶輸入,因此咱們能夠同時控制它們。這樣就能夠當即測試與多個傾斜角度相互做用時球體的行爲。對於大多數這些測試,我將進入播放模式,而後連續按向右鍵。
斜率測試。
使用默認球體配置,咱們能夠看到前五個球體以幾乎徹底相同的水平速度移動,而與傾斜角無關。第六個幾乎沒有通過,而其他的則回滾或被陡峭的斜坡徹底擋住了。
由於大多數球體都有效地結束了飛行,因此咱們將最大空氣加速度設置爲零。這樣,咱們只有在考慮到基礎上才考慮加速。
空氣加速與零空氣加速之間的差別並不重要,由於它們飛出了斜坡。可是第六球如今再也不到達另外一側,其餘球也因爲重力而提早中止。發生這種狀況是由於它們的坡度太陡而沒法保持足夠的動力。在第六球的狀況下,其空氣加速度足以將其推向上方。
接地角
目前,咱們使用0.9做爲閾值來將某物歸類爲不歸類,但這是任意的。咱們可使用0–1範圍內的任何閾值。嘗試兩個極端會產生很是不一樣的結果。
讓咱們經過控制最大地面角度使閾值可配置,由於最大地面角度比坡度法線向量的Y份量更直觀。讓咱們使用25°做爲默認值。
[SerializeField, Range(0f, 90f)]float maxGroundAngle = 25f;
當表面水平時,其法線向量的Y份量爲1。對於徹底垂直的牆,Y份量爲零。Y份量根據傾斜角度在這些極端之間變化:它是該角度的餘弦。咱們在這裏處理單位圓,其中Y是垂直軸,水平軸位於XZ平面中的某個位置。另外一種說法是,咱們正在查看向上矢量和表面法線的點積。
組態的角度定義了仍算做地面的最小結果。讓咱們的門檻存儲在一個領域,並經過Mathf.Cos計算它的一個OnValidate
方法。這樣,當咱們在播放模式下經過檢查器更改角度時,它將保持與角度同步。同時Awake調用它,以便在構建中對其進行計算。
float minGroundDotProduct;void OnValidate () {minGroundDotProduct = Mathf.Cos(maxGroundAngle);}void Awake () {body = GetComponent<Rigidbody>();OnValidate();}
咱們以度爲單位指定角度,但Mathf.Cos但願將其表示爲弧度。咱們能夠經過乘以Mathf.Deg2Rad將其轉換。
minGroundDotProduct = Mathf.Cos(maxGroundAngle* Mathf.Deg2Rad);
如今咱們能夠調整最大地面角度,看看它如何影響球體的運動。從如今開始,我將角度設置爲40°。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;onGround |= normal.y >=minGroundDotProduct;}}
在斜坡上跳躍
不管當前球面的角度如何,咱們的球體始終會直線向上跳躍。
另外一種方法是沿法線向量的方向跳離地面。每一個坡度測試車道都會產生不一樣的跳躍,因此讓咱們這樣作。
咱們須要跟蹤一個領域中的當前接觸法線,並在遇到地面接觸EvaluateCollision時將其存儲起來。
Vector3 contactNormal;…void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;//onGround |= normal.y >= minGroundDotProduct;if (normal.y >= minGroundDotProduct) {onGround = true;contactNormal = normal;}}}
可是,咱們最終可能沒有觸及地面。在這種狀況下,咱們將使用up向量做爲接觸法線,所以空氣跳躍仍然會直線上升。若是須要,將其在UpdateState中設置。
void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;}else {contactNormal = Vector3.up;}}
如今,咱們必須將按跳躍速度縮放的跳躍接觸法線添加到跳躍時的速度上,而不是始終僅增長Y份量。這意味着跳躍高度表示咱們在平坦地面或僅在空中時跳躍的距離。在斜坡上跳躍不會達到很高,但會影響水平速度。
void Jump () {if (onGround || jumpPhase < maxAirJumps) {…//velocity.y += jumpSpeed;velocity += contactNormal * jumpSpeed;}}
但這意味着對垂直速度爲正的檢查也再也不正確。它必須成爲檢查與接觸法線對齊速度的方法。咱們能夠經過將速度投影到接觸法線上並經過計算它們的點積Vector3.Dot來找到該速度。
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);float alignedSpeed = Vector3.Dot(velocity, contactNormal);if (alignedSpeed> 0f) {jumpSpeed = Mathf.Max(jumpSpeed -alignedSpeed, 0f);}velocity += contactNormal * jumpSpeed;
如今,這些跳躍與坡度對齊,咱們的測試場景中的每一個球體都具備惟一的跳躍軌跡。陡峭的斜坡上的球再也不直接跳入其斜坡,而是隨着跳躍將球朝與運動相反的方向推進而變慢。您能夠經過大幅下降最大速度來嘗試在全部斜坡上更清楚地看到這一點。
沿着斜坡移動
到目前爲止,不管傾斜角度如何,咱們始終在水平XZ平面中定義所需的速度。若是球體沿坡度上升,那是由於PhysX將球向上推以解決發生的碰撞,由於咱們給它指定了指向坡度的水平速度。在上坡時,這能夠很好地工做,可是在下坡時,球體會遠離地面移動,而且當它們的加速度足夠高時最終會掉落。結果是難以控制的彈性運動。在上坡時反轉方向時,尤爲是在將最大加速度設置爲較高值時,您能夠清楚地看到這一點。
失去接地;最大加速度100。
咱們能夠經過將所需速度與地面對齊來避免這種狀況。它的工做方式與咱們在法線上投影速度以得到跳躍速度的方式相似,只是如今咱們必須在平面上投影速度才能獲取新速度。咱們經過像之前同樣取向量和法線的點積,而後從原始速度向量中減去由該法線縮放的法線來作到這一點。讓咱們爲使用任意矢量參數的方法建立一個方法ProjectOnContactPlane。
Vector3 ProjectOnContactPlane (Vector3 vector) {return vector - contactNormal * Vector3.Dot(vector, contactNormal);}
爲何不使用Vector3.ProjectOnPlane?
該方法執行相同的操做,但不假定提供的法向向量具備單位長度。它將結果除以法線的平方長度(一般爲1,所以不須要)。
讓咱們建立一個新方法AdjustVelocity來調整速度。首先經過在接觸平面上投影右向向量和向前向量來肯定投影的X軸和Z軸。
void AdjustVelocity () {Vector3 xAxis = ProjectOnContactPlane(Vector3.right);Vector3 zAxis = ProjectOnContactPlane(Vector3.forward);}
這使咱們的向量與地面對齊,可是當地面徹底平坦時,它們只有單位長度。一般,咱們必須對向量進行歸一化以得到正確的方向。
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;
如今,咱們能夠將當前速度投影到兩個向量上,以得到相對的X和Z速度。
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;float currentX = Vector3.Dot(velocity, xAxis);float currentZ = Vector3.Dot(velocity, zAxis);
咱們能夠像之前同樣使用它們來計算新的X和Z速度,可是如今相對於地面。
float currentX = Vector3.Dot(velocity, xAxis);float currentZ = Vector3.Dot(velocity, zAxis);float acceleration = onGround ? maxAcceleration : maxAirAcceleration;float maxSpeedChange = acceleration * Time.deltaTime;float newX =Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);float newZ =Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);
最後,經過沿相對軸添加新舊速度之間的差別來調整速度。
float newX =Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);float newZ =Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
FixedUpdate
代替舊的速度調節代碼,調用此新方法。
void FixedUpdate () {UpdateState();AdjustVelocity();//float acceleration = onGround ? maxAcceleration : maxAirAcceleration;//float maxSpeedChange = acceleration * Time.deltaTime;//velocity.x =// Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);//velocity.z =// Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);if (desiredJump) {desiredJump = false;Jump();}body.velocity = velocity;onGround = false;}
與地面保持一致;最大加速度100。
使用咱們新的速度調整方法,當在斜坡上忽然忽然反轉方向時,球再也不與地面失去接觸。除此以外,因爲指望速度會調整其方向以匹配斜率,所以如今每一個車道都會改變絕對指望水平速度。
請注意,若是坡度未與X軸或Z軸對齊,則相對投影軸之間的角度將不爲90°。除非斜坡很是陡峭,不然這並非很明顯。您仍然能夠在全部方向上移動,可是要精確地在某些方向上進行導航比在其餘方向上更難。這在某種程度上模仿了試圖穿越但不與陡坡對齊的尷尬。
多個地面法線
當只有一個地面接觸點時,使用接觸法線來調整所需的速度和跳躍方向效果很好,可是當同時存在多個地面接觸時,行爲可能會變得奇怪且不可預測。爲了說明這一點,我建立了另外一個測試場景,該測試場景的地面有些凹陷,一次最多能夠有四個接觸點。
跳躍時,球體會朝哪一個方向前進?就我而言,擁有四個聯繫人的人傾向於偏向一個方向,但最終會朝四個不一樣方向前進。一樣,具備兩個接觸的球體在兩個方向之間任意拾取。具備三個接觸的球始終以相同的方式跳躍,以匹配僅接觸單個坡度的附近球。
出現這種現象的緣由是,只要咱們發現地面接觸點,便將法線設置爲EvaluateCollision。所以,若是咱們發現多個,則最後一個贏。因爲移動的順序是任意的,或者因爲PhysX計算碰撞的順序,順序老是相同的。
哪一個方向最好?沒有一個。將它們所有組合成一個表明平均接地平面的法線是最有意義的。爲此,咱們必須累積法線向量。這就要求咱們在FixedUpdate的末尾將接觸法線設置爲零。讓咱們將代碼與onGround重置一塊兒放入新方法ClearState中。
void FixedUpdate () {…body.velocity = velocity;//onGround = false;ClearState();}void ClearState () {onGround = false;contactNormal = Vector3.zero;}
如今在EvaluateCollision累積法線而不是覆蓋前一個法線。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;if (normal.y >= minGroundDotProduct) {onGround = true;contactNormal+=normal;}}}
最後,將UpdateState中在地面上的接觸法線歸一化以使其成爲適當的法線向量。
void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;contactNormal.Normalize();}else {contactNormal = Vector3.up;}}
地面接觸點計算
雖然不是必需的,但咱們能夠算出咱們有多少個地面接觸點,而不只僅是跟蹤是否至少有一個。咱們經過將布爾字段替換爲整數來作到這一點。而後,咱們引入一個布爾型只讀屬性OnGround(注意大小寫),該屬性檢查計數是否大於零,並替換該onGround
字段。
//bool onGround;int groundContactCount;bool OnGround => groundContactCount > 0;
該代碼如何工做?
這是定義單語句只讀屬性的一種簡便方法。與如下內容相同:
bool
OnGround {
get
{
return
groundContactCount > 0;
}
}
ClearState
如今必須將計數設置爲零。
void ClearState () {//onGround = false;groundContactCount = 0;contactNormal = Vector3.zero;}
而且UpdateState
必須依靠屬性而不是字段。除此以外,咱們還能夠經過僅對接觸法線進行歸一化(若是是聚合的話)進行歸一化來進行一些優化,不然它已是單位長度了。
void UpdateState () {velocity = body.velocity;if (OnGround) {jumpPhase = 0;if (groundContactCount > 1) {contactNormal.Normalize();}}…}
還要在Evaluate
適當的時候增長計數。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;if (normal.y >= minGroundDotProduct) {//onGround = true;groundContactCount += 1;contactNormal += normal;}}}
最後,用OnGround
的AdjustVelocity
和Jump
更換onGround
。
除了UpdateState中地面接觸數量的優化,對調試也頗有用。例如,您能夠記錄計數或根據計數調整球體的顏色,以更好地瞭解其狀態。
您是如何改變顏色的?
我將如下代碼添加到Update:
GetComponent<Renderer>().material.SetColor(
"_Color", Color.white * (groundContactCount * 0.25f)
);
假定球體的材質具備_Color屬性,默認渲染管線的標準着色器就是這種狀況。若是您使用的是Lightweight / Universal管道的默認着色器,則須要使用_BaseColor。
下一個教程是表面接觸(Surface Contact)。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-02-physics/
往期精選
Unity3D遊戲開發中100+效果的實現和源碼大全 - 收藏起來確定用得着
聲明:發佈此文是出於傳遞更多知識以供交流學習之目的。如有來源標註錯誤或侵犯了您的合法權益,請做者持權屬證實與咱們聯繫,咱們將及時更正、刪除,謝謝。
原做者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/physics/
翻譯、編輯、整理:MarsZhou
More:【微信公衆號】 u3dnotes