用Unity重現《空洞騎士》的苦痛之路(2)——人物控制篇
用Unity重現《空洞騎士》的苦痛之路(1):動作篇
用Unity重現《空洞騎士》的苦痛之路(2)——人物控制篇
用Unity重現《空洞騎士》的苦痛之路(3)——地圖篇
用Unity重現《空洞騎士》的苦痛之路(4)——特效篇
本篇難度:★★★☆☆
大家好。受苦受難的蟲子們啊,我又來繼續了。
小姐姐這廂有禮了
緊接上一期的內容,這期主要講解人物控制的程式碼。
程式碼邏輯塊相對複雜,並且程式碼較多,本期將會挑選重點進行講解,程式碼不會完全貼出(太長了),所以大部分方法都不是完整的,細節方面還請下載文章末尾的工程,開啟檢視。
本期相對於上篇難度巨幅提高,如果說上篇是normal那這篇就是very hard。食用時請注意別噎著。
移動核心邏輯(難)
為了讓遊戲中玩家的移動能夠完全被開發者掌握,本工程並沒有使用Unity的物理引擎來進行移動操作,而是手動模擬相應的物理特性並進行移動。
在2D遊戲中,玩家的移動總是能夠分解成2個方向上的位移。我們移動的邏輯實現也是這樣。
首先獲取到這一幀的移動速度,然後計算出2個方向上的位移,再計算出下一個位置是否能夠進行移動。如果無法移動,就需要對下一幀該方向的移動邏輯進行修正後再實施移動。原理如下圖:
移動原理
其中關於位置修正,我們還得手動打Box射線來檢測是否碰撞,並根據結果進行相應的操作。原理如下:
位移修正原理
請注意:由於我們是先移動X軸,在移動Y軸,並沒有進行線性的移動,會在特定情況下捨棄部分位移(即位置出現偏差)。但是由於我們的移動是每一幀進行的,此處的誤差實際上可以忽略,強迫症患者可以考慮在這個方法基礎上進行修改。
程式碼實現需要獲取到當前幀的速度,並且根據速度計算出這幀玩家給個方向的位移,用於下面的計算。然後根據移動的方向,分開處理。
先處理左右方向的位移,然後使用box射線檢測,判斷下一幀是否會碰到碰撞體或者陷阱,如果是牆壁(碰撞體),還需要通過碰撞點的位置進行修正,達到滿意的移動效果。同時根據需求,更新對應的動畫狀態。程式碼如下:
- public Vector3 moveSpeed; //每一幀的移動速度
- public Vector2 boxSize; //玩家碰撞盒的大小
- public void CheckNextMove()
- {
- Vector3 moveDistance = moveSpeed * Time.deltaTime;//當前幀的移動位移
- if (moveSpeed.x!= 0)//當左右速度有值時
- {
- RaycastHit2D lRHit2D = Physics2D.BoxCast(transform.position, boxSize, 0, Vector2.right * moveSpeed.x, 5.0f, playerLayerMask);
- if (lRHit2D.collider != null)//如果當前方向上有碰撞體
- {
- float tempXVaule = (float)Math.Round(lRHit2D.point.x, 1); //取X軸方向的數值,並保留1位小數精度。防止由於精度產生鬼畜行為
- Vector3 colliderPoint = new Vector3(tempXVaule, transform.position.y); //重新構建射線的碰撞點
- float tempDistance = Vector3.Distance(colliderPoint, transform.position); //計算玩家與碰撞點的位置
- if (tempDistance > (boxSize.x * 0.5f + distance)) //如果距離大於 碰撞盒子的高度的一半+最小地面距離
- {
- transform.position += new Vector3(moveDistance.x, 0, 0); //說明此時還能進行正常移動,不需要進行修正
- }
- else//如果距離小於 根據方向進行位移修正
- {
- float tempX = 0;//新的X軸的位置
- if (moveSpeed.x> 0)
- {
- tempX = tempXVaule - boxSize.x * 0.5f - distance + 0.05f; //多加上0.05f的修正距離,防止出現由於精度問題產生的鬼畜行為
- }
- else
- {
- tempX = tempXVaule + boxSize.x * 0.5f + distance - 0.05f;
- }
- transform.position = new Vector3(tempX, transform.position.y, 0);//修改玩家的位置
- }
- }
- else
- {
- transform.position += new Vector3(moveDistance.x, 0, 0);
- }
- }
- }
同理,上下也是一樣的。只不過需要考慮受到重力的情況。在不進行上下移動的時候,加上判斷是否在地面的功能,用來決定是否新增重力。這可以通過朝地面打射線的方式進行判斷。
程式碼如下:
- public bool CheckIsGround()
- {
- RaycastHit2D hit2D = Physics2D.BoxCast(transform.position, boxSize, 0, Vector2.down,5f, playerLayerMask);
- if (hit2D.collider != null)
- {
- float tempDistance = Vector3.Distance(transform.position, hit2D.point);
- if (tempDistance > (boxSize.y * 0.5f + distance))//如果距離大於 碰撞盒子的高度的一半+最小地面距離
- {
- return false;
- }
- else
- {
- return true;
- }
- }
- else
- {
- return false;
- }
- }
左右移動
完成了上面主要移動核心邏輯、並在Update函式呼叫後,接下來的內容就要稍微簡單一些。
由於實際的移動是由別的函式來進行實現,那麼左右移動函式就只需要根據玩家的按鍵輸入,來更新玩家的速度即可。如下:
- public void LRMove()
- {
- float h = Input.GetAxis("Horizontal");
- moveSpeed.x = h * speed;//更新左右軸上的速度
- //接下來更新各種動畫狀態
- }
跳躍
在《空洞騎士》中,跳躍是根據按鍵的蓄力時長,來控制跳躍高度。如果我們需要實現這個功能,就需要在KeyDown事件中觸發跳躍,key事件中進行蓄力,KeyUp事件中停止跳躍蓄力,跳躍狀態取消,並更新動畫。邏輯圖如下:
跳躍功能邏輯圖
根據上面的邏輯圖實現的跳躍程式碼如下:
- public float jumpTime; //跳躍的最大蓄力時間
- float timeJump; //跳躍當前的蓄力時間
- public void Jump()
- {
- if (Input.GetKeyDown(KeyCode.Space))
- {
- jumpState = true; //進入跳躍狀態
- moveSpeed.y += jumpPower;//初始新增向上的力
- timeJump = 0;//蓄力時間清零
- }
- else if (Input.GetKey(KeyCode.Space) && jumpCount<=2 && jumpState)
- {
- timeJump += Time.deltaTime;//蓄力時間增加
- if (timeJump < jumpTime)
- {
- moveSpeed.y += jumpPower;//蓄力
- }
- }
- else if (Input.GetKeyUp(KeyCode.Space))
- {
- jumpState = false;//退出跳躍狀態
- timeJump = 0;//蓄力時間清零
- }
- }
二段跳實現的方法原理也是一樣,只是需要在KeyDown事件中,區分當前是二段跳還是一段跳,並分別進行蓄力操作即可。
暗影衝刺
在衝刺狀態下,玩家不受到重力,且此時不會接受按鍵輸入。於是我們需要宣告2個變數分別用來控制是否獲取按鍵輸入,以及應用重力。並在衝刺開始的時候關閉對應的開關,給玩家一個固定的移動速度,在衝刺結束後重新開啟對應的開關,速度回滾。邏輯圖如下:
暗影衝刺邏輯圖
實現的程式碼如下:
- public bool gravityEnable; //重力開關
- public bool inputEnable; //接受輸入開關 true 遊戲接受按鍵輸入 false不接受按鍵輸入
- public void SprintFunc()
- {
- if (Input.GetKeyDown(KeyCode.J) && isCanSprint)
- {
- StartCoroutine(SprintMove(sprintTime));//衝刺協程
- }
- }
- IEnumerator SprintMove(float time)
- {
- inputEnable = false;
- gravityEnable = false;//關閉按鍵輸入,以及不在應用重力
- moveSpeed.y = 0;//Y軸速度清零
- isCanSprint = false;
- if (nowDir == PlayDir.Left)
- {
- moveSpeed.x = 15*-1;
- }
- else
- {
- moveSpeed.x = 15;
- }//根據方向施加速度
- yield return new WaitForSeconds(time);//延遲time秒後執行
- inputEnable = true;
- gravityEnable = true;
- isCanSprint = true;//狀態回滾
- }
超級衝刺原理同上,但是需要考慮到需要蓄力,並且該狀態結束是由按鍵與移動進行控制的。所以我們在實現超級衝刺的時候,需要用兩種方式來結束。思路圖如下:
超級衝刺邏輯圖
程式碼就不貼出了,歡迎參考工程食用。
爬牆切換
原版遊戲裡,在空中時碰到特定牆壁會進入到爬牆的狀態。
條件是下一幀需要碰到牆壁,且距離地面有一定的高度才行。程式碼如下:
- public void EnterClimpFunc(Vector3 rayPoint) //移動檢測到下一幀碰到牆壁時呼叫
- {
- //設定碰到牆 且 從碰撞點往下 玩家碰撞盒子高度內 沒有碰撞體 就可進入碰撞狀態。
- RaycastHit2D hit2D = Physics2D.BoxCast(rayPoint, boxSize, 0, Vector2.down, boxSize.y, playerLayerMask);
- if (hit2D.collider != null)
- {
- Debug.Log("無法進入爬牆狀態 "+ hit2D.collider.name);
- }
- else
- {
- playAnimator.SetTrigger("IsClimb");//動畫切換
- isClimb = true;
- isCanSprint = true; //爬牆狀態,衝刺重置
- }
- }
有3種方式可以退出爬牆狀態:
1.玩家自己受重力下落,超出牆壁的範圍。
2.玩家按下跳躍鍵退出。
3.玩家按下反方向移動鍵退出。
這裡只提一下跳躍退出實現的原理。比較簡單,就不貼出程式碼了。在按下跳躍鍵後朝著牆壁的反方向施加一個朝上的力,就可使玩家離開碰撞體,退出爬牆狀態。圖示如下:
跳躍退出實現原理圖
攻擊互動
在《空洞騎士》中,攻擊碰到特定的物體時,會有後座力的效果,而且會重新整理玩家身上特定的技能狀態。實現邏輯就是在攻擊的時候,進行射線檢測,並根據碰撞體的標籤,以及攻擊的方向,來決定狀態的重新整理,以及是否施加後坐力的效果。邏輯圖大致如下:
攻擊判定邏輯圖
在程式碼中的實現:
- public void AttackFunc()
- {
- if (Input.GetKeyDown(KeyCode.K))//按下攻擊鍵
- {
- CheckAckInteractive((int)nowDir);
- }
- }
- public void CheckAckInteractive(int dir) //引數為攻擊的方向
- {
- float distance = 1.8f; //射線的檢測長度
- RaycastHit2D hit2D = new RaycastHit2D();
- Vector2 raySize = new Vector2(boxSize.x + 0.5f, boxSize.y); //擴大檢測X軸範圍
- switch (dir)
- {
- case 1:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.left, distance, playerLayerMask);
- break;
- case 2:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.right, distance, playerLayerMask);
- break;
- case 3:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.up, distance, playerLayerMask);
- break;
- case 4:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.down, distance, playerLayerMask);
- break;
- }
- if (hit2D.collider!=null)
- {
- if (hit2D.collider.gameObject.CompareTag("Trap")) //如果是陷阱就有後坐力
- {
- StartCoroutine(InteractiveMove(dir, 10)); //開啟協程 施加後座力效果,並重新整理狀態
- }
- }
- }
結語
完成了本期文章的內容後,我們的主角在動作方面已經完全滿足了這個專案的要求。
接下來需要完善的就是發揮自己的創意,搭建快樂地圖啦。而且在本期文章中,考慮到有的童鞋可能動畫切換這塊的想法跟我不一致,我有意刪減了動畫切換相關的程式碼。若是需要參考的話,歡迎點選文章末尾的下載連結下載後進行檢視。(PS:我使用的是2017.4.17f1版本)
工程下載連結
連結:https://pan.baidu.com/s/1wJTQmSup2EOOdGBYVmYvVw提取碼:etvn
相關連結,很(mai)重(mai)要(mai)
空洞騎士購買連結:https://store.steampowered.com/app/367520/Hollow_Knight/
有線下學習遊戲開發打算的童鞋,歡迎訪問http://levelpp.com/。
線上課程的傳送門則如下:簡明易懂的C#入門指南-網易雲課堂study.163.com
另有專業開發交(gao)流(ji)群等待大家強勢插入:869551769
作者:繁華如夢
專欄地址:https://zhuanlan.zhihu.com/p/58882234
相關文章
- 用Unity重現《空洞騎士》的苦痛之路(4)——特效篇Unity特效
- 用Unity重現《空洞騎士》的苦痛之路(3)——地圖篇Unity地圖
- 用Unity重現《空洞騎士》的苦痛之路(1):動作篇Unity
- 《空洞騎士》關卡設計解析
- 《黑暗之魂》,《空洞騎士》和《赤痕》共同的“祖宗”是誰?
- 開放世界關卡設計:《空洞騎士》的奧祕,2D地圖如何實現探索感?地圖
- 《空洞騎士》製作人訪談:一致性的藝術
- AI訓練的空洞騎士能打敗大黃蜂小姐姐嗎?AI
- 《空洞騎士》為何讓人意猶未盡?聊聊碎片化敘事的魅力
- 《ENDER LILIES》:“蘿莉版《空洞騎士》”不足以表達對它的期待
- 萬字長文乾貨分享:類《空洞騎士》關卡設計研究
- 《EDGE》:12月31日公佈《空洞騎士:絲之歌》獨家新聞
- 《骷髏騎士:重製版》如何實現經典原作的初衷?
- 析電子遊戲《空洞騎士》場景切換時的音樂音響處理遊戲
- 《空洞騎士》:我們為什麼深愛這款玩起來看著像是自虐的遊戲遊戲
- 重構之路:開篇
- 一個看似不起眼的市場,卻孕育出了空洞騎士、無題大鵝和Florence等創意遊戲遊戲
- Unity3d 人物的跳躍Unity3D
- 鏟子騎士:2D遊戲,3D引擎打造遊戲3D
- 用科幻藝術描繪未知的魅力-人物篇
- 死亡騎士形象是如何誕生和成型的
- 暴雪:一位屈服於時代的騎士
- 2-39. 建立 TransitionManager 控制人物場景切換
- 本土手繪 Roguelike 《形骸騎士》的優勢與不足
- 《蒼之騎士團2》iOS預約開啟!遊戲動態大曝光!iOS遊戲
- 騎士放置 Page435 最大獨立集
- 《鏟子騎士》:“復古遊戲”的集大成者遊戲
- 用Unity做半個2D戰棋小遊戲(四):加入玩家控制Unity遊戲
- 2020餓了麼藍騎士群體畫像
- PAINGATE 苦痛系AI
- 【每日一題】 688. 騎士在棋盤上的機率每日一題
- 《國王的恩賜2》聖騎士艾爾莎的祝福 最後一位可控角色揭曉
- 餓了麼:2020年00後藍騎士報告
- 用Unity實現彈反效果Unity
- 怎麼修復網站漏洞騎士cms的漏洞修復方案網站
- 《蒼之騎士團2》“宿命之約”首測定檔!限量測試即將開啟
- wwdc見聞之人物篇
- Unity控制把執行Unity