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=vxt,y=vyt+21gt2
调用计算函数,每隔固定时刻取一个点,显示整条轨迹。函数将在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){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>();}
}