Unity開發2D類銀河惡魔城游戲學習筆記
Unity教程(零)Unity和VS的使用相關內容
Unity教程(一)開始學習狀態機
Unity教程(二)角色移動的實現
Unity教程(三)角色跳躍的實現
Unity教程(四)碰撞檢測
Unity教程(五)角色沖刺的實現
Unity教程(六)角色滑墻的實現
Unity教程(七)角色蹬墻跳的實現
Unity教程(八)角色攻擊的基本實現
Unity教程(九)角色攻擊的改進
Unity教程(十)Tile Palette搭建平臺關卡
Unity教程(十一)相機
Unity教程(十二)視差背景
Unity教程(十三)敵人狀態機
Unity教程(十四)敵人空閑和移動的實現
Unity教程(十五)敵人戰斗狀態的實現
Unity教程(十六)敵人攻擊狀態的實現
Unity教程(十七)敵人戰斗狀態的完善
Unity教程(十八)戰斗系統 攻擊邏輯
Unity教程(十九)戰斗系統 受擊反饋
Unity教程(二十)戰斗系統 角色反擊
Unity教程(二十一)技能系統 基礎部分
Unity教程(二十二)技能系統 分身技能
Unity教程(二十三)技能系統 擲劍技能(上)基礎實現
Unity教程(二十四)技能系統 擲劍技能(中)技能變種實現
如果你更習慣用知乎
Unity開發2D類銀河惡魔城游戲學習筆記目錄
文章目錄
- Unity開發2D類銀河惡魔城游戲學習筆記
- 前言
- 一、概述
- 二、投劍相關狀態的創建與實現
- (1)創建PlayerAimSwordState和PlayerCatchSwordState
- (2)創建瞄準與接劍的動畫
- (3)實現瞄準投擲的狀態轉換
- 三、投劍技能的創建與實現
- (1)創建Sword物體
- (2)創建Sword閑置和旋轉動畫
- (3)創建投劍技能及控制器腳本
- (4)投擲劍的實現
- 四、瞄準的實現
- (1)瞄準的實現
- (2)顯示瞄準點和路徑
- (3)劍的旋轉與嵌入
- (4)解決投劍方向的問題
- (5)瞄準投擲時滑動問題的解決
- 五、接劍的實現
- (1)劍的回收
- (2)改進接劍方向
- (3)接劍狀態的轉換
- 總結 完整代碼
- Player.cs
- PlayerAimSwordState.cs
- PlayerCatchSwordState.cs
- PlayerGroundedState.cs
- PlayerAnimationTriggers.cs
- Sword_Skill.cs
- Sword_Skill_Controller.cs
- SkillManager.cs
前言
本文為Udemy課程The Ultimate Guide to Creating an RPG Game in Unity學習筆記,如有錯誤,歡迎指正。
本節實現角色投劍技能。
Udemy課程地址
對應視頻:
Sword Throw Skill State
Setting up details of the sword
Setting up sword’s aim
Improving sword’s behaviour
Improving sword throwing state
一、概述
本節實現投劍技能。
實現投劍的基本功能部分,包括創建動畫、狀態轉換和劍的預制體創建和投出。
實現瞄準的基本功能,包括瞄準狀態轉換、瞄準軌跡的顯示、劍的旋轉和嵌入、瞄準方向等。
實現接劍的基本功能,包括保證劍的唯一性、接劍狀態轉換和接劍的反饋效果等。
新創建瞄準投劍和接劍兩個狀態,具體狀態轉換如下:
主要具體實現如下:
二、投劍相關狀態的創建與實現
(1)創建PlayerAimSwordState和PlayerCatchSwordState
創建腳本瞄準狀態PlayerAimSwordState和接劍狀態PlayerCatchSwordState,它們繼承自PlayerState。
Alt+Enter生成構造函數和重寫。
在Player中聲明兩個狀態。
#region 狀態public PlayerStateMachine StateMachine { get; private set; }public PlayerIdleState idleState { get; private set; }public PlayerMoveState moveState { get; private set; }public PlayerJumpState jumpState { get; private set; }public PlayerAirState airState { get; private set; }public PlayerDashState dashState { get; private set; }public PlayerWallSlideState wallSlideState { get; private set; }public PlayerWallJumpState wallJumpState { get; private set; }public PlayerPrimaryAttackState primaryAttack { get; private set; }public PlayerCounterAttackState counterAttack { get; private set; }public PlayerAimSwordState aimSword { get; private set; }public PlayerCatchSwordState catchSword { get; private set; }#endregion//創建對象protected override void Awake(){base.Awake();StateMachine = new PlayerStateMachine();idleState = new PlayerIdleState(StateMachine, this, "Idle");moveState = new PlayerMoveState(StateMachine, this, "Move");jumpState = new PlayerJumpState(StateMachine, this, "Jump");airState = new PlayerAirState(StateMachine, this, "Jump");dashState = new PlayerDashState(StateMachine, this, "Dash");wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");primaryAttack = new PlayerPrimaryAttackState(StateMachine, this, "Attack");counterAttack = new PlayerCounterAttackState(StateMachine, this, "CounterAttack");aimSword = new PlayerAimSwordState(StateMachine, this, "AimSword");catchSword = new PlayerCatchSwordState(StateMachine, this, "CatchSword");}
(2)創建瞄準與接劍的動畫
瞄準與接劍相關動畫在Sword+Aim+Throw這張精靈表中
層次面板中選中Animator,在Animation面板中創建動畫playerAimSword
playerAimSword精靈表標號14-16,采樣率改為15
具體講解見Unity教程(零)Unity和VS的使用相關內容
同理,playerThrowSword,精靈表標號16、17、21、22,采樣率改為14
同理,playerCatchSword,精靈表標號17、16、15、14,采樣率改為14
取消三個動畫的循環時間
連接狀態機。
連接playerAimSword和PlayerThrowSword
添加過渡條件AimSword,并修改過渡設置
Entry->playerAimSword的過渡,加條件變量
playerAimSword->playerThrowSword的過渡,加條件變量
playerThrowSword->Exit的過渡,修改退出時間和持續時間
連接playerCatchSword
添加過渡條件CatchSword,并修改過渡設置
Entry->playerCatchSword的過渡,加條件變量
playerCatchSword->Exit的過渡,加條件變量
(3)實現瞄準投擲的狀態轉換
要實現的功能為按下鼠標右鍵玩家進入瞄準狀態,松開鼠標右鍵玩家恢復空閑狀態。
在playerGroundedState玩家接地狀態中添加轉換到playerAimSwordState的代碼。
public override void Update(){base.Update();if (Input.GetKeyDown(KeyCode.Mouse1))stateMachine.ChangeState(player.aimSword);if (Input.GetKeyDown(KeyCode.Q))stateMachine.ChangeState(player.counterAttack);if (Input.GetKeyDown(KeyCode.Mouse0))stateMachine.ChangeState(player.primaryAttack);if(!player.isGroundDetected())stateMachine.ChangeState(player.airState);if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())stateMachine.ChangeState(player.jumpState);}
在playerAimSwordState中退出狀態。
public override void Update(){base.Update();if(Input.GetKeyUp(KeyCode.Mouse1))stateMachine.ChangeState(player.idleState);}
效果如下:
三、投劍技能的創建與實現
(1)創建Sword物體
劍的精靈表是SwordSpin這一張。
我們拖一張出來到層次面板中重命名為Sword。
修改劍的層次。
我們還是把它掛在一個空物體下面,方便添加組件。
右擊->Create Empty Parent->重命名為Sword
在父物體上添加剛體和圓形碰撞器
(2)創建Sword閑置和旋轉動畫
創建動畫控制器Sword_AC,把它掛在子物體Sword上。
在Animations文件夾中,新建Sword文件夾存放劍的閑置動畫和旋轉動畫。
在Animation面板中創建動畫SwordIdle
SwordIdle精靈表標號23
在Animation面板中創建動畫SwordFlip
SwordFlip精靈表標號3、5、9、12,采樣率改為20
把swordFlip設置為默認狀態
連接狀態機。
連接SwordFlip和SwordIdle。
添加過渡條件Rotation,并修改過渡設置。
SwordFlip->SwordIdle的過渡,加條件變量
SwordIdle->SwordFlip的過渡,加條件變量
(3)創建投劍技能及控制器腳本
創建投劍技能腳本Sword_Skill,它繼承自Skill技能基類。
在投劍技能腳本中添加經常修改的技能信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Sword_Skill : Skill
{[Header("Skill Info")][SerializeField] private GameObject swordPrefab;[SerializeField] private Vector2 launchForce;[SerializeField] private float swordGravity;
}
在SkillManager中創建技能。
public Dash_Skill dash { get; private set; }public Clone_Skill clone { get; private set; }public Sword_Skill sword { get; private set; }private void Start(){dash = GetComponent<Dash_Skill>();clone = GetComponent<Clone_Skill>();sword = GetComponent<Sword_Skill>();}
創建控制器腳本Sword_Skill_Controller。
在Sword_Skill_Controller中添加變量獲取要用的組件。
//Sword_Skill_Controller:擲劍技能控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Sword_Skill_Controller : MonoBehaviour
{private Animator anim;private Rigidbody2D rb;private CircleCollider2D cd;private Player player;private void Awake(){anim = GetComponentInChildren<Animator>();rb = GetComponent<Rigidbody2D>();cd = GetComponent<CircleCollider2D>();}
}
技能實現依然類似上節的分身技能,預制體的創建與設置分別在技能腳本和控制器腳本中實現。
在控制器腳本中添加參數設置。
public void SetupSword(Vector2 _dir, float _gravityScale, Player _player){player = _player;rb.velocity = _dir;rb.gravityScale = _gravityScale;}
在技能腳本中實例化Sword預制體到角色現在的位置。
這要用到player的位置參數,先在Skill基類中創建player變量并賦值,方便后續書寫。
protected Player player;protected virtual void Start(){player = PlayerManager.instance.player;}
在Sword_Skill中添加函數CreateSword,實例化預制體,并調用控制器中的函數SetupSword設置參數。
public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(launchForce, swordGravity,player); }
將Sword_Skill掛在SkillManager下。
將Sword_Skill_Controller掛到創建的物體Sword下。
將Sword拉成預制體。
為Sword_Skill的參數賦值,從預制體文件夾中拖入劍的預制體,為參數賦一個合適的值
(4)投擲劍的實現
在使用技能時,我們要讓角色扔出劍,這里要用Animator的事件實現。
在玩家觸發器腳本PlayerAnimationTriggers中添加調用CreateSword的函數。
private void ThrowSword(){SkillManager.instance.sword.CreateSword();}
在playerThrowSword動畫中添加事件,觸發事件時創建Sword。
效果如下:
四、瞄準的實現
(1)瞄準的實現
現在已經實現了將劍投擲出去,但作為技能使用,我們希望劍沿著鼠標所指的方向投擲。
實現這一部分需要獲取鼠標的世界坐標。Input.mousePosition可以獲得鼠標的屏幕坐標,我們將它轉換到世界坐標后計算方向。最終的投擲方向由以玩家位置為起點、鼠標位置為終點的向量和投擲力度計算得到。
在Sword_Skill中添加函數AimDirection獲取瞄準的方向。
public Vector2 AimDirection(){Vector2 playerPosition = player.transform.position;Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);Vector2 direction = mousePosition - playerPosition;return direction;}
這里用到了函數Camera.ScreenToWorldPoint,它可以把點從屏幕空間變換到三維空間。
它的介紹見 Unity官方手冊
創建變量finalDir存放最終的方向,并在Update函數中不斷計算更新方向。計算時,將瞄準方向歸一化后,乘該方向的投擲力度獲得最終方向。將CreateSword中的投擲方向改為finalDir。
private Vector2 finalDir;protected override void Update(){if(Input.GetKeyUp(KeyCode.Mouse1))finalDir = new Vector2(AimDirection().normalized.x * launchForce.x , AimDirection().normalized.y * launchForce.y);}public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(finalDir, swordGravity, player); }
(2)顯示瞄準點和路徑
先把瞄準點都生成在玩家位置,但是先不激活不顯示。直到按下鼠標右鍵進入瞄準狀態,再激活瞄準點并計算修改點的位置。在將劍實例化后就可以取消點的顯示,再次設置為未激活狀態了。
劍被拋出去后運動路線是個拋物線,通過運動公式計算間隔一定時刻劍的位移來確定位置。設置點的個數和間隔控制瞄準點的顯示。
在Sword_Skill腳本中添加瞄準點相關變量。
[Header("Aim dots")][SerializeField] private int numberOfDots;[SerializeField] private float spaceBetweenDots;[SerializeField] private GameObject dotPrefab;[SerializeField] private Transform dotsParent;private GameObject[] dots;
先創建生成瞄準點的函數,實例化預制體存儲在數組中,將瞄準點改為未激活狀態。
創建激活瞄準點的函數方便使用。
生成與激活瞄準點的函數如下:
protected override void Start(){base.Start();GenerateDots();}private void GenerateDots(){dots = new GameObject[numberOfDots];for (int i = 0; i < numberOfDots; i++){dots[i] = Instantiate(dotPrefab, player.transform.position, Quaternion.identity, dotsParent);dots[i].SetActive(false);}}public void DotsActive(bool _isActive){for (int i = 0; i < dots.Length; i++){dots[i].SetActive(_isActive);}}
顯示瞄準點要根據時間變化計算軌跡上點的位置,將t作為參數傳入,采用簡單的運動公式計算軌跡上的點
{ x = v x t , y = v y t + 1 2 g t 2 \begin{cases} x = v_x t, \\ y = v_y t + \frac{1}{2} g t^2 \end{cases} {x=vx?t,y=vy?t+21?gt2?
調用計算函數,每隔固定時刻取一個點,顯示整條軌跡。函數將在Update函數中調用,在鼠標右鍵被按住時,隨鼠標運動改變瞄準點顯示。
注意:這里是按著鼠標時,要用GetKey()
代碼如下:
protected override void Update(){if(Input.GetKeyUp(KeyCode.Mouse1))finalDir = new Vector2(AimDirection().normalized.x * launchForce.x , AimDirection().normalized.y * launchForce.y);if(Input.GetKey(KeyCode.Mouse1)){for(int i = 0; i < dots.Length; i++){dots[i].transform.position = DotsPosition(i * spaceBetweenDots);}}}private Vector2 DotsPosition(float t){Vector2 position = (Vector2)player.transform.position +new Vector2(AimDirection().normalized.x * launchForce.x, AimDirection().normalized.y * launchForce.y) * t +0.5f * (Physics2D.gravity * swordGravity) * (t * t);return position;}
在PlayerAimSwordState的Enter函數中添加瞄準點的激活
public override void Enter(){base.Enter();player.skill.sword.DotsActive(true);}
在SwordSkill的CreateSword函數中設置完參數劍要丟出了,這時就可以關閉瞄準點把它改為未激活了。
public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(finalDir, swordGravity, player); DotsActive(false);}
創建瞄準點預制體。
創建一個圓形精靈,改變它的層次為地面層 -2,使它在地里面不可見。
右鍵 -> 2D Object -> Sprite ->Circle ->重命名為AimDot
修改它的大小、顏色到合適的值
拉成預制體
在Player上創建空物體AimDotParent作為瞄準點的父物體。
在技能管理器上給Sword_Skill參數賦值
效果如下:
(3)劍的旋轉與嵌入
可以看到,現在在投擲出去后,劍是完全不動的,而且會砸在骷髏身上彈回來,我們需要把它改的更符合規律一點。
讓劍尖順著投擲的軌跡旋轉直到擊中骷髏。我們可以通過修改坐標軸的方向實現,這里選取了x軸,讓坐標軸與運動方向一致,劍也會隨之旋轉。
首先要把劍調整為與x軸平齊,劍尖朝向x軸正方向,劍才能沿著軌跡運動。調整時旋轉子物體,把它朝向右,到以下程度:
注意:記得應用預制體的更改
在Sword_Skill_Controller中添加update函數,根據速度實時改變坐標軸方向
private void Update(){transform.right = rb.velocity;}
接下來實現讓劍嵌入敵人和地面,在預制體面板中進行更改。
在碰撞盒上勾選觸發器。
我們要讓劍在插入物體后靜止,要將剛體類型改為Kinematic,Kinematic 2D 剛體不受重力和作用力影響且開銷較小。
更詳細的介紹請見 Unity官方手冊 2D剛體
同時鎖定XYZ三個軸的旋轉和移動避免劍產生多余的運動,關閉碰撞器避免產生連續碰撞。
在Sword_Skill_Controller中添加參數canRotate來表示是否處于旋轉,默認值設為true。只有處于旋轉時需要改變劍的坐標軸。
創建觸發器函數,在有物體進入劍的觸發器時,將canRotate改為false,并實行以上操作讓劍靜止插入物體,同時將劍作為子物體掛在插入的物體上,讓劍后續跟隨物體一起移動。
private bool canRotate = true;private void Update(){if (canRotate){transform.right = rb.velocity;}}private void OnTriggerEnter2D(Collider2D collision){canRotate = false;cd.enabled = false;rb.isKinematic = true;rb.constraints = RigidbodyConstraints2D.FreezeAll;transform.parent = collision.transform;}
現在投擲劍,劍會與玩家碰撞,粘在玩家身上,我們要更改劍的圖層來解決這一問題。
添加Sword圖層,將劍及其子物體都改為Sword層
修改碰撞矩陣
Edit -> Project Setting -> Physics 2D -> Layer Collision Matrix -> 僅保留Sword與Ground和Enemy的碰撞
效果如下
(4)解決投劍方向的問題
角色不會隨投擲方向的變化隨之改變面朝的方向,要修改代碼使角色隨鼠標位置改變面向。
改變朝向的條件如下:
鼠標在角色左側,角色面向右
鼠標在角色右側,角色面向左
在PlayerAimSwordState中添加:
public override void Update(){base.Update();if(Input.GetKeyUp(KeyCode.Mouse1))stateMachine.ChangeState(player.idleState);Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);if (player.transform.position.x > mousePosition.x && player.facingDir == 1)player.Flip();else if(player.transform.position.x < mousePosition.x && player.facingDir == -1)player.Flip();}
效果如下:
(5)瞄準投擲時滑動問題的解決
我們要參照之前空閑狀態寫的,在瞄準和接劍狀態退出時要調用協程使它處于"忙的狀態"
玩家從移動狀態轉換到瞄準狀態時也依然會滑動,因此在瞄準狀態中將速度設置為0。
在瞄準和接劍狀態里分別添加
//PlayerAimSwordState: 玩家瞄準狀態public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.2f);}public override void Update(){base.Update();player.ZeroVelocity();if (Input.GetKeyUp(KeyCode.Mouse1))stateMachine.ChangeState(player.idleState);Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);if (player.transform.position.x > mousePosition.x && player.facingDir == 1)player.Flip();else if(player.transform.position.x < mousePosition.x && player.facingDir == -1)player.Flip();}
//PlayerCatchSwordState: 玩家接劍狀態public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.1f);}
五、接劍的實現
(1)劍的回收
現在可以同時存在多把劍,于是可能出現下面把骷髏扎成刺猬的場景,我們需要改進這一點,讓場景中只能同時存在一把劍,劍擊中敵人后可以回收。
添加變量sword,每次創建后都分配新的劍給此變量
在player中添加變量sword,并添加分配和接住sword的函數。接劍時將轉到PlayerCatchSwordState。
public SkillManager skill{get; private set;}public GameObject sword { get; private set; }public void AssignNewSword(GameObject _newSword){sword = _newSword;}public void ClearTheSword(){Destroy(sword);}
在創建劍后,將劍分配給sword,在Sword_Skill中調用函數
public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(finalDir, swordGravity, player);player.AssignNewSword(newSword);DotsActive(false);}
接下來實現劍的返回。
在Sword_Skill中添加返回速度returnSpeed,傳遞到控制器中。
[Header("Skill Info")][SerializeField] private GameObject swordPrefab;[SerializeField] private Vector2 launchForce;[SerializeField] private float swordGravity;[SerializeField] private float returnSpeed;public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);player.AssignNewSword(newSword);DotsActive(false);}
在Sword_Skill_Controller中添加返回相關變量,isReturning和returnSpeed,分別表示是否返回中和返回速度。
創建ReturnSword函數,讓劍不再旋轉并且不再跟隨插入的物體移動,同時把isReturning改為true,表示正在收回劍。這里不使用改為Kinematic剛體的方式,因為這會使劍無法中途收回。
當劍處于收回狀態時,讓它以設定好的速度由當前位置向著玩家位置移動,當距離小于某一值時,銷毀劍,這一部分寫在Update中隨時間更新位置。
private bool isReturning;private float returnSpeed = 12;public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed){player = _player;rb.velocity = _dir;rb.gravityScale = _gravityScale;returnSpeed = _returnSpeed;}public void ReturnSword(){rb.constraints = RigidbodyConstraints2D.FreezeAll;transform .parent = null;isReturning = true;}private void Update(){if (canRotate){transform.right = rb.velocity;}if (isReturning){transform.position = Vector2.MoveTowards(transform.position, player.transform.position, returnSpeed * Time.deltaTime);if(Vector2.Distance(transform.position, player.transform.position) < 1)player.ClearTheSword();}}
修改進入瞄準狀態的條件,將它改為瞄準投劍和收回劍共用的操作。
按下鼠標右鍵且當player沒有被分配劍時轉換到瞄準狀態;如果有分配劍,則調用Sword_Skill_Controller中讓劍返回的函數ReturnSword()。寫個函數HasNoSword判斷并實現
//更新public override void Update(){base.Update();if (Input.GetKeyDown(KeyCode.Mouse1) && HasNoSword())stateMachine.ChangeState(player.aimSword);if (Input.GetKeyDown(KeyCode.Q))stateMachine.ChangeState(player.counterAttack);if (Input.GetKeyDown(KeyCode.Mouse0))stateMachine.ChangeState(player.primaryAttack);if(!player.isGroundDetected())stateMachine.ChangeState(player.airState);if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())stateMachine.ChangeState(player.jumpState);}private bool HasNoSword(){if (!player.sword){return true;}player.sword.GetComponent<Sword_Skill_Controller>().ReturnSword();return false;}
給回收速度賦一個合適的值
為了提升技能效果,在扔出劍時,播放劍的旋轉動畫;在劍接觸到物體碰撞時停止播放動畫。
在設置劍和觸發器函數中分別添加改變動畫變量Rotation的語句。
public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed){player = _player;rb.velocity = _dir;rb.gravityScale = _gravityScale;returnSpeed = _returnSpeed;anim.SetBool("Rotation", true);}private void OnTriggerEnter2D(Collider2D collision){anim.SetBool("Rotation", false);canRotate = false;cd.enabled = false;rb.isKinematic = true;rb.constraints = RigidbodyConstraints2D.FreezeAll;transform.parent = collision.transform;}}
(2)改進接劍方向
接劍的方向和投擲劍的方向問題一樣,在PlayerCatchSwordState中獲取劍的位置,比較角色位置與劍的位置確定是否應該翻轉。
public override void Enter(){base.Enter();sword = player.sword.transform;if (player.transform.position.x > sword.position.x && player.facingDir == 1)player.Flip();else if (player.transform.position.x < sword.position.x && player.facingDir == -1)player.Flip();}
(3)接劍狀態的轉換
前面在接劍時,我們直接調用ClearSword函數銷毀劍。除此之外,還會播放接劍動畫,讓我們添加這部分。
首先,Ctrl+R快捷鍵,將Player中的函數ClearSword改名為CatchSword。在函數中增加到PlayerCatchSwordState的狀態轉換。
public void CatchTheSword(){StateMachine.ChangeState(catchSword);Destroy(sword);}
退出接劍狀態,要在playerCatchSword中觸發事件實現。
在動畫的最后一幀添加動畫事件,當觸發器被觸發時,轉換為空閑狀態。
在playerCatchSwordState中添加
public override void Update(){base.Update();if (triggerCalled)stateMachine.ChangeState(player.idleState);}
接劍的時候讓角色后退一點,顯示劍有一個小的沖擊力。
在Player中添加變量swordReturnImpact。
[Header("Move Info")]public float moveSpeed = 8f;public float jumpForce = 12f;public float swordReturnImpact = 7f;
在接劍狀態中添加接劍后改變速度。
public override void Enter(){base.Enter();sword = player.sword.transform;if (player.transform.position.x > sword.position.x && player.facingDir == 1)player.Flip();else if (player.transform.position.x < sword.position.x && player.facingDir == -1)player.Flip();rb.velocity = new Vector2 (player.swordReturnImpact * -player.facingDir,rb.velocity.y);}
賦上合適的值
總結 完整代碼
Player.cs
創建PlayerAimSwordState和PlayerCatchSwordState兩個狀態并賦值。
添加接劍時后退的相關參數swordReturnImpact。
添加參數sword獲取投擲的劍,并判斷現在是否有劍被創建分配。
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : Entity
{[Header("Attack details")]public Vector2[] attackMovement;public float counterAttackDuration = 0.2f;public bool isBusy { get; private set; }[Header("Move Info")]public float moveSpeed = 8f;public float jumpForce = 12f;public float swordReturnImpact = 7f;[Header("Dash Info")]public float dashSpeed=25f;public float dashDuration=0.2f;public float dashDir { get; private set; }public SkillManager skill{get; private set;}public GameObject sword { get; private set; }#region 狀態public PlayerStateMachine StateMachine { get; private set; }public PlayerIdleState idleState { get; private set; }public PlayerMoveState moveState { get; private set; }public PlayerJumpState jumpState { get; private set; }public PlayerAirState airState { get; private set; }public PlayerDashState dashState { get; private set; }public PlayerWallSlideState wallSlideState { get; private set; }public PlayerWallJumpState wallJumpState { get; private set; }public PlayerPrimaryAttackState primaryAttack { get; private set; }public PlayerCounterAttackState counterAttack { get; private set; }public PlayerAimSwordState aimSword { get; private set; }public PlayerCatchSwordState catchSword { get; private set; }#endregion//創建對象protected override void Awake(){base.Awake();StateMachine = new PlayerStateMachine();idleState = new PlayerIdleState(StateMachine, this, "Idle");moveState = new PlayerMoveState(StateMachine, this, "Move");jumpState = new PlayerJumpState(StateMachine, this, "Jump");airState = new PlayerAirState(StateMachine, this, "Jump");dashState = new PlayerDashState(StateMachine, this, "Dash");wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");primaryAttack = new PlayerPrimaryAttackState(StateMachine, this, "Attack");counterAttack = new PlayerCounterAttackState(StateMachine, this, "CounterAttack");aimSword = new PlayerAimSwordState(StateMachine, this, "AimSword");catchSword = new PlayerCatchSwordState(StateMachine, this, "CatchSword");}// 設置初始狀態protected override void Start(){base.Start();skill = SkillManager.instance;StateMachine.Initialize(idleState);}// 更新protected override void Update(){base.Update();StateMachine.currentState.Update();CheckForDashInput();}public void AssignNewSword(GameObject _newSword){sword = _newSword;}public void CatchTheSword(){StateMachine.ChangeState(catchSword);Destroy(sword);}public IEnumerator BusyFor(float _seconds){isBusy = true;yield return new WaitForSeconds(_seconds);isBusy = false;}//設置觸發器public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();//檢查沖刺輸入public void CheckForDashInput(){if (Input.GetKeyDown(KeyCode.LeftShift) && SkillManager.instance.dash.CanUseSkill()){dashDir = Input.GetAxisRaw("Horizontal");if (dashDir == 0)dashDir = facingDir;StateMachine.ChangeState(dashState);}}}
PlayerAimSwordState.cs
激活瞄準點。
實現瞄準狀態到空閑狀態的轉換。
實現瞄準時根據鼠標位置左右翻轉。
設置處于忙碌中,防止滑動。
//PlayerAimSwordState: 玩家瞄準狀態
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerAimSwordState : PlayerState
{public PlayerAimSwordState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}public override void Enter(){base.Enter();player.skill.sword.DotsActive(true);}public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.2f);}public override void Update(){base.Update();player.ZeroVelocity();if (Input.GetKeyUp(KeyCode.Mouse1))stateMachine.ChangeState(player.idleState);Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);if (player.transform.position.x > mousePosition.x && player.facingDir == 1)player.Flip();else if(player.transform.position.x < mousePosition.x && player.facingDir == -1)player.Flip();}
}
PlayerCatchSwordState.cs
實現接劍狀態到空閑狀態的轉換。
實現接劍時根據劍的位置左右翻轉和后退效果。
設置處于忙碌中,防止滑動。
//PlayerCatchSwordState: 玩家接劍狀態
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerCatchSwordState : PlayerState
{private Transform sword;public PlayerCatchSwordState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}public override void Enter(){base.Enter();sword = player.sword.transform;if (player.transform.position.x > sword.position.x && player.facingDir == 1)player.Flip();else if (player.transform.position.x < sword.position.x && player.facingDir == -1)player.Flip();rb.velocity = new Vector2 (player.swordReturnImpact * -player.facingDir,rb.velocity.y);}public override void Exit(){base.Exit();player.StartCoroutine("BusyFor", 0.1f);}public override void Update(){base.Update();if (triggerCalled)stateMachine.ChangeState(player.idleState);}
}
PlayerGroundedState.cs
添加player是否被分配劍的判斷,根據判斷轉到瞄準狀態或回收劍。
//超級狀態PlayerGroundedState:接地狀態
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerGroundedState : PlayerState
{//構造函數public PlayerGroundedState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}//進入public override void Enter(){base.Enter();}//退出public override void Exit(){base.Exit();}//更新public override void Update(){base.Update();if (Input.GetKeyDown(KeyCode.Mouse1) && HasNoSword())stateMachine.ChangeState(player.aimSword);if (Input.GetKeyDown(KeyCode.Q))stateMachine.ChangeState(player.counterAttack);if (Input.GetKeyDown(KeyCode.Mouse0))stateMachine.ChangeState(player.primaryAttack);if(!player.isGroundDetected())stateMachine.ChangeState(player.airState);if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())stateMachine.ChangeState(player.jumpState);}private bool HasNoSword(){if (!player.sword){return true;}player.sword.GetComponent<Sword_Skill_Controller>().ReturnSword();return false;}
}
PlayerAnimationTriggers.cs
實現在動畫播完觸發事件時扔出劍。
//PlayerAnimationTriggers:觸發器組件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerAnimationTriggers : MonoBehaviour
{private Player player => GetComponentInParent<Player>();private void AnimationTrigger(){player.AnimationTrigger();}private void AttackTrigger(){Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);foreach(var hit in colliders){if (hit.GetComponent<Enemy>() != null)hit.GetComponent<Enemy>().Damage();}}private void ThrowSword(){SkillManager.instance.sword.CreateSword();}
}
Sword_Skill.cs
創建Skill相關信息,和瞄準點所需信息。
創造劍的函數CreateSword,實例化劍并調用函數設置劍的相關變量,將劍分配給player,關閉瞄準點顯示。
實現瞄準點相關函數,包括計算瞄準方向、生成點瞄準點、激活/關閉瞄準點、計算運動軌跡。
//Sword_Skill:擲劍技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Sword_Skill : Skill
{[Header("Skill Info")][SerializeField] private GameObject swordPrefab;[SerializeField] private Vector2 launchForce;[SerializeField] private float swordGravity;[SerializeField] private float returnSpeed;[Header("Aim dots")][SerializeField] private int numberOfDots;[SerializeField] private float spaceBetweenDots;[SerializeField] private GameObject dotPrefab;[SerializeField] private Transform dotsParent;private GameObject[] dots;private Vector2 finalDir;protected override void Start(){base.Start();GenerateDots();}protected override void Update(){if(Input.GetKeyUp(KeyCode.Mouse1))finalDir = new Vector2(AimDirection().normalized.x * launchForce.x , AimDirection().normalized.y * launchForce.y);if(Input.GetKey(KeyCode.Mouse1)){for(int i = 0; i < dots.Length; i++){dots[i].transform.position = DotsPosition(i * spaceBetweenDots);}}}public void CreateSword(){GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);player.AssignNewSword(newSword);DotsActive(false);}public Vector2 AimDirection(){Vector2 playerPosition = player.transform.position;Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);Vector2 direction = mousePosition - playerPosition;return direction;}private void GenerateDots(){dots = new GameObject[numberOfDots];for (int i = 0; i < numberOfDots; i++){dots[i] = Instantiate(dotPrefab, player.transform.position, Quaternion.identity, dotsParent);dots[i].SetActive(false);}}public void DotsActive(bool _isActive){for (int i = 0; i < dots.Length; i++){dots[i].SetActive(_isActive);}}private Vector2 DotsPosition(float t){Vector2 position = (Vector2)player.transform.position +new Vector2(AimDirection().normalized.x * launchForce.x, AimDirection().normalized.y * launchForce.y) * t +0.5f * (Physics2D.gravity * swordGravity) * (t * t);return position;}
}
Sword_Skill_Controller.cs
獲取相關組件信息,創建相關變量并賦值。
根據Sword_Skill傳值,設置劍的相關參數,設置劍的動畫。
實現包圍盒觸發器函數,設置劍的相關參數,設置劍的動畫。
實現到接劍狀態的轉換,讓劍飛回玩家位置。實現劍返回的參數設置。
//Sword_Skill_Controller:擲劍技能控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Sword_Skill_Controller : MonoBehaviour
{private Animator anim;private Rigidbody2D rb;private CircleCollider2D cd;private Player player;private bool canRotate = true;private bool isReturning;private float returnSpeed = 12;private void Awake(){anim = GetComponentInChildren<Animator>();rb = GetComponent<Rigidbody2D>();cd = GetComponent<CircleCollider2D>();}public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed){player = _player;rb.velocity = _dir;rb.gravityScale = _gravityScale;returnSpeed = _returnSpeed;anim.SetBool("Rotation", true);}public void ReturnSword(){rb.constraints = RigidbodyConstraints2D.FreezeAll;transform .parent = null;isReturning = true;}private void Update(){if (canRotate){transform.right = rb.velocity;}if (isReturning){transform.position = Vector2.MoveTowards(transform.position, player.transform.position, returnSpeed * Time.deltaTime);if(Vector2.Distance(transform.position, player.transform.position) < 1)player.CatchTheSword();}}private void OnTriggerEnter2D(Collider2D collision){if (isReturning)return;StuckInto(collision);}private void StuckInto(Collider2D collision){anim.SetBool("Rotation", false);canRotate = false;cd.enabled = false;rb.isKinematic = true;rb.constraints = RigidbodyConstraints2D.FreezeAll;transform.parent = collision.transform;}}
SkillManager.cs
創建投劍技能。
//SkillManager:玩家管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SkillManager : MonoBehaviour
{public static SkillManager instance;public Dash_Skill dash { get; private set; }public Clone_Skill clone { get; private set; }public Sword_Skill sword { get; private set; }private void Awake(){if (instance != null && instance != this){Destroy(this.gameObject);}else{instance = this;}}private void Start(){dash = GetComponent<Dash_Skill>();clone = GetComponent<Clone_Skill>();sword = GetComponent<Sword_Skill>();}
}