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開發2D類銀河惡魔城游戲學習筆記目錄
文章目錄
- Unity開發2D類銀河惡魔城游戲學習筆記
- 前言
- 一、概述
- 二、預制件(Prefab)
- (1)預制件介紹
- (2)創建分身預制件
- 三、分身技能的實現
- (1)分身技能的創建
- (2)分身技能的實現
- (3)分身的消失與銷毀
- (4)補充:將預制件Clone實例化到當前位置
- 四、分身攻擊的實現
- (1)攻擊動畫
- (2)分身攻擊的實現
- (2)分身攻擊的實現
- 總結 完整代碼
- PlayerDashState.cs
- Clone_Skill.cs
- Clone_Skill_Controller.cs
- SkillManager.cs
- Player.cs
前言
本文為Udemy課程The Ultimate Guide to Creating an RPG Game in Unity學習筆記,如有錯誤,歡迎指正。
本節實現角色分身技能。
Udemy課程地址
對應視頻:
Clone Creating Ability
Clone’s Attack
一、概述
本節實現分身技能。
實現解鎖技能后,在角色沖刺時產生一個分身,分身會攻擊距離最近的一個敵人。
二、預制件(Prefab)
(1)預制件介紹
Unity的預制件相當于創建一個模板,使得游戲對象作為可重用資源。可以用這個模板在場景中創建新的預制件實例。
詳細內容可見Unity官方手冊預制件
首先是預制件的創建。
預制件的創建很簡單,將一個游戲對象從 Hierarchy 窗口拖入 Project 窗口就可以了。
預制件的實例化。
最簡單的是將預制件資源從 Project 視圖拖動到 Hierarchy 或 Scene 視圖,創建實例。
也可通過腳本進行實例化。使用Object的Instantiate函數。
Object Instantiate (Object original);Object Instantiate (Object original, Transform parent);Object Instantiate (Object original, Transform parent, bool instantiateInWorldSpace);Object Instantiate (Object original, Vector3 position, Quaternion rotation);Object Instantiate (Object original, Vector3 position, Quaternion rotation, Transform parent);
參數含義如下表:
參數 | 含義 |
---|---|
original | 要復制的現有對象(如預制件) |
position | 新對象的位置 |
rotation | 新對象的方向 |
parent | 將指定給新對象的父對象 |
instantiateInWorldSpace | 分配父對象時,傳遞 true 可直接在世界空間中定位新對象。 傳遞 false 可相對于其新父項來設置對象的位置。 |
預制件的編輯。 可以在預制件模式下編輯預制件。這種情況可以單獨或在上下文中編輯預制件資源。
也可以在實例中編輯后覆蓋預制體。使用Overrides應用和還原覆蓋。
(2)創建分身預制件
將玩家精靈表第一幀拖入層次面板中并重命名為Clone。
創建動畫控制器Clone_AC
將Clone_AC掛載到Clone下面
這時我們發現Clone的圖像被遮擋了。
調整SpriteRenderer中的層次順序,使Clone位于敵人之前,玩家之后。
給Clone添加動畫,這里我們不需要重新建立動畫,只需要把原來Player的動畫復用。
打開Clone的Aniamtor面板,將PlayerIdle拖入作為默認狀態,再拖入PlayerAttack1、PlayerAttack2、PlayerAttack3。
創建Prefabs文件夾存放預制件。
將層次面板中的Clone拖入文件夾,預制件創建完成。
把層次面板中的Clone刪除,后續我們用到它時會在腳本中創建。
三、分身技能的實現
分身技能Clone_Skill繼承自Skill基類。我們要實現按下沖刺鍵后,在玩家沖刺的位置創建一個分身,因此我們需要在PlayerDashState開始時傳入Player的位置創建分身。
實現時還要創建一個分身技能的控制器Clone_Skill_Controller,將它掛載到Clone預制件下,控制分身的位置。這里讓人容易疑惑,為什么要大費周章再創建一個控制器腳本。個人理解,更多是因為馬上要實現的分身攻擊,攻擊時需要觸發動畫事件,因此必須有個腳本掛在Clone預制體下面。這里就把控制分身位置的功能也寫進去了。
此外,還可在實例化時就指定分身位置,這種形式也會在下面寫一下。
(1)分身技能的創建
在Scripts文件夾中創建Skills文件夾存放技能相關腳本。
創建分身技能腳本Clone_Skill,它繼承自Skill基類,將它掛到技能管理器下。
在技能管理器腳本中創建分身技能并賦值。
public Clone_Skill clone { get; private set; }private void Start(){dash = GetComponent<Dash_Skill>();clone = GetComponent<Clone_Skill>();}
(2)分身技能的實現
創建分身技能控制器Clone_Skill_Controller,創建函數SetupClone設置分身信息。
雙擊預制件Clone,在面板中點擊Add Component添加組件。
注意:腳本是添加在預制件上的。
在Clone_Skill_Controller中添加設置分身信息的函數。
//Clone_Skill_Controller:分身技能控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Clone_Skill_Controller : MonoBehaviour
{//設置分身信息public void SetupClone(Transform _newTransform){transform.position = _newTransform.position;}
}
在Clone_Skill腳本中添加實例化預制體的函數,并調用SetupClone設置分身信息。
//Clone_Skill:分身技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Clone_Skill : Skill
{[SerializeField] private GameObject clonePerfab;public void CreateClone(Transform _clonePosition){GameObject newClone = Instantiate(clonePerfab);newClone.GetComponent<Clone_Skill_Controller>().SetupClone(_clonePosition);}
}
為預制體賦值
在Player中創建SkillManager方便管理。
public SkillManager skill;// 設置初始狀態protected override void Start(){base.Start();skill = SkillManager.instance;StateMachine.Initialize(idleState);}
接著在PlayerDashState的中調用CreateClone,使得沖刺后在玩家位置創建一個分身。
//進入狀態public override void Enter(){base.Enter();player.skill.clone.CreateClone(player.transform);//設置沖刺持續時間stateTimer = player.dashDuration;}
效果如下:
基本功能已經實現,但有個很明顯的問題,創建的分身沒有回收。
(3)分身的消失與銷毀
此技能需要讓分身持續一段時間后逐漸消失,可以使clone預制件的圖像隨時間透明度逐漸降低實現,最后透明度降到0時銷毀分身。這需要用到計時器,而且要設置分身持續的時長和消失的速度。
我們把經常需要變動修改的變量放在CloneSkill中方便管理,這里將分身持續時長cloneDuration放在里面。
public class Clone_Skill : Skill
{[Header("Clone Info")][SerializeField] private GameObject clonePerfab;[SerializeField] private float cloneDuration;public void CreateClone(Transform _clonePosition){GameObject newClone = Instantiate(clonePerfab);newClone.GetComponent<Clone_Skill_Controller>().SetupClone(_clonePosition, cloneDuration);}
}
在Clone_Skill_Controller中設置計時器,實現圖像透明度遞減,并在透明度為0時銷毀對象。
public class Clone_Skill_Controller : MonoBehaviour
{private SpriteRenderer sr;[SerializeField] private float colorLosingSpeed;private float cloneTimer;private void Awake(){sr = GetComponent<SpriteRenderer>();}private void Update(){cloneTimer -= Time.deltaTime;if(cloneTimer < 0){sr.color = new Color(1, 1, 1, sr.color.a - Time.deltaTime * colorLosingSpeed);if (sr.color.a <= 0)Destroy(gameObject);}}//設置分身信息public void SetupClone(Transform _newTransform, float _cloneDuration){transform.position = _newTransform.position;cloneTimer = _cloneDuration;}
}
設置合適的技能持續時間和消失速度
效果如下:
(4)補充:將預制件Clone實例化到當前位置
只需在實例化時直接傳入位置參數即可,其他函數要隨著做一些更改。
在Clone_Skill中
public void CreateClone(Transform _clonePosition){GameObject newClone = Instantiate(clonePerfab,_clonePosition);newClone.GetComponent<Clone_Skill_Controller>().SetupClone(cloneDuration);}
在Clone_Skill_Controller中
//設置分身信息public void SetupClone(float _cloneDuration){cloneTimer = _cloneDuration;}
四、分身攻擊的實現
(1)攻擊動畫
進入預制件Clone的編輯,在Animator中創建空狀態用于空閑和攻擊狀態過渡。創建Int型變量AttackNumber用于確定攻擊段數。
連接playerIdle與Empty狀態,當AttackNumber>0時,進入Empty。
PlayerIdle->Empty, 加條件變量并更改設置
分別連接Empty和三個攻擊狀態,在AttackNumber等于1時進入playerAttack1,同理等于2、3時分別進入其他兩個狀態
(2)分身攻擊的實現
在技能樹中分身攻擊技能解鎖后才可使用,這里我們先實現功能部分,所以先在Clone_Skill中添加一個變量canAttack用于測試。
public class Clone_Skill : Skill
{[Header("Clone Info")][SerializeField] private GameObject clonePerfab;[SerializeField] private float cloneDuration;[Space][SerializeField] private bool canAttack;public void CreateClone(Transform _clonePosition){GameObject newClone = Instantiate(clonePerfab);newClone.GetComponent<Clone_Skill_Controller>().SetupClone(_clonePosition, cloneDuration,canAttack);}
}
當解鎖分身攻擊技能時,在Clone_Skill_Controller中實現分身攻擊的設置。
設置Animator中的條件變量AttackNumber為1-3中隨機一個數,播放相應攻擊動畫。
注意:在Unity中Random.Range(a, b) 生成的是 [ a, b ) 區間內的整數。
private Animator anim;private void Awake(){sr = GetComponent<SpriteRenderer>();anim = GetComponent<Animator>();}//設置分身信息public void SetupClone(Transform _newTransform, float _cloneDuration, bool _canAttack){if (_canAttack)anim.SetInteger("AttackNumber", Random.Range(1, 4));transform.position = _newTransform.position;cloneTimer = _cloneDuration;}
效果如下:
現在分身攻擊是連續不斷地沒有結束攻擊的部分,我們可以復用攻擊動畫以前的事件實現。
參照PlayerAnimationTriggers里函數的寫法,在Clone_Skill_Controller中添加AnimationTrigger和AttackTrigger兩個函數,分別用于結束攻擊和觸發攻擊效果。
AnimatonTrigger會將計時器設置為小于0的數,相當于分身技能持續時間直接結束,開始逐漸消失。
AttackTrigger與玩家攻擊的實現邏輯一樣,檢查攻擊范圍內的敵人,造成傷害效果。
添加如下代碼:
[SerializeField] private Transform attackCheck;[SerializeField] private float attackCheckRadius = 0.8f;private void AnimationTrigger(){cloneTimer = -0.1f;}private void AttackTrigger(){Collider2D[] colliders = Physics2D.OverlapCircleAll(attackCheck.position, attackCheckRadius);foreach (var hit in colliders){if (hit.GetComponent<Enemy>() != null)hit.GetComponent<Enemy>().Damage();}}
在Clone下創建一個空物體并重命名為attackCheck,將它移動到分身前方合適的位置。然后用它為變量attackCheck賦值。
進行修改時可以在預制體的編輯里,也可以將預制體拖到場景中修改完再覆蓋原來的預制體。
做完這些你會發現分身可以攻擊敵人了,但攻擊還是接連不斷。
因為這次在Animator中沒有將PlayerAttack連到Exit狀態,沒有退出,所以動畫會循環播放。
將三個攻擊動畫的lLoop Time勾掉就可以了。
現在每次沖刺產生分身就是正常攻擊一次了。
(2)分身攻擊的實現
最后一個需要解決的問題是分身面向現在是固定朝右的,我們需要再添加一個讓分身朝向最近敵人攻擊的功能。
在分身一定范圍內檢測敵人,比較后選取離分身最近的一個。如果它在分身左側,則翻轉分身讓它面向左側。
private Transform closestEnemy;//設置分身信息public void SetupClone(Transform _newTransform, float _cloneDuration, bool _canAttack){if (_canAttack)anim.SetInteger("AttackNumber", Random.Range(1, 4));transform.position = _newTransform.position;cloneTimer = _cloneDuration;FaceClosestTarget();}private void FaceClosestTarget(){Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);float closestDistace = Mathf.Infinity;foreach(var hit in colliders){if (hit.GetComponent<Enemy>() != null){float distanceToEnemy = Vector2.Distance(transform.position, hit.transform.position);if (distanceToEnemy < closestDistace){closestDistace = distanceToEnemy;closestEnemy = hit.transform;}}}if(closestEnemy != null){if (closestEnemy.position.x < transform.position.x)transform.Rotate(0, 180, 0);}}
效果如下:
總結 完整代碼
PlayerDashState.cs
添加分身的創建。
//PlayerDashState:沖刺狀態
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerDashState : PlayerState
{//構造函數public PlayerDashState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName){}//進入狀態public override void Enter(){base.Enter();player.skill.clone.CreateClone(player.transform);//設置沖刺持續時間stateTimer = player.dashDuration;}//退出狀態public override void Exit(){base.Exit();}//更新public override void Update(){base.Update();//切換滑墻狀態if(!player.isGroundDetected() && player.isWallDetected()) stateMachine.ChangeState(player.wallSlideState);//設置沖刺速度player.SetVelocity(player.dashDir * player.dashSpeed, 0);//切換到空閑狀態if (stateTimer < 0)stateMachine.ChangeState(player.idleState);}
}
Clone_Skill.cs
創建分身,添加經常需要在面板上更改的相關變量。
//Clone_Skill:分身技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Clone_Skill : Skill
{[Header("Clone Info")][SerializeField] private GameObject clonePerfab;[SerializeField] private float cloneDuration;[Space][SerializeField] private bool canAttack;public void CreateClone(Transform _clonePosition){GameObject newClone = Instantiate(clonePerfab);newClone.GetComponent<Clone_Skill_Controller>().SetupClone(_clonePosition, cloneDuration,canAttack);}
}
Clone_Skill_Controller.cs
設置分身信息,實現分身逐漸消失,實現分身攻擊,將分身改為面向最近的敵人。
//Clone_Skill_Controller:分身技能控制器
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;public class Clone_Skill_Controller : MonoBehaviour
{private SpriteRenderer sr;private Animator anim;[SerializeField] private float colorLosingSpeed;private float cloneTimer;[SerializeField] private Transform attackCheck;[SerializeField] private float attackCheckRadius = 0.8f;private Transform closestEnemy;private void Awake(){sr = GetComponent<SpriteRenderer>();anim = GetComponent<Animator>();}private void Update(){cloneTimer -= Time.deltaTime;if(cloneTimer < 0){sr.color = new Color(1, 1, 1, sr.color.a - Time.deltaTime * colorLosingSpeed);if (sr.color.a <= 0)Destroy(gameObject);}}//設置分身信息public void SetupClone(Transform _newTransform, float _cloneDuration, bool _canAttack){if (_canAttack)anim.SetInteger("AttackNumber", Random.Range(1, 4));transform.position = _newTransform.position;cloneTimer = _cloneDuration;FaceClosestTarget();}private void AnimationTrigger(){cloneTimer = -0.1f;}private void AttackTrigger(){Collider2D[] colliders = Physics2D.OverlapCircleAll(attackCheck.position, attackCheckRadius);foreach (var hit in colliders){if (hit.GetComponent<Enemy>() != null)hit.GetComponent<Enemy>().Damage();}}private void FaceClosestTarget(){Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 25);float closestDistance = Mathf.Infinity;foreach(var hit in colliders){if (hit.GetComponent<Enemy>() != null){float distanceToEnemy = Vector2.Distance(transform.position, hit.transform.position);if (distanceToEnemy < closestDistance){closestDistance = distanceToEnemy;closestEnemy = hit.transform;}}}if(closestEnemy != null){if (closestEnemy.position.x < transform.position.x)transform.Rotate(0, 180, 0);}}
}
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; }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>();}
}
Player.cs
創建并初始化技能管理器。
//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;[Header("Dash Info")]public float dashSpeed=25f;public float dashDuration=0.2f;public float dashDir { get; private set; }public SkillManager skill;#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; }#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");}// 設置初始狀態protected override void Start(){base.Start();skill = SkillManager.instance;StateMachine.Initialize(idleState);}// 更新protected override void Update(){base.Update();StateMachine.currentState.Update();CheckForDashInput();}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);}}}