[TDS周会分享]协程2

问题背景

[图片]

拿伤害判定举例,在灭蚊大战中,两个玩家通过电蚊拍形成的激光来消灭蚊子。
那么逻辑就是在蚊子触碰到激光后受到伤害,问题也随之出现:如果蚊子持续与激光接触,难道要每一帧都让蚊子受到伤害吗?显然不可能,持续造成伤害必然是按照一定频率进行的,否则像英雄联盟中的厄运小姐,每次放E和R,如果不设置内置冷却,每一帧都进行一次命中判定和伤害判定,那么不管是谁在里面都会被秒杀,这显然不合理。
在灭蚊大战中,我通过协程的形式设置冷却机制,同时也作为上一次协程分享的延续,以一次实际应用加深对协程的理解。

实现功能

因为这个项目本身体量比较小,我进行的时候也比较自由,很多数据就比较随意了。目前我是对蚊子的移动和所有单位的受击设置了冷却器。
敌人移动冷却
策划里的描述是,蚊子出现后,每三秒进行一次移动,每次移动朝着最近的玩家,移动持续三秒
流程可以分解为
待机(3s)→移动(3s)→待机…
因为是协程,所以我先定义一个IEnumerator

关于其中一些变量的说明:

  • moveTarget:一个transform对象,用来指定蚊子移动的目标
  • GameController:一个单例类,用来控制游戏核心行为的脚本,里面存放了当前游戏的玩家
  • bluePlayer:蓝色玩家
  • redPlayer:红色玩家
  • isMoving:移动状态的开关,当为true时,蚊子向其moveTarget进行移动
IEnumerator moveTimer()
{
    while (true)
    {
        //Debug.Log("wait 3s");
        yield return new WaitForSeconds(3);
        moveTarget = Vector2.Distance(GameController.Instance.bluePlayer.position, transform.position) 
        < Vector2.Distance(GameController.Instance.redPlayer.position, transform.position)
        ? GameController.Instance.bluePlayer : GameController.Instance.redPlayer;
        isMoving = !isMoving;//通过取反实现状态的切换
    }
}

受击冷却

首先我们设置一个受击的频率,这里我设置为一秒一次。采用受击频率是因为我这里通过碰撞检测是否受击,因为碰撞检测是每帧进行的,我认为通过操控受击单位的可否受击状态相对更容易,当然根据实际情况,也可以设置攻击频率而非受击频率。
这里我计划攻击成功时,将hitable切换为false状态,同时处理受击行为(这个行为通过一个接口实现,所有类似的动作都是处理受击行为而非攻击行为,因为攻击方式是固定的)。

IEnumerator hitTimer()//受击冷却
{
    while(true)
    {
        if(!hitable)
        {
            Debug.Log("蚊子挨打了");
            yield return new WaitForSeconds(1);
            hitable = true;
        }
        yield return null;//这一行非常关键
    }
}

开发过程中问题的产生与解析

移动时的moveTarget为什么不是实时更新

玩家体验

这个考量主要根据我自身的感觉。实时更新并不会产生程序的错误,但是我们从玩家角度来看,蚊子三秒一动,那么在其移动时,就是我们需要关注的点。假设我现在是蓝色玩家,有三只蚊子A,B,C,我在蚊子开始移动时观察到了A朝我移动,那么此时我的关注点在A上,我会默认B和C朝红色玩家移动。如果采用实时更新,那么在操作过程中,B和C可能转移目标朝我移动,这是我不希望看到的,因为这代表了一种不确定性。

性能

实时更新意味着每一帧都要进行坐标的计算,在蚊子数量较大时,这个性能消耗也会被放大。采用定时更新的做法,由于这个冷却器绑定在蚊子个体上,每只蚊子的刷新时间不一定相同,这也使得计算坐标的任务被分散开来,降低了性能的消耗。
为什么要在IEnumerator中使用while(true)
因为冷却器是一个常驻的机制,意味着我需要该冷却器永远存在。虽然我可以通过把行为写在update中实现,但这会导致update的内容过于臃肿,而通过StartCoroutine启用一个带有while(true)的IEnumerator,只要合理的处理while中的内容,我就可以实现一个类似于update但又不在update里的方法,在有多个冷却器共存的情况下,就可以实现他们的解耦。
在IEnumerator使用while(true)为什么有时候会使unity卡死
可以看到,我在两个IEnumerator中都是通过while(true)的形式实现冷却器的,而while(true)本身也是一个十分危险的写法,因为如果不在内部进行处理,那while(true)出现即代表死循环。
我在一开始(编写移动冷却器时)并没有意识到while(true)的问题,因为移动是不受影响的,我只需要三秒一次进行状态的切换,那么while的内部,就会有yield return new WaitForSeconds(3);进行阻塞,避免死循环
但在编写受击冷却时,因为我是基于hitable这个变量进行的,因而在没有受击,也就是hitabl为true时不作处理,此时while内部没有产生阻塞,那么这个while(true)就会进入死循环。到这一步,基本就宣告死刑了。但是编译器并不会有语法错误的提示。我发现这个问题是在我写好代码开始运行游戏测试时,unity提示Waiting for Unity’s code to finish executing 持续了近半小时的情况下,感到不对劲,才发现了该问题。
所以说在IEnumerator使用while(true)时,一定要存在阻塞的条件,即使没有任何行为,也要通过 yield return null来避免陷入死循环。

其他的话

IEnumerator并非实现冷却器的唯一解法,只是我觉得比较好用就这么写了,对于性能之类的也没有过多的考虑。
通过这两个功能的例子,可以引申到许多需要控制频率和冷却的功能上,比如说英雄联盟的角色技能,就可以通过开四个IEnumerator实现四个技能的分别计时。角色的受击僵直和受击频率也可以通过一个IEnumerator控制好,比如说我挨一次打,那么下一秒我不会再受到攻击,而这一秒我也可以播放受击僵直动画。
截止 2023年5月25日00:34:31 为止的源代码
如果上面的不太方便看,可以看这个,因为没写完,并且目前也挺简陋的,所以随便看

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

public class NormalMos : MonoBehaviour,IEnemy
{
    // Start is called before the first frame update
    CharacterInfo info=new CharacterInfo();
    bool isMoving = false;
    [SerializeField]bool hitable = true;//是否可以受击,防止缪撒
    Transform moveTarget;
    void Start()
    {
        info.initAsNormalMos();
        StartCoroutine(moveTimer());
        StartCoroutine(hitTimer());
    }
    // Update is called once per frame
    void Update()
    {
        move();
    }
    void move()
    {
        if (isMoving)
        {
            transform.Translate((moveTarget.position - transform.position).normalized * Time.deltaTime * info.speedRat*info.baseSpeed, Space.World);
        }
    }

    //IEnemy接口实现
    public void onHit(int damage)//挨打,激光碰到就调这个
    {
        if(hitable)
        {
            info.health -= damage;
            hitable = false;
        }
    }
    public void onAttack(Transform player)//攻击,玩家碰到就调这个
    {
        player.GetComponent<StateControllerP>().onGetDamage(info.damage);
    }
    IEnumerator moveTimer()
    {
        while (true)
        {
            //Debug.Log("wait 3s");
            yield return new WaitForSeconds(3);
            moveTarget = Vector2.Distance(GameController.Instance.bluePlayer.position, transform.position) 
            < Vector2.Distance(GameController.Instance.redPlayer.position, transform.position)
            ? GameController.Instance.bluePlayer : GameController.Instance.redPlayer;
            isMoving = !isMoving;
        }
    }
    IEnumerator hitTimer()//受击冷却
    {
        while(true)
        {
            if(!hitable)
            {
                Debug.Log("蚊子挨打了");
                yield return new WaitForSeconds(1);
                hitable = true;
            }
            yield return null;
        }
    }
}

ps:
[2023年9月9日00:30:44]该项目停了,代码不再会修改了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值