黑魂復刻遊戲的玩家控制器(鎖定狀態)——Unity隨手記
今天實現的內容:
新增鎖定輸入
為輸入模組新增鎖定鍵和鎖定訊號,更新訊號。
--- IPlayerInput public bool lockOn; //鎖定訊號 --- JoystickInput public MyButton buttonLockOn = new MyButton(); //鎖定鍵 // Update is called once per frame void Update() { // 更新按鍵 buttonLockOn.Tick(Input.GetButton(btnRS));// 鎖定訊號 lockOn = buttonLockOn.onPressed;
鎖定和解鎖的程式碼邏輯和攝像機程式碼邏輯
首先,要鎖定目標,先要確定要鎖定的目標是什麼。我們使用Physics.OverlapBox來得到指定盒子區域內的碰撞體。
鎖定需要始終將攝像機對準目標。所以我們在CameraController中新增新方法LockOn_or_Unlock
用來處理鎖定解鎖邏輯,然後在PlayerController中呼叫該方法。
// 攝像機鎖定/解除鎖定 public void LockOn_or_Unlock() { // 嘗試去鎖定一個Vector3 tmp_modelCenter = modelGO.transform.position + Vector3.up; //獲得模型的中心 Vector3 tmp_boxCenter = tmp_modelCenter + modelGO.transform.forward * 5.0f; //得到OverlapBox的中心 Collider[] cols = Physics.OverlapBox(tmp_boxCenter, //通過OverlapBox嘗試獲取範圍內Enemy標籤的碰撞體 new Vector3(1.0f, 1.0f, 5.0f), modelGO.transform.rotation, LayerMask.GetMask("Enemy")); if (cols.Length != 0 && lockTarget == null) { //如果得到碰撞體並且沒有鎖定目標 將第一個賦值給lockTarget lockTarget = cols[0].gameObject; lockonIcon.enabled = true; // 設定LockonIcon的圖片位置 lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position); isLockon = true; } else { // 如果沒有檢測到任何東西或者已經鎖定了目標 將lockTarget設定為null lockTarget = null; lockonIcon.enabled = false; isLockon = false; } }
LockOn_or_Unlock會使用OverlapBox檢視給定範圍內是否有碰撞體並且是否已經鎖定目標,如果找到了碰撞體,將陣列中的第一個給lockTarget,如果沒找到或已經鎖定了目標則設定為null。
如果攝像機沒有鎖定,則在FixedUpdate中將playerController按輸入設定旋轉。如果鎖定了,則攝像機要計算模型和鎖定目標的方向向量,把這個方向向量交給攝像機,在我們的架構中,直接用來設定PlayerController的forward就行,記得要將該向量的y軸設定為0。最後,我們會在鎖定時讓攝像機始終看向目標的“腳底”來讓視角更貼近黑魂。
void FixedUpdate() { if (lockTarget == null) //如果沒有鎖定目標 按輸入控制PlayerController旋轉 { // 得到攝像機旋轉前的模型尤拉角 Vector3 temp_modelEuler = modelGO.transform.eulerAngles; // 左右旋轉時直接旋轉PlayerHandle 攝像機也會跟著 playerController.transform.Rotate(Vector3.up, current_pi.cameraRight * horizontalSensitivity * Time.fixedDeltaTime); // 攝像機旋轉後將模型原來的尤拉角再賦給模型 保證模型不動 modelGO.transform.eulerAngles = temp_modelEuler; // 上下旋轉時旋轉CameraHandle temp_eulerX -= current_pi.cameraUp * verticalSensitivity * Time.fixedDeltaTime; // 限制俯仰角 temp_eulerX = Mathf.Clamp(temp_eulerX, -40, 30); // 賦值localEulerAngles cameraHandle.transform.localEulerAngles = new Vector3(temp_eulerX, 0, 0); } else //如果鎖定了目標 計算從模型到鎖定目標的方向向量 將PlayerController的forward設定為該向量 { // 計算方向向量 Vector3 temp_forward = lockTarget.transform.position - modelGO.transform.position; // 將向量的y軸設定為0 PlayerController的Y軸不需要旋轉 temp_forward.y = 0; // 設定PlayerController的forward playerController.transform.forward = temp_forward; // 鎖定攝像機看目標的“腳底” cameraHandle.transform.LookAt(lockTarget.transform.parent.position); } // 攝像機的位置通過SmoothDamp來實現一種延遲移動的效果 cameraGO.transform.position = Vector3.SmoothDamp( cameraGO.transform.position, this.transform.position, ref temp_dampValue, 0.1f * current_pi.dirMag); // 讓攝像機保持看向一個位置 防止位置進行SmoothDamp時的抖動 cameraGO.transform.LookAt(cameraHandle.transform); }
鎖定的提示UI
為了在遊戲中提示玩家當前鎖定了哪個目標,我們要加入一個UI。
需要在沒有進行鎖定時將其enabled設定為false。只在鎖定時將其設定為true。最後將UI的位置放到鎖定的物件身上。
private void Update() { if(isLockon) { // 更新LockonIcon的圖片位置 lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position); } }
鎖定的控制器程式碼邏輯
鎖定時控制器中的程式碼和攝像機類似,一個是設定攝像機對準目標,一個是設定模型對準目標。只有當模型對準目標,我們才能引入鎖定時相應的動畫。同樣重要的還有鎖定狀態下的移動計算,由於鎖定時模型和PlayerController的方向都已鎖死,此時方向的判斷不根據模型了,而是直接來自輸入的產生方向,我們在輸入模組中用dirVec來表示。
// -- PlayerController -- // Update is called once per frame void Update() { // ... // 觸發鎖定 if (current_pi.lockOn) { camCon.LockOn_or_Unlock(); } if (camCon.isLockon) //攝像機已鎖定 將模型旋轉設定為朝向目標 { // 由於已經在CameraController設定了PlayerController物件的旋轉所以直接給模型就行 model.transform.forward = this.transform.forward; // 計算鎖定時的移動 if (!lockPlanar) { m_planarVec = current_pi.dirVec * walkSpeed * (current_pi.run ? runMultiplier : 1.0f); } } else //攝像機沒有鎖定 根據輸入控制模型旋轉 { // 只在有速度時能夠旋轉 防止原地旋轉 if (current_pi.dirMag > 0.1f) { // 運用旋轉 使用Slerp進行效果優化 model.transform.forward = Vector3.Slerp(model.transform.forward, current_pi.dirVec, 0.3f); } // 計算沒有鎖定時的移動量 if (!lockPlanar) { m_planarVec = current_pi.dirMag * model.transform.forward * walkSpeed * (current_pi.run ? runMultiplier : 1.0f); } } }
總結一下,由於我們水平旋轉攝像機是靠旋轉PlayerController遊戲物件來實現,所以鎖定目標將PlayerController遊戲物件的forward指向目標就行。同樣的,由於已經設定了PlayerController在鎖定時指向目標,要將模型指向目標只需要將PlayerController物件的forward給模型就行。移動的方向判斷需要通過輸入產生的方向,也就是我們之前做輸入模組時寫的dirVec來判斷。
鎖定的動畫
之前說要將模型始終對準目標才能引入鎖定對應的動畫,鎖定對應的動畫就是始終朝向一個方向時的前進後退和左右側步。要加入這些動畫,我們需要將ground混合樹修改為2D Freeform型別。原來的移動依然只由forward引數操控,我們要額外加入right引數,在鎖定時,我們將通過修改right引數操控左右側步,以及調整forward為負值來操控後退。這樣還不需要加入新混合樹就可以實現原來的移動和鎖定時的移動了。
至於動畫引數的程式碼,結構和之前的邏輯類似,在鎖定狀態下要調整的是forward和right,非鎖定狀態下只調整forward就行。具體程式碼如下:
// Update is called once per frame void Update() { // --------------------- 動畫引數 --------------------- if (camCon.isLockon) { Vector3 localDirVec = this.transform.InverseTransformDirection(current_pi.dirVec); anim.SetFloat("forward", localDirVec.z * ((current_pi.run) ? 2.0f : 1.0f)); anim.SetFloat("right", localDirVec.x * ((current_pi.run) ? 2.0f : 1.0f)); } else { anim.SetFloat("forward", current_pi.dirMag * Mathf.Lerp(anim.GetFloat("forward"), (current_pi.run) ? 2.0f : 1.0f, 0.5f)); anim.SetFloat("right", 0); } // ... }
鎖定狀態下的翻滾和跳躍
由於鎖定狀態下模型方向判斷和之前不一樣了,所以我們需要重新設計鎖定狀態下的翻滾和後跳。設計方案是,設定一個新的bool值trackDirection,當該bool為true時,將追蹤m_planarVec,提供給翻滾後跳作為方向。當我們開始跳躍或翻滾時,將trackDirection設定為true。落地時設定為false。
// 進入Base層的動畫節點roll時執行的方法 // 通過PlayerController動畫機中的roll節點上掛載的FSMOnEnter呼叫 public void OnRollEnter() { // 關閉輸入模組 current_pi.inputEnabled = false; // 鎖定平臺移動計算 lockPlanar = true; // 運用翻滾衝量 m_planarVec = m_planarVec.normalized * rollThrust; // 追蹤dirVec trackDirection = true; }
當鎖定時,要旋轉模型的時候,如果trackDirection為true,則按照m_planarVec作為方向。
// --------------------- 模型旋轉和位移 --------------------- if (camCon.isLockon) //攝像機已鎖定 將模型旋轉設定為朝向目標 { if(trackDirection) //當前是否追蹤方向 { model.transform.forward = m_planarVec.normalized; } else { // 由於已經在CameraController設定了PlayerController物件的旋轉所以直接給模型就行 model.transform.forward = this.transform.forward; }
最後,將動畫機調整一下,將right引數考慮進翻滾和跳躍。
自動解除鎖定
到目前為止,我們的鎖定功能基本刊用了,但是每當遊戲玩家想要解除鎖定時必須再次按下鎖定鍵,否則無論跑出多遠都會鎖定到目標上,從遊戲設計的角度來講,玩家跑出很遠的距離一般都是希望取消鎖定的,而且如果在很遠距離都能鎖定敵人會導致一些追蹤魔法能打很遠。所以最後我們再做一個自動解除鎖定功能。
通過計算玩家和目標的距離,如果距離大於某個值,則自動執行解鎖。
private void Update() { // 如果鎖定了目標 if(isLockon) { // 更新LockonIcon的圖片位置 lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position); // 超出某個距離自動解除鎖定 if (Vector3.Distance(lockTarget.transform.position, modelGO.transform.position) > 10f) { lockTarget = null; lockonIcon.enabled = false; isLockon = false; } } }