Unity教程(二十二)技能系统 分身技能

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开发2D类银河恶魔城游戏学习笔记目录


文章目录


前言

本文为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函数。

csharp 复制代码
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基类,将它挂到技能管理器下。


在技能管理器脚本中创建分身技能并赋值。

csharp 复制代码
    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中添加设置分身信息的函数。

csharp 复制代码
//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设置分身信息。

csharp 复制代码
//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方便管理。

csharp 复制代码
    public SkillManager skill;

    // 设置初始状态
    protected override void Start()
    {
        base.Start();

        skill = SkillManager.instance;
        
        StateMachine.Initialize(idleState);
    }

接着在PlayerDashState的中调用CreateClone,使得冲刺后在玩家位置创建一个分身。

csharp 复制代码
    //进入状态
    public override void Enter()
    {
        base.Enter();

        player.skill.clone.CreateClone(player.transform);

        //设置冲刺持续时间
        stateTimer = player.dashDuration;
    }

效果如下:

基本功能已经实现,但有个很明显的问题,创建的分身没有回收。

(3)分身的消失与销毁

此技能需要让分身持续一段时间后逐渐消失,可以使clone预制件的图像随时间透明度逐渐降低实现,最后透明度降到0时销毁分身。这需要用到计时器,而且要设置分身持续的时长和消失的速度。

我们把经常需要变动修改的变量放在CloneSkill中方便管理,这里将分身持续时长cloneDuration放在里面。

csharp 复制代码
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时销毁对象。

csharp 复制代码
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中

csharp 复制代码
    public void CreateClone(Transform _clonePosition)
    {
        GameObject newClone = Instantiate(clonePerfab,_clonePosition);

        newClone.GetComponent<Clone_Skill_Controller>().SetupClone(cloneDuration);
    }

在Clone_Skill_Controller中

csharp 复制代码
    //设置分身信息
    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用于测试。

csharp 复制代码
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 ) 区间内的整数。

csharp 复制代码
    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与玩家攻击的实现逻辑一样,检查攻击范围内的敌人,造成伤害效果。

添加如下代码:

csharp 复制代码
    [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勾掉就可以了。

现在每次冲刺产生分身就是正常攻击一次了。

(3)分身攻击方向

最后一个需要解决的问题是分身面向现在是固定朝右的,我们需要再添加一个让分身朝向最近敌人攻击的功能。

在分身一定范围内检测敌人,比较后选取离分身最近的一个。如果它在分身左侧,则翻转分身让它面向左侧。

csharp 复制代码
    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

添加分身的创建。

csharp 复制代码
//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

创建分身,添加经常需要在面板上更改的相关变量。

csharp 复制代码
//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

设置分身信息,实现分身逐渐消失,实现分身攻击,将分身改为面向最近的敌人。

csharp 复制代码
//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

创建分身技能。

csharp 复制代码
//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

创建并初始化技能管理器。

csharp 复制代码
//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);
        }
    }

}
相关推荐
charlie1145141914 小时前
单片机开发资源分析的实战——以STM32F103C8T6为例子的单片机资源分析
stm32·单片机·嵌入式硬件·学习·教程
努力往上爬de蜗牛4 小时前
react学习1.搭建react环境
javascript·学习·react.js
sealaugh324 小时前
aws(学习笔记第三十三课) 深入使用cdk 练习aws athena
笔记·学习·aws
Liii4035 小时前
Java学习——数据库查询操作
java·数据库·学习
TO_WebNow6 小时前
Python学习- 数据结构类型
开发语言·python·学习
怪我冷i6 小时前
LogicFlow介绍
人工智能·学习·大模型
Suckerbin6 小时前
PHP前置知识-HTML学习
前端·学习·html
且听风吟5676 小时前
E902基于bash与VCS的仿真环境建立
笔记·学习