Unity教程(二十四)技能系统 投剑技能(中)技能变种实现

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

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

技能系统

Unity教程(二十一)技能系统 基础部分
Unity教程(二十二)技能系统 分身技能
Unity教程(二十三)技能系统 掷剑技能(上)基础实现
Unity教程(二十四)技能系统 掷剑技能(中)技能变种实现
Unity教程(二十五)技能系统 掷剑技能(下)冻结时间实现

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


文章目录


前言

注意:Udemy上更新了课程,原来的版本删掉了。新版教程与旧版顺序相差较大,并且改用Unity6。但这个笔记已经写到一半了,所以还是按照旧版来。

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

本节实现角色投剑技能的变种。

Udemy课程地址

对应视频:

Bouncy sword

Setting sword type

Pierce sword

Saw spin sword


一、概述

本节实现投剑技能的几种变种类型,包括常规、弹射、穿刺和盘旋,并实现对敌人的伤害。

二、类型设置

在Sword_Skill脚本中创建枚举类型SwordType表示四种投掷类型。

添加变量Sword_Skill类中添加投剑类型变量,默认为常规。

csharp 复制代码
public enum SwordType
{
	Regular,
	Bounce,
	Pierce,
	Spin
}
csharp 复制代码
public class Sword_Skill : Skill
{
    public SwordType swordType = SwordType.Regular;
}

三、弹射类型的实现

弹射要实现的效果是在击中一个敌人后,在附近敌人身上弹射攻击几次,弹射次数归零后返回。

因此,实现时要在击中某个敌人后,检测以剑为中心一定半径范围内的敌人并记录。依据列表让剑在敌人之间移动,同时设置好动画,击中时取消嵌入,弹射次数归零后返回。

(1)创建参数

首先在Sword_Skill中添加弹射类型需要更改的变量,包括弹射的重力、次数、速度。

在创建剑时判断剑的类型,给剑的重力赋值,将弹跳相关参数传入控制器。这部分要写在设置Sword的参数之前,因为要先给剑的重力赋值再通过SetupSword传入。

csharp 复制代码
    [Header("Bounce Info")]
    [SerializeField] private float bounceGravity;
    [SerializeField] private int bounceAmount;
    [SerializeField] private float bounceSpeed;

    public void CreateSword()
    {
        GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);

        Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();

        if(swordType == SwordType.Bounce)
        {
            swordGravity = bounceGravity;
            newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
        }

        newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);

        player.AssignNewSword(newSword);

        DotsActive(false);
    }

在Sword_Skill_Controller中添加弹射所需参数

添加SetupBounce函数设置参数

csharp 复制代码
    [Header("Bounce Info")]
    private bool isBouncing;
    private int amountOfBounce;
    private float bounceSpeed;
    
    public void SetupBounce(bool _isBouncing, int _amountOfBounces , float _bounceSpeed)
    {
        isBouncing = _isBouncing;
        amountOfBounce = _amountOfBounces;
        bounceSpeed = _bounceSpeed;
    }

(2)弹射的实现

在弹射类型时让剑一直保持旋转动画的状态,并且不挂载到角色下随角色移动。

csharp 复制代码
    private void StuckInto(Collider2D collision)
    {
        canRotate = false;
        cd.enabled = false;

        rb.isKinematic = true;
        rb.constraints = RigidbodyConstraints2D.FreezeAll;

        if(isBouncing)
            return;

        anim.SetBool("Rotation", false);
        transform.parent = collision.transform;
    }

在Sword_Skill_Controller中添加列表存储范围内敌人,添加当前索引变量。设置弹射参数时将列表初始化。

在玩家击中第一个敌人时,获取一定范围内的敌人,并将它们存在列表中。

将这部分提取出来,重命名为SetupTargetsForBounce函数。

csharp 复制代码
    [Header("Bounce Info")]
    private bool isBouncing;
    private int amountOfBounce;
    private float bounceSpeed;
    private List<Transform> enemyTarget;
    private int targetIndex;

    public void SetupBounce(bool _isBouncing, int _amountOfBounces , float _bounce)
    {
        isBouncing = _isBouncing;
        amountOfBounce = _amountOfBounces;

        enemyTarget = new List<Transform>();
    }
    
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (isReturning)
            return;
            
        SetupTargetsForBounce(collision);

        StuckInto(collision);
    }

    private void SetupTargetsForBounce(Collider2D collision)
    {
        if (collision.GetComponent<Enemy>() != null)
        {
            if (isBouncing && enemyTarget.Count <= 0)
            {
                Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 10);

                foreach (var hit in colliders)
                {
                    if (hit.GetComponent<Enemy>() != null)
                        enemyTarget.Add(hit.transform);
                }
            }
        }
    }

在Update中实现剑在敌人中弹射移动。当处于弹射类型且在上面步骤中能获取到敌人时,让剑以一定速度从现在的位置向当前索引敌人位置移动。移动到剑与敌人间距很小时,将索引增加,换向下一个目标弹射。

由于敌人数目可能小于弹射次数,可以进行取模操作,当弹射完整个列表的敌人时,从列表第一个敌人再次开始弹射。

每次弹射时,将弹射次数减一,弹射次数小于等于零时,结束弹射并让剑返回。

将弹射的实现部分提取成函数BounceLogic。

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

        BounceLogic();
    }

    private void BounceLogic()
    {
        if (isBouncing && enemyTarget.Count > 0)
        {

            transform.position = Vector2.MoveTowards(transform.position, enemyTarget[targetIndex].position, bounceSpeed * Time.deltaTime);

            if (Vector2.Distance(transform.position, enemyTarget[targetIndex].position) < 0.1f)
            {
                targetIndex = (targetIndex + 1) % enemyTarget.Count;

                bounceAmount--;
                if (bounceAmount <= 0)
                {
                    isBouncing = false;
                    isReturning = true;
                }

            }
        }
    }

将骷髅复制一个方便测试。

在技能管理器中给弹射参数赋上合适的值。

效果如下:

(3)弹射伤害的实现

每次距离接近时调用当前敌人的Damage函数即可。

csharp 复制代码
    private void BounceLogic()
    {
        if (isBouncing && enemyTarget.Count > 0)
        {

            transform.position = Vector2.MoveTowards(transform.position, enemyTarget[targetIndex].position, bounceSpeed * Time.deltaTime);

            if (Vector2.Distance(transform.position, enemyTarget[targetIndex].position) < 0.1f)
            {
                enemyTarget[targetIndex].GetComponent<Enemy>().Damage();

                targetIndex = (targetIndex + 1) % enemyTarget.Count;

                bounceAmount--;
                if (bounceAmount <= 0)
                {
                    isBouncing = false;
                    isReturning = true;
                }

            }
        }
    }

(3)问题改进

这时存在一个问题,剑如果在碰撞在地面和墙体上会一直空转。

解决方式很简单,在实现嵌入墙体功能的StuckInto函数中,修改弹射类型返回的条件。当处于弹射类型且击中了其他东西时,由于没有与敌人发生碰撞,因此获取不到敌人的列表,只需添加条件获取到的敌人数量大于0时才可返回即可。

csharp 复制代码
    private void StuckInto(Collider2D collision)
    {
        canRotate = false;
        cd.enabled = false;

        rb.isKinematic = true;
        rb.constraints = RigidbodyConstraints2D.FreezeAll;

        if(isBouncing && enemyTarget.Count > 0)
            return;

        anim.SetBool("Rotation", false);
        transform.parent = collision.transform;
    }

由于剑的碰撞面积过大,容易出现卡在边角的情况,修改碰撞盒大小,还可以增加一点偏移,调一下碰撞盒位置。

这里发现一个小问题,敌人和敌人之间不应该碰撞,改变碰撞矩阵。

三、穿刺类型的实现

穿刺要实现的效果是根据穿刺次数,直线穿过所有敌人造成伤害。

实现直线穿刺可以将剑的重力设置小一些,就会产生直线的轨迹。穿刺时关闭旋转动画取消嵌入,检测敌人并造成伤害。

(1)创建参数

首先在Sword_Skill中添加穿刺类型需要更改的变量,包括穿刺的重力、次数。

由于每个类型都要设置重力,就直接将设置重力提出来作为一个函数,为每个类型设置不同重力,在Start中调用函数。

CreateSword中依旧调用控制器中的设置函数,设置不同类型的参数,将穿刺次数传入。

csharp 复制代码
    [Header("Pierce Info")]
    [SerializeField] private int pierceAmount;
    [SerializeField] private float pierceGravity;

    protected override void Start()
    {
        base.Start();

        GenerateDots();

        SetupGravity();
    }
    
    private void SetupGravity()
    {
        if (swordType == SwordType.Bounce)
            swordGravity = bounceGravity;
        else if (swordType == SwordType.Pierce)
            swordGravity = pierceGravity;
    }

    public void CreateSword()
    {
        GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);

        Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();

        if (swordType == SwordType.Bounce)
            newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
        else if (swordType == SwordType.Pierce)
            newSwordScript.SetupPierce(pierceAmount);

        newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);

        player.AssignNewSword(newSword);

        DotsActive(false);
    }

在Sword_Skill_Controller中添加穿刺所需参数

添加SetupPierce函数设置参数

csharp 复制代码
    [Header("Pierce Info")]
    private float pierceAmount;

    public void SetupPierce(int _pierceAmount)
    {
        pierceAmount = _pierceAmount;
    }

(2)穿刺的实现

在穿刺类型时让剑在穿刺次数不为零时,不打开旋转动画,且穿过敌人不嵌入其中。

在Sword_Skill_Controller中添加

csharp 复制代码
    public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed)
    {
        player = _player;

        rb.velocity = _dir;
        rb.gravityScale = _gravityScale;

        returnSpeed = _returnSpeed;

        if(pierceAmount<=0)
            anim.SetBool("Rotation", true);
    }
    
    private void StuckInto(Collider2D collision)
    {
        if(pierceAmount >0 && collision.GetComponent<Enemy>() != null)
        {
            pierceAmount--;
            return;
        }

        canRotate = false;
        cd.enabled = false;

        rb.isKinematic = true;
        rb.constraints = RigidbodyConstraints2D.FreezeAll;

        if(isBouncing && enemyTarget.Count > 0)
            return;

        anim.SetBool("Rotation", false);
        transform.parent = collision.transform;
    }

在技能管理器中选择穿刺类型,并给穿刺的参数赋上合适的值。

(3)穿刺伤害的实现

在触发器函数中调用,当剑的触发器检测到敌人时,敌人调用Damage函数。

教程里用了C#中的 ?. Null条件运算符。详情请见C#官方文档

在这里的作用是判断获取的敌人是否为空,不为空则调用Damage函数。

csharp 复制代码
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (isReturning)
            return;

        collision.GetComponent<Enemy>()?.Damage();

        SetupTargetsForBounce(collision);

        StuckInto(collision);
    }

    private void SetupTargetsForBounce(Collider2D collision)
    {
        if (collision.GetComponent<Enemy>() != null)
        {
            if (isBouncing && enemyTarget.Count <= 0)
            {
                Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 10);

                foreach (var hit in colliders)
                {
                    if (hit.GetComponent<Enemy>() != null)
                        enemyTarget.Add(hit.transform);
                }
            }
        }
    }

(4)问题改进

现在创建的剑会一直存在,在某些角度下会飞出去很远很难收回,因此需要添加剑在一定时间后自动销毁。

创建销毁剑的函数DestroyMe。使用Invoke在一定时间后调用。

csharp 复制代码
    private void DestroyMe()
    {
        Destroy(gameObject);
    }

    public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed)
    {
        player = _player;

        rb.velocity = _dir;
        rb.gravityScale = _gravityScale;

        returnSpeed = _returnSpeed;

        if(pierceAmount<=0)
            anim.SetBool("Rotation", true);

        Invoke("DestroyMe", 7);
    }

为了方便观察效果,我先把销毁时间调一个很小的数。

四、盘旋类型的实现

盘旋要实现的效果是扔出剑后,剑命中敌人或达到最远距离后,悬停对敌人造成伤害,持续时间结束后返回。

盘旋状态分为两段,前半段剑正常移动,判断最远距离和击中敌人两个条件,达成时切换到悬停状态。悬停状态每隔一段冷却时间检测敌人对敌人造成伤害,悬停持续时长结束后返回。

(1)创建参数

首先在Sword_Skill中添加盘旋类型需要更改的变量,包括盘旋的最远距离、持续时长、冷却时间等。设置重力,调用SetupSpin设置参数。

这里if-else如果觉得别扭,也可Alt+Enter进入子菜单,转换为switch语句。

csharp 复制代码
    [Header("Spin Info")]
    [SerializeField] private float maxTravelDistance;
    [SerializeField] private float spinDuration;
    [SerializeField] private float spinGravity;

    private void SetupGravity()
    {
        if (swordType == SwordType.Bounce)
            swordGravity = bounceGravity;
        else if (swordType == SwordType.Pierce)
            swordGravity = pierceGravity;
        else if(swordType == SwordType.Spin)
            swordGravity = spinGravity;
    }
    
    public void CreateSword()
    {
        GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);

        Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();

        if (swordType == SwordType.Bounce)
            newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
        else if (swordType == SwordType.Pierce)
            newSwordScript.SetupPierce(pierceAmount);
        else if (swordType== SwordType.Spin)
            newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration);

        newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);

        player.AssignNewSword(newSword);

        DotsActive(false);
    }

在Sword_Skill_Controller中添加盘旋所需参数

添加SetupSpin函数设置参数

csharp 复制代码
    [Header("Spin Info")]
    private bool isSpinning;
    private float maxTravelDistance;
    private float spinDuration;
    private float spinTimer;
    private bool wasStopped;

    public void SetupSpin(bool _isSpinning, float _maxTravelDistance,float _spinDuration)
    {
        isSpinning = _isSpinning;
        maxTravelDistance = _maxTravelDistance;
        spinDuration = _spinDuration;
    }

(2)盘旋的实现

创建函数SpinLogic进行盘旋的实现,在Update函数中调用它,具体实现如下:

创建函数StopWhenSpinning,进行悬停的操作,将wasStopped设置为true并冻结剑的位置。此时开始悬停,设置盘旋的计时器。

处于盘旋状态时,要判断剑飞出去的距离有没有到达最大距离限制,在到达最大距离时让剑悬停。在StuckInto函数中添加盘旋状态时返回,剑不嵌入物体中。

开始悬停后让计时器递减,直到时间归零让剑停止盘旋返回。

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

        BounceLogic();

        SpinLogic();
    }
    
   private void SpinLogic()
   { 
        if(isSpinning)
        {
            if(Vector2.Distance(player.transform.position, transform.position) > maxTravelDistance && !wasStopped)
                StopWhenSpinning();
        }

        if(wasStopped)
        {
            spinTimer  -= Time.deltaTime;

            if(spinTimer < 0)
            {
                isReturning = true;
                isSpinning = false;
            }
        }
   }   
    private void StopWhenSpinning()
    {
        wasStopped = true;
        rb.constraints = RigidbodyConstraints2D.FreezePosition;
        spinTimer = spinDuration;
    }
    
    private void StuckInto(Collider2D collision)
    {
        if(pierceAmount >0 && collision.GetComponent<Enemy>() != null)
        {
            pierceAmount--;
            return;
        }

        if (isSpinning)

            return;

        canRotate = false;
        cd.enabled = false;

        rb.isKinematic = true;
        rb.constraints = RigidbodyConstraints2D.FreezeAll;

        if(isBouncing && enemyTarget.Count > 0)
            return;

        anim.SetBool("Rotation", false);
        transform.parent = collision.transform;
    }

在技能管理器中设置参数

效果如下:

下边这一一小块选做:

依据教程还有一部分修改,在击中第一个敌人后不管有没有达到最远距离直接进入悬停,只需在StuckInto函数中调用StopWhenSpinning函数。这里根据自己想法设置即可。

注意: 这种做法会产生一个问题,每次和敌人产生碰撞时都会刷新一次悬停持续时间,敌人数量很多时持续时间会多次刷新。但是剑的触发器范围并不大,敌人进入的时间差别也不大,所以影响还可以接受。

如果想改进,可以考虑不将将计时器重置写在StopWhenSpinning中,而是只在超出距离和第一次有敌人进入触发器时重置。或者干脆将spinDuration改成整个盘旋类型的技能持续时长,而不是悬停那一小段的。在这里就不详细实现了。

csharp 复制代码
   private void StuckInto(Collider2D collision)
   {
       if(pierceAmount >0 && collision.GetComponent<Enemy>() != null)
       {
           pierceAmount--;
           return;
       }

       if (isSpinning)
       {
           StopWhenSpinning();
           return;
       }

       canRotate = false;
       cd.enabled = false;

       rb.isKinematic = true;
       rb.constraints = RigidbodyConstraints2D.FreezeAll;

       if(isBouncing && enemyTarget.Count > 0)
           return;

       anim.SetBool("Rotation", false);
       transform.parent = collision.transform;
   }

(3)盘旋伤害的实现

在Sword_Skill中添加参数hitCoolDown,表示每隔多久攻击一次。将它传入控制器中。

csharp 复制代码
    [Header("Spin Info")]
    [SerializeField] private float maxTravelDistance;
    [SerializeField] private float spinDuration;
    [SerializeField] private float spinGravity;
    [SerializeField] private float hitCooldown;

    public void CreateSword()
    {
        GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);

        Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();

        if (swordType == SwordType.Bounce)
            newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
        else if (swordType == SwordType.Pierce)
            newSwordScript.SetupPierce(pierceAmount);
        else if (swordType== SwordType.Spin)
            newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration, hitCooldown);

        newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);

        player.AssignNewSword(newSword);

        DotsActive(false);
    }

在Sword_Skill_Controller中,添加冷却时间和计时器。

处于盘旋悬停状态时,每隔一个冷却时间进行一次攻击,由计时器控制。每次计时器归零时,检测周围的敌人,对敌人产生一次伤害,并重置计时器。

csharp 复制代码
    private float hitTimer;
    private float hitCooldown;

    public void SetupSpin(bool _isSpinning, float _maxTravelDistance,float _spinDuration, float _hitCooldown)
    {
        isSpinning = _isSpinning;
        maxTravelDistance = _maxTravelDistance;
        spinDuration = _spinDuration;
        hitCooldown = _hitCooldown;
    }
    
    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();
        }

        BounceLogic();

        SpinLogic();
    }

    private void SpinLogic()
    {
        if (isSpinning)
        {
            if (Vector2.Distance(player.transform.position, transform.position) > maxTravelDistance && !wasStopped)
            {
                StopWhenSpinning();
            }
        }

        if (wasStopped)
        {
            spinTimer -= Time.deltaTime;

            if (spinTimer < 0)
            {
                isReturning = true;
                isSpinning = false;
            }

            hitTimer -= Time.deltaTime;

            if (hitTimer < 0)
            {
                hitTimer = hitCooldown;

                Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 1);

                foreach (var hit in colliders)
                {
                    if (hit.GetComponent<Enemy>() != null)
                        hit.GetComponent<Enemy>().Damage();
                }
            }
        }
    }

设置伤害间隔时间。

效果如下:

总结 完整代码

SwordSkill.cs

添加枚举类型,表示四种投掷方式。

添加三种类型投掷方式的相关参数,并传递到控制器。

添加设置重力的函数。

csharp 复制代码
//Sword_Skill:掷剑技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum SwordType
{
    Regular,
    Bounce,
    Pierce,
    Spin
}

public class Sword_Skill : Skill
{
    public SwordType swordType = SwordType.Regular;

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

    [Header("Bounce Info")]
    [SerializeField] private float bounceGravity;
    [SerializeField] private int bounceAmount;
    [SerializeField] private float bounceSpeed;

    [Header("Pierce Info")]
    [SerializeField] private int pierceAmount;
    [SerializeField] private float pierceGravity;

    [Header("Spin Info")]
    [SerializeField] private float maxTravelDistance;
    [SerializeField] private float spinDuration;
    [SerializeField] private float spinGravity;
    [SerializeField] private float hitCooldown;

    protected override void Start()
    {
        base.Start();

        GenerateDots();

        SetupGravity();
    }

    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 void SetupGravity()
    {
        if (swordType == SwordType.Bounce)
            swordGravity = bounceGravity;
        else if (swordType == SwordType.Pierce)
            swordGravity = pierceGravity;
        else if(swordType == SwordType.Spin)
            swordGravity = spinGravity;
    }

    public void CreateSword()
    {
        GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);

        Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();

        if (swordType == SwordType.Bounce)
            newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
        else if (swordType == SwordType.Pierce)
            newSwordScript.SetupPierce(pierceAmount);
        else if (swordType== SwordType.Spin)
            newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration, hitCooldown);

        newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed);

        player.AssignNewSword(newSword);

        DotsActive(false);
    }

    #region 瞄准
    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;
    }

    #endregion
}

Sword_Skill_Controller.cs

实现三种投掷类型的具体逻辑,添加对敌人的伤害。

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

    [Header("Bounce Info")]
    private bool isBouncing;
    private int bounceAmount;
    private float bounceSpeed;
    private List<Transform> enemyTarget;
    private int targetIndex;

    [Header("Pierce Info")]
    private float pierceAmount;

    [Header("Spin Info")]
    private bool isSpinning;
    private float maxTravelDistance;
    private float spinDuration;
    private float spinTimer;
    private bool wasStopped;

    private float hitTimer;
    private float hitCooldown;


    private void Awake()
    {
        anim = GetComponentInChildren<Animator>();
        rb = GetComponent<Rigidbody2D>();
        cd = GetComponent<CircleCollider2D>();
    }

    private void DestroyMe()
    {
        Destroy(gameObject);
    }

    public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed, float _freezeTimeDuration)
    {
        player = _player;

        rb.velocity = _dir;
        rb.gravityScale = _gravityScale;

        returnSpeed = _returnSpeed;
        freezeTimeDuration = _freezeTimeDuration;

        if (pierceAmount<=0)
            anim.SetBool("Rotation", true);

        Invoke("DestroyMe", 7.0f);
    }

    public void SetupBounce(bool _isBouncing, int _bounceAmount , float _bounceSpeed)
    {
        isBouncing = _isBouncing;
        bounceAmount = _bounceAmount;
        bounceSpeed = _bounceSpeed;

        enemyTarget = new List<Transform>();
    }

    public void SetupPierce(int _pierceAmount)
    {
        pierceAmount = _pierceAmount;
    }

    public void SetupSpin(bool _isSpinning, float _maxTravelDistance,float _spinDuration, float _hitCooldown)
    {
        isSpinning = _isSpinning;
        maxTravelDistance = _maxTravelDistance;
        spinDuration = _spinDuration;
        hitCooldown = _hitCooldown;
    }

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

        BounceLogic();

        SpinLogic();
    }

    private void SpinLogic()
    {
        if (isSpinning)
        {
            if (Vector2.Distance(player.transform.position, transform.position) > maxTravelDistance && !wasStopped)
            {
                StopWhenSpinning();
            }
        }

        if (wasStopped)
        {
            spinTimer -= Time.deltaTime;

            if (spinTimer < 0)
            {
                isReturning = true;
                isSpinning = false;
            }

            hitTimer -= Time.deltaTime;

            if (hitTimer < 0)
            {
                hitTimer = hitCooldown;

                Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 1);

                foreach (var hit in colliders)
                {
                    if (hit.GetComponent<Enemy>() != null)
                        hit.GetComponent<Enemy>().Damage();
                }
            }
        }
    }

    private void BounceLogic()
    {
        if (isBouncing && enemyTarget.Count > 0)
        {

            transform.position = Vector2.MoveTowards(transform.position, enemyTarget[targetIndex].position, bounceSpeed * Time.deltaTime);

            if (Vector2.Distance(transform.position, enemyTarget[targetIndex].position) < 0.1f)
            {
                enemyTarget[targetIndex].GetComponent<Enemy>().Damage();

                targetIndex = (targetIndex + 1) % enemyTarget.Count;

                bounceAmount--;
                if (bounceAmount <= 0)
                {
                    isBouncing = false;
                    isReturning = true;
                }

            }
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (isReturning)
            return;

        if(collision.GetComponent<Enemy>()!=null)
        {
            Enemy enemy = collision.GetComponent<Enemy>();

            enemy.Damage();

            if (!isBouncing && !isSpinning)
                enemy.StartCoroutine("FreezeTimeFor", freezeTimeDuration);
        }

        SetupTargetsForBounce(collision);

        StuckInto(collision);
    }

    private void SetupTargetsForBounce(Collider2D collision)
    {
        if (collision.GetComponent<Enemy>() != null)
        {
            if (isBouncing && enemyTarget.Count <= 0)
            {
                Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 10);

                foreach (var hit in colliders)
                {
                    if (hit.GetComponent<Enemy>() != null)
                        enemyTarget.Add(hit.transform);
                }
            }
        }
    }

    private void StopWhenSpinning()
    {
        wasStopped = true;
        rb.constraints = RigidbodyConstraints2D.FreezePosition;
        spinTimer = spinDuration;
    }

    private void StuckInto(Collider2D collision)
    {
        if(pierceAmount >0 && collision.GetComponent<Enemy>() != null)
        {
            pierceAmount--;
            return;
        }

        if (isSpinning)
        {
            //StopWhenSpinning();
            return;
        }

        canRotate = false;
        cd.enabled = false;

        rb.isKinematic = true;
        rb.constraints = RigidbodyConstraints2D.FreezeAll;

        if(isBouncing && enemyTarget.Count > 0)
            return;

        anim.SetBool("Rotation", false);
        transform.parent = collision.transform;
    }

}
相关推荐
Brookty6 小时前
Java线程安全与中断机制详解
java·开发语言·后端·学习·java-ee
飞速移动的代码菌6 小时前
【DataWhale】快乐学习大模型 | 202507,Task08笔记
笔记·学习
不在了情绪6 小时前
[ The Missing Semester of Your CS Education ] 学习笔记 Vim篇
笔记·学习·vim
试着6 小时前
零基础学习性能测试第五章:JVM性能分析与调优-JVM概念,java程序运行原理
java·jvm·学习·零基础·性能测试
DaLiangChen7 小时前
Unity 实时 CPU 使用率监控
unity·游戏引擎
遇见尚硅谷8 小时前
C语言:20250728学习(指针)
c语言·开发语言·数据结构·c++·笔记·学习·算法
Aronup8 小时前
NLP学习开始01-线性回归
学习·自然语言处理·线性回归
墨迹的陌离10 小时前
【Linux】重生之从零开始学习运维之Mysql
linux·运维·服务器·数据库·学习·mysql
幻风_huanfeng10 小时前
学习人工智能所需知识体系及路径详解
人工智能·学习
试着11 小时前
零基础学习性能测试第六章:性能难点-Jmeter文件上传场景压测
学习·jmeter·零基础·性能测试