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



前言

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

本节进行角色基本攻击的实现,实现基本攻击和连击。
对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P38
【Unity教程】从0编程制作类银河恶魔城游戏P39


一、概述

本节主要进行角色基本攻击和连击的实现。

攻击的切换条件如下:
在这里插入图片描述

为了方便管理与控制我们写一个触发器组件,用来控制攻击动画。
整体结构如下:
在这里插入图片描述

二、基础攻击的实现

(1)使用触发器

我们想实现三连击需要播放动画,这些动画的时长不一,这时如果使用计时器逐个计算设置非常麻烦,因此我们打算使用Unity的Animation Event,在运行到时间线上固定的位置时触发事件。
详情请见官方手册动画事件
之所以大费周章写一个触发器组件,是由于动画事件调用的函数要挂在Animator下,但要将脚本作为组件挂载必须继承自MonoBehaviour类,所以我们需要做这么一个周转。
在这里插入图片描述
在运行到事件帧时,整个调用流程如下图所示:
触发事件帧后,Animatord动画师会调用我们写的触发器组件中的函数AnimationTrigger()。
由于我们修改参数更改状态是在攻击状态中进行的,因此我们要调用当前状态中的函数实现,而触发器没有权限直接获取状态,它要调用父组件player中的函数AnimationTrigger()来实现。
接着这个函数会调用状态机当前状态的AnimationFinishTigger()函数设置参数triggerCalled的值。
这时基础攻击状态类中转换状态的条件满足,状态转换为空闲状态。

在这里插入图片描述
我们先在状态基类PlayerState中创建参数triggerCalled显示触发器有没有被调用,并写改变参数的函数。添加代码:

    protected bool triggerCalled;
    //修改触发器参数
    public virtual void AnimationFinishTrigger()
    {
        triggerCalled = true;
    }

在player中添加函数AnimationFinishTrigger,调用当前状态中的函数

    //设置触发器
    public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();

创建触发器脚本PlayerAnimationTriggers

//PlayerAnimationTriggers:触发器组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerAnimationTriggers : MonoBehaviour
{
    private Player player => GetComponentInParent<Player>();
    private void AnimationTrigger()
    {
        player.AnimationTrigger();
    }
}

把触发器脚本挂在Animator下面 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9458d08cd927421a876c4ade9f65a19a.png#pic_center)

(2)创建攻击动画并设置事件帧

层次面板中选中Animator,在Animation面板中创建动画playerAttack1
playerAttack1精灵表标号14-22、17、18,采样率改为14
具体讲解见Unity教程(零)Unity和VS的使用相关内容
在这里插入图片描述

在这里插入图片描述

同理,playerAttack2,精灵表标号22-25,采样率改为10
在这里插入图片描述
playerAttack3,精灵表标号77-83,采样率改为10
在这里插入图片描述
接下来我们连接状态机。
先把playerAttack2、3移到一旁,做连击的时候再用。
连接playerAttack1并添加过渡条件Attack,并修改过渡设置
在这里插入图片描述

Entry->playerAttack1的过渡,加条件变量
在这里插入图片描述

playerDash -> Exit的过渡,如图更改条件
在这里插入图片描述


我们在playerAttack1的最后一帧设置事件帧,调用触发器函数设置triggerCalled参数。
先把白色竖线拉到最后一帧,把最后一帧调成当前帧
在这里插入图片描述
点击动画面板左边钉子符号的标志添加事件,你会看到最后一帧多出一个小钉子标志
在这里插入图片描述
点击选中事件帧,在右边面板选中函数AnimationTrigger
Fuction->PlayerAnimationTriggers->Methods->AnimationTrigger()
在这里插入图片描述

(3)攻击状态切换

首先创建PlayerPrimaryAttack,它继承自PlayerState,通过菜单生成构造函数和重写。
在这里插入图片描述

在Player中创建PlayerAttack1状态。

   public PlayerPrimaryAttack primaryAttack { get; private set; }  
   
   //创建对象
   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>();

   }

在接地状态PlayerGroundedState中添加切换,当按下鼠标左键时切换到攻击状态

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

        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);
    }

点击鼠标左键小人开始攻击。
在这里插入图片描述
接下来添加停止攻击的条件。
在PlayerPrimaryAttack中添加,当TriggerCalled为真,即触发器被触发时,转换为空闲状态。

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

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

这时就是鼠标点击一下攻击一下了。
在这里插入图片描述

三、连击的实现

(1)连击计数

在PlayerPrimaryAttack中添加连击计数的变量comboCounter,分别对应三个攻击动作。每次攻击动作结束都将comboCounter加一。
同时连击之间不能间隔过长的时间,我们可以设置一个连击窗口comboWindow,两次攻击间隔超过这个时间则重置连击计数。具体实现时可以设置一个变量lastTimeAttack记录上次攻击完成的时间。当前时间小于等于上次攻击时间加连击窗口,则证明还处于连击窗口内,这时攻击能接上连击;反之则连击中断,重新计数。
在这里插入图片描述

    private int comboCounter;

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

    //退出
    public override void Exit()
    {
        base.Exit();
        comboCounter++;
        lastTimeAttacked = Time.time;
    }

(2)动画师创建子状态PrimaryAttack

创建子状态PrimaryAttack
右键->Create Sub-State Machine->重名
在这里插入图片描述
双击进入子状态。
先创建一个空状态,从空状态过渡到三个攻击状态。
在这里插入图片描述
在Animation文件夹中拖入三个攻击动画
在这里插入图片描述
在这里插入图片描述
创建int型参数comboCounter作为进入攻击状态的过渡条件
在这里插入图片描述

在这里插入图片描述
进入攻击状态的过渡,在combo等于0时进入playerAttack1,同理等于1、2时分别进入其他两个状态
在这里插入图片描述
退出攻击状态的条件是攻击为false
在这里插入图片描述
最终
在这里插入图片描述

删除Base Layer中三个攻击状态。
把Entry与子状态PrimaryAttack相连,过渡到Empty
在Entry上右键->MakeTransition->连到PrimaryAttack->States->Empty
在这里插入图片描述
添加条件Attack为true
在这里插入图片描述
完成连接
在这里插入图片描述

最后在另外两个攻击动画上添加事件。
最后一个攻击状态是重击可以把事件向后拖一帧。
在这里插入图片描述

(3)连击完成

在PlayerPrimaryAttack中,进入时设置动画师中comboCounter的值,以便判断进入哪个动画。

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

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

我们成功完成了连击
在这里插入图片描述

总结 完整代码

PlayerState.cs

设置参数triggerCalled

//PlayerState:状态基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerState
{

    protected PlayerStateMachine stateMachine;
    protected Player player;

    protected Rigidbody2D rb;

    protected float xInput;
    protected float yInput;
    private string animBoolName;

    protected float stateTimer;
    protected bool triggerCalled;

    //构造函数
    public PlayerState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName)
    {
        this.stateMachine = _stateMachine;
        this.player = _player;
        this.animBoolName = _animBoolName;
    }

    //进入状态
    public virtual void Enter()
    {
        //进入动画播放条件变量设置
        player.anim.SetBool(animBoolName, true);

        //获取刚体
        rb = player.rb;

        //设置触发器
        triggerCalled = false;
    }

    //更新状态
    public virtual void Update()
    {
        xInput = Input.GetAxisRaw("Horizontal");
        yInput = Input.GetAxisRaw("Vertical");
        player.anim.SetFloat("yVelocity", rb.velocity .y);

        stateTimer -= Time.deltaTime;
    }

    //退出状态
    public virtual void Exit()
    {
        //退出动画播放条件变量设置
        player.anim.SetBool(animBoolName, false);
    }

    //修改触发器参数
    public virtual void AnimationFinishTrigger()
    {
        triggerCalled = true;
    }
}

Player.cs

创建基础攻击状态,调用AnimationFinishTrigger()

//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    [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 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);
        }
    }

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

    //翻转实现
    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();
    }


    //碰撞检测
    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));
    }
}

PlayerAnimationTriggers.cs

触发器组件,调用player中的AnimationTrigger

//PlayerAnimationTriggers:触发器组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerAnimationTriggers : MonoBehaviour
{
    private Player player => GetComponentInParent<Player>();
    private void AnimationTrigger()
    {
        player.AnimationTrigger();
    }
}

PlayerGroundedState.cs

按下鼠标左键切换到基础攻击状态

//超级状态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.Mouse0))
            stateMachine.ChangeState(player.primaryAttack);

        if(!player.isGroundDetected())
            stateMachine.ChangeState(player.airState);

        if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
            stateMachine.ChangeState(player.jumpState);
    }
}

PlayerPrimaryAttack.cs

实现连击,切换空闲状态等

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerPrimaryAttack : PlayerState
{

    private int comboCounter;

    private float lastTimeAttacked;
    private float comboWindow = 2
        ;
    public PlayerPrimaryAttack(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);
        
    }

    //退出
    public override void Exit()
    {
        base.Exit();
        comboCounter++;
        lastTimeAttacked = Time.time;
    }

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

        if(triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值