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


文章目录


前言

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

本节实现战斗系统的受击反馈部分。

Udemy课程地址

对应视频:

On Hit Fx

On Hit Impact

Attack's direction hot fix


一、概述

本节我们实现角色的受击反馈,包括闪烁特效,击退效果两部分。

为实现特效,创建了实体特效组件,用于存放作用于实体的特效。闪烁特效用两种材质交替实现。击退效果设置受击角色速度实现。

除此之外,还修复了学习过程中发现的攻击方向bug。

二、闪烁特效的实现

在开始之前我们先整理一下脚本文件,把相应脚本放入创建的文件夹中,文件整理大致如下:

(1)创建闪烁特效材质

首先我们在Materials文件夹中创建闪烁特效FlashFXMaterial。

在Project面板中
右键->Create->Material->重命名为FlashFXMaterial

Shader选择GUI->Text Shader ,Text Color选择白色


更换Playerd Animator的材质看一下效果

将原始材质与这个纯白的来回交替就可以产生闪烁的特效。

(2)实现闪烁特效脚本

首先我们创建一个特效组件EntityFX,用来实现实体的特效。

特效由更换精灵的材质实现,即要调用接口,修改对应实体下Animator中SpriteRenderer组件的Material属性。

首先要在EntityFX中获取相应SpriteRenderer组件,并设置两个变量分别存储原始材质和受击后的材质。还要创建变量flashDuration,定义受击后闪烁的时长。

csharp 复制代码
//EntityFX:实体特效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class EntityFX : MonoBehaviour
{
    private SpriteRenderer sr;

    [Header("Flash FX")]
    [SerializeField] private Material hitMat;
    private Material originalMat;
    [SerializeField] private float flashDuration;


    private void Start()
    {
        sr = GetComponentInChildren<SpriteRenderer>();
        originalMat = sr.material;
    }
}

接着使用协程实现材质每隔一小段时间更替一次材质。

csharp 复制代码
    //闪烁特性
    private IEnumerator FlashFX()
    {
        sr.material = hitMat;

        yield return new WaitForSeconds(flashDuration);

        sr.material = originalMat;
    }

在Entity中创建特效组件EntityFX并赋值。

csharp 复制代码
    #region 组件
    public EntityFX fx {  get; private set; }
    public Rigidbody2D rb { get; private set; }
    public Animator anim { get; private set; }
    #endregion

    //获取组件
    protected virtual void Start()
    {
        fx= GetComponent<EntityFX>();
        rb= GetComponent<Rigidbody2D>();
        anim= GetComponentInChildren<Animator>();
    }

在Entity的Damage函数中调用闪烁特效。

csharp 复制代码
    public virtual void Damage()
    {
        fx.StartCoroutine("FlashFX");
        Debug.Log(gameObject.name + " was damaged");
    }

将EntityFX拖到Player上作为组件,拖入闪烁特效作为受击材质,并给闪烁持续时间赋一个合适的值。

骷髅小怪Enemy_Skeleton同理,最终效果如下:

三、击退效果的实现

击退效果我们依然用协程实现。在实体受到攻击时,设置实体后退速度,等待一小段时间后恢复原来速度设置。

在Entity中创建击退效果相关的变量,分别为击退方向,是否被击退,击退持续的时间。

cpp 复制代码
    [Header("Knockback Info")]
    [SerializeField] protected Vector2 knockbackDirection;
    [SerializeField] protected float knockbackDuration;
    protected bool isKnocked;

创建一个协程HitKnockback设置击退速度。一个实体受到攻击后,后退方向与它面向的方向相反。这里存在一个问题,在玩家从背后攻击骷髅时,一般骷髅会在检测后迅速转身,但个别时候会发生它没有转身就受到攻击的情况,这时骷髅会向它朝向的方向跌,这个问题本节暂不解决。

isKnocked用来记录实体是否处于被击退的状态,在击退效果持续期间我们需要让其他涉及设置速度的操作都不生效,在击退结束后再恢复原来状态里的速度设置。

csharp 复制代码
    protected virtual IEnumerator HitKnockback()
    {
        isKnocked = true;

        rb.velocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);

        yield return new WaitForSeconds(knockbackDuration);

        isKnocked= false;
    }

在速度设置和速度置零函数中添加一个判定,当处于击退状态时直接return,不执行后面设置速度的部分,直到击退结束。

csharp 复制代码
    #region 速度设置
    //速度置零
    public void ZeroVelocity()
    {
        if (isKnocked)
            return;

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

        rb.velocity = new Vector2(_xVelocity, _yVelocity);
        FlipController(_xVelocity);
    }
    #endregion

在Damage函数中调用击退效果。

csharp 复制代码
    public virtual void Damage()
    {
        fx.StartCoroutine("FlashFX");
        StartCoroutine("HitKnockback");


        Debug.Log(gameObject.name + " was damaged");
    }

骷髅受到攻击后直接飘出去了。加大重力就可以解决这一问题。但需要注意加大重力后,骷髅的移速也会变迟缓,可以根据需要设定。

四、攻击方向问题的修复

在学习过程中,发现了一个问题,在个别先向左后向右跑的时候,角色面向右进行攻击时,攻击方向却是向左。

问题出在下图位置。在基本攻击状态PlayerPrimaryAttackState中,我们原本设置的是攻击时如果有输入,攻击方向为输入方向,但在进入攻击状态时xInput并不是最新的。

xInput的定义在状态基类PlayerState中,它的赋值在Update函数中进行,PlayerPrimaryAttackState继承自状态基类,所以每次进入攻击状态获取的xInput都是上次攻击Update中获取的。

教程里的改法是在进入状态时,xInput赋0。我选择了在进入时,重新获取一次xInput。

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


        xInput = Input.GetAxisRaw("Horizontal");
        //攻击方向问题教程改法
        //xInput = 0;

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

总结 完整代码

EntityFX.cs

实体特效组件。实现闪烁特效。

csharp 复制代码
//EntityFX:实体特效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class EntityFX : MonoBehaviour
{
    private SpriteRenderer sr;

    [Header("Flash FX")]
    [SerializeField] private Material hitMat;
    private Material originalMat;
    [SerializeField] private float flashDuration;


    private void Start()
    {
        sr = GetComponentInChildren<SpriteRenderer>();
        originalMat = sr.material;
    }

    //闪烁特效
    private IEnumerator FlashFX()
    {
        sr.material = hitMat;

        yield return new WaitForSeconds(flashDuration);

        sr.material = originalMat;
    }
}

Entity.cs

获取实体特效组件,调用闪烁特效。实现并调用击退效果,修改速度设置。

csharp 复制代码
//Entity:实体类
using System.Collections;
using System.Collections.Generic;
using Unity.IO.LowLevel.Unsafe;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;

public class Entity : MonoBehaviour
{
    [Header("Knockback Info")]
    [SerializeField] protected Vector2 knockbackDirection;
    [SerializeField] protected float knockbackDuration;
    protected bool isKnocked;


    [Header("Flip Info")]
    protected bool facingRight = true;
    public int facingDir { get; private set; } = 1;



    [Header("Collision Info")]
    public Transform attackCheck;
    public float attackCheckRadius;
    [SerializeField] protected Transform groundCheck;
    [SerializeField] protected float groundCheckDistance;
    [SerializeField] protected Transform wallCheck;
    [SerializeField] protected float wallCheckDistance;
    [SerializeField] protected LayerMask whatIsGround;


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

    protected virtual void Awake()
    {

    }

    //获取组件
    protected virtual void Start()
    {
        fx= GetComponent<EntityFX>();
        rb= GetComponent<Rigidbody2D>();
        anim= GetComponentInChildren<Animator>();
    }

    // 更新
    protected virtual void Update()
    {
        
    }

    public virtual void Damage()
    {
        fx.StartCoroutine("FlashFX");
        StartCoroutine("HitKnockback");


        Debug.Log(gameObject.name + " was damaged");
    }

    protected virtual IEnumerator HitKnockback()
    {
        isKnocked = true;

        rb.velocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);

        yield return new WaitForSeconds(knockbackDuration);

        isKnocked= false;
    }

    #region 速度设置
    //速度置零
    public void ZeroVelocity()
    {
        if (isKnocked)
            return;

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

        rb.velocity = new Vector2(_xVelocity, _yVelocity);
        FlipController(_xVelocity);
    }
    #endregion

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

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


    //绘制碰撞检测
    protected virtual 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));
        Gizmos.DrawWireSphere(attackCheck.position, attackCheckRadius);
    }
    #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();


        xInput = Input.GetAxisRaw("Horizontal");
        //攻击方向问题教程改法
        //xInput = 0;

        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);
    }
}
相关推荐
Thomas_YXQ5 小时前
Unity3D项目为什么要使用FairyGUI
开发语言·unity·游戏引擎·unity3d·游戏开发
子燕若水6 小时前
虚幻开发中的MYPROJECTFORPLUG_API
游戏引擎·虚幻
半夏知半秋8 小时前
unity打包流程整理-Windows/Mac/Linux平台
windows·笔记·学习·macos·unity·游戏引擎
benben0448 小时前
通过PS和Unity制作2D动画之四:脚本控制动画
unity·游戏引擎
棪燊8 小时前
Unity集成Wwise并进行开发
unity·游戏引擎
林枫依依9 小时前
Unity Newtonsoft遍历json中的键值对
unity·json
虾球xz9 小时前
游戏引擎学习第41天
学习·算法·游戏引擎
ue星空13 小时前
虚幻引擎生存建造系统
ue5·游戏引擎·虚幻·虚幻引擎
RogerLHJ14 小时前
cocos creator 的 widget组件的使用及踩坑
typescript·游戏引擎·游戏程序·cocos2d
tealcwu16 小时前
【Unity技巧】Unity项目中哪些文件不用管理(.gitignore)
unity·游戏引擎