Unity教程(九)角色攻击的改进

Unity开发2D类银河恶魔城游戏学习笔记

Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进

Unity教程(十)Tile Palette搭建平台关卡

Unity教程(十一)相机

如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录


文章目录


前言

本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。

本节进行角色基本攻击的改进。

对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P40
【Unity教程】从0编程制作类银河恶魔城游戏P41


一、概述

本节主要进行角色基本攻击的改进。

空闲状态到移动状态之间的转换添加条件 ! isBusy

本节改进了基本攻击的小问题,添加了攻击移动和攻击方向,提升了动画流畅度。在最后对代码进行了一些整理。整体结构如下:


二、基础攻击的改进

(1)调整动画提升流畅度

调整三个攻击动画的采样率,并且将最后一个攻击动画事件提前。


(2)解决攻击时滑动的问题

现在我们攻击时角色会来回滑动,因为我们的角色还保持着移动的速度但已经播放攻击动画了。

这里教程中的解决方式是在攻击时将角色速度置为0。

但我个人感觉这种处理方式会让攻击缺少一些灵活性。但如果想实现边移动边攻击就需要有另外的动画和另外的逻辑来实现,需要另行设计,所以在此先按教程中的实现。

此外,我们可以在进入时给stateTimer赋一个值,让角色停顿一下再攻击,做出惯性的效果。

在PlayerPrimaryAttack中修改:

csharp 复制代码
    //进入
    public override void Enter()
    {
        base.Enter();
        if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
            comboCounter = 0;

        player.anim.SetInteger("comboCounter",comboCounter);


        stateTimer = 0.1f;
        
    }

    // 更新
    public override void Update()
    {
        base.Update();

        if (stateTimer < 0)
            rb.velocity = new Vector2(0, 0);

        if(triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }

(3)解决两次攻击间角色移动的问题

我依然认为这种处理方法缺乏灵活性,但可以先学习一下他的处理方式。

如果需要解决这个问题,我们可以使用协程。我参照了以下文章的讲解:
进程、线程和协程之间的区别和联系
Unity 协程(Coroutine)原理与用法详解
Unity官方手册

协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义。

启动协程:

StartCoroutine(IEnumerator routine:通过方法形式调用

StartCoroutine(string methodName,object values):带参数的通过方法名进行调用

停止携程:

StopCoroutine(string methodName:通过方法名(字符串)来进行

StopCoroutine(IEnumerator routine:通过方法形式来调用

StopCoroutine(Coroutine routine):通过指定的协程来关闭

yield方法:

yield return null; 暂停协程等待下一帧继续执行

yield return 0或其他数字; 暂停协程等待下一帧继续执行

yield return new WairForSeconds(时间); 等待规定时间后继续执行

yield return StartCoroutine("协程方法名"); 开启一个协程(嵌套协程)

在Player中创建一个参数isBusy,并定义BusyFor函数

csharp 复制代码
    public bool isBusy { get; private set; }

    public IEnumerator BusyFor(float _seconds)
    {
        isBusy = true;

        yield return new WaitForSeconds(_seconds);

        isBusy = false;
    }

在每次攻击结束时调用,在PlayerPriamaryAttack

csharp 复制代码
    //退出
    public override void Exit()
    {
        base.Exit();

        player.StartCoroutine("BusyFor", 0.15f);

        comboCounter++;
        lastTimeAttacked = Time.time;
    }

然后给空闲状态转到移动状态添加一个 ! Busy 的条件:

csharp 复制代码
    //更新
    public override void Update()
    {
        base.Update();

        //切换到移动状态
        if(xInput!=0 && !player.isBusy)
            stateMachine.ChangeState(player.moveState);
    }

在攻击期间即使我一直按着移动键,角色也不能移动了,效果如下:

(4)攻击间移动

给连击中的每一段设置一个位移。

在Player中添加变量攻击位移数组

csharp 复制代码
    [Header("Attack details")]
    public float[] attackMovement;

在开始攻击时设置位移

csharp 复制代码
    //进入
    public override void Enter()
    {
        base.Enter();
        if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
            comboCounter = 0;

        player.anim.SetInteger("comboCounter",comboCounter);

        player.SetVelocity(player.attackMovement[comboCounter] * player.facingDir, rb.velocity.y);

        stateTimer = 0.1f;
        
    }

给数列赋值,调整数值直到你想要的效果

(5)添加攻击方向

现在的攻击还有一个小问题,在攻击后马上按相反方向键向角色身后攻击,我们会发现角色完全没有转向。

因为我们在攻击时始终是向着角色面向的方向,没有翻转。

我们在PlayerPrimaryAttack中添加这个功能:

csharp 复制代码
    //进入
    public override void Enter()
    {
        base.Enter();
        if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
            comboCounter = 0;

        player.anim.SetInteger("comboCounter",comboCounter);


        float attackDir = player.facingDir;

        if (xInput != 0)
            attackDir = xInput;

        player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);

        stateTimer = 0.1f;
        
    }

当没有输入时,向角色面向方向攻击;当有输入时,向输入方向攻击。现在角色可以迅速回身攻击了。

(6)加速操作(不需要做)

教程中顺便讲到了使所有动画加速的操作。这个操作可以实现不同武器不同攻速等操作,或者实现整体加速。

用player.anim.speed 进行实现,在进入攻击状态时加速,在退出时恢复原速。

csharp 复制代码
    //进入
    public override void Enter()
    {
        base.Enter();
        if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
            comboCounter = 0;

        player.anim.SetInteger("comboCounter",comboCounter);
        player.anim.speed = 3.0f;

        player.SetVelocity(player.attackMovement[comboCounter].x * player.facingDir, player.attackMovement[comboCounter].y);

        stateTimer = 0.1f;
        
    }

    //退出
    public override void Exit()
    {
        base.Exit();

        player.StartCoroutine("BusyFor", 0.15f);
        player.anim.speed = 1.0f;

        comboCounter++;
        lastTimeAttacked = Time.time;
    }

为了效果明显,我调了三倍速。

三、代码整理

速度置零的操作,我们在Player中写一个函数ZeroVelocity()用来调用

csharp 复制代码
    //速度置零
    public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);

在PlayerPrimaryAttack中改为调用函数

csharp 复制代码
//PlayerPrimaryAttackState:基本攻击状态
    // 更新
    public override void Update()
    {
        base.Update();

        if (stateTimer < 0)
            player.ZeroVelocity();

        if(triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }

Player中代码划分区域

速度设置

csharp 复制代码
    #region 速度设置
    //速度置零
    public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);

    //设置速度
    public void SetVelocity(float _xVelocity, float _yVelocity)
    {
        rb.velocity = new Vector2(_xVelocity, _yVelocity);
        FlipController(_xVelocity);
    }
    #endregion

翻转

csharp 复制代码
    #region 翻转
    //翻转实现
    public void Flip()
    {
        facingDir = -1 * facingDir;
        facingRight = !facingRight;
        transform.Rotate(0, 180, 0);
    }
    //翻转控制
    public void FlipController(float _x)
    {
        if (_x > 0 && !facingRight)
            Flip();
        else if(_x < 0 && facingRight)
            Flip();
    }
    #endregion

碰撞

csharp 复制代码
    #region 碰撞
    //碰撞检测
    public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
    public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,whatIsGround);


    //绘制碰撞检测
    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
        Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x+ wallCheckDistance, wallCheck.position.y));
    }
    #endregion

给PlayerPrimaryAttack改名为PlayerPrimaryAttackState
右键状态名->重命名->Enter

总结 完整代码

Player.cs

添加攻击位移变量

添加isBusy和协程

csharp 复制代码
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [Header("Attack details")]
    public Vector2[] attackMovement;
    public bool isBusy { get; private set; }

    [Header("Move Info")]
    public float moveSpeed = 8f;
    public int facingDir { get; private set; } = 1;
    private bool facingRight = true;

    public float jumpForce = 12f;


    [Header("Dash Info")]
    [SerializeField] private float dashCoolDown;
    private float dashUsageTimer;
    public float dashSpeed=25f;
    public float dashDuration=0.2f;
    public float dashDir {  get; private set; }
    


    [Header("Collision Info")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckDistance;
    [SerializeField] private Transform wallCheck;
    [SerializeField] private float wallCheckDistance;
    [SerializeField] private LayerMask whatIsGround;

    #region 组件
    public Animator anim { get; private set; }
    public Rigidbody2D rb { get; private set; }
    #endregion


    #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 PlayerPrimaryAttack primaryAttack { get; private set; }  

    #endregion

    //创建对象
    private void 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 PlayerPrimaryAttack(StateMachine, this, "Attack");

        anim = GetComponentInChildren<Animator>();
        rb = GetComponent<Rigidbody2D>();

    }

    // 设置初始状态
    private void Start()
    {
        StateMachine.Initialize(idleState);
    }

    // 更新
    private void 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()
    {

        dashUsageTimer -= Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer<0)
        {
            dashUsageTimer = dashCoolDown;
            dashDir = Input.GetAxisRaw("Horizontal");

            if (dashDir == 0)
                dashDir = facingDir;

            StateMachine.ChangeState(dashState);
        }
    }

    #region 速度设置
    //速度置零
    public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);

    //设置速度
    public void SetVelocity(float _xVelocity, float _yVelocity)
    {
        rb.velocity = new Vector2(_xVelocity, _yVelocity);
        FlipController(_xVelocity);
    }
    #endregion

    #region 翻转
    //翻转实现
    public void Flip()
    {
        facingDir = -1 * facingDir;
        facingRight = !facingRight;
        transform.Rotate(0, 180, 0);
    }
    //翻转控制
    public void FlipController(float _x)
    {
        if (_x > 0 && !facingRight)
            Flip();
        else if(_x < 0 && facingRight)
            Flip();
    }
    #endregion

    #region 碰撞
    //碰撞检测
    public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
    public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,whatIsGround);


    //绘制碰撞检测
    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
        Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x+ wallCheckDistance, wallCheck.position.y));
    }
    #endregion
}

PlayerPrimaryAttackState.cs

攻击时速度置零

添加协程

添加攻击位移

添加攻击方向

csharp 复制代码
//PlayerPrimaryAttackState:基本攻击状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerPrimaryAttackState : PlayerState
{

    private int comboCounter;

    private float lastTimeAttacked;
    private float comboWindow = 2
        ;
    public PlayerPrimaryAttackState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
    {
    }

    //进入
    public override void Enter()
    {
        base.Enter();
        if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
            comboCounter = 0;

        player.anim.SetInteger("comboCounter",comboCounter);


        float attackDir = player.facingDir;

        if (xInput != 0)
            attackDir = xInput;

        player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);

        stateTimer = 0.1f;
        
    }

    //退出
    public override void Exit()
    {
        base.Exit();

        player.StartCoroutine("BusyFor", 0.15f);

        comboCounter++;
        lastTimeAttacked = Time.time;
    }

    // 更新
    public override void Update()
    {
        base.Update();

        if (stateTimer < 0)
            player.ZeroVelocity();

        if(triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }
}

PlayerIdleState.cs

修改转到移动状态的条件

csharp 复制代码
//PlayerIdleState:空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;

public class PlayerIdleState : PlayerGroundedState
{
    //构造函数
    public PlayerIdleState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
    {
    }

    //进入
    public override void Enter()
    {
        base.Enter();

        player.SetVelocity(0, rb.velocity.y);
    }

    //退出
    public override void Exit()
    {
        base.Exit();
    }

    //更新
    public override void Update()
    {
        base.Update();

        //切换到移动状态
        if(xInput!=0 && !player.isBusy)
            stateMachine.ChangeState(player.moveState);
    }
}
相关推荐
软件黑马王子1 小时前
Unity游戏制作中的C#基础(3)加减乘除算术操作符,比较运算符,逻辑与,或运算符
开发语言·unity·c#
Aphelios3802 小时前
Linux 下 VIM 编辑器学习记录:从基础到进阶(下)
java·linux·学习·编辑器·vim
Best_Me072 小时前
【CVPR2024-工业异常检测】PromptAD:与只有正常样本的少样本异常检测的学习提示
人工智能·学习·算法·计算机视觉
日记成书2 小时前
详细介绍STM32(32位单片机)外设应用
stm32·学习
li星野2 小时前
std::thread的同步机制
开发语言·c++·学习
技术小齐3 小时前
网络运维学习笔记 021 HCIA-Datacom新增知识点02 SDN与NFV概述
运维·网络·学习
im长街4 小时前
Ubuntu22.04 - brpc的安装和使用
学习
知识分享小能手4 小时前
Html5学习教程,从入门到精通,HTML5 简介语法知识点及案例代码(1)
开发语言·前端·javascript·学习·前端框架·html·html5
你可以叫我仔哥呀5 小时前
k8s学习记录:环境搭建(基于Kubeadmin)
学习·容器·kubernetes
[奋斗不止]5 小时前
Unity打包APK报错 using a newer Android Gradle plugin to use compileSdk = 35
unity·unity apk 报错