Unity通过协程实现回合制战斗(一、1v1部分)

本文介绍了如何使用Unity的协程来创建一个1V1回合制战斗系统。通过创建角色数据类和战斗管理类,实现了角色属性配置、战斗逻辑,包括角色血量、攻击力、防御力和闪避率的计算,并通过协程控制每回合的间隔。文章还提供了战斗结果的回调函数,便于扩展战斗结束后的处理。
相信大家都玩过放置类或挂机类游戏,这些游戏普遍都有回合制战斗,本篇文章将会使用协程来复刻出这种玩法
对于一个回合制游戏,我们需要有2种角色,一种是我方角色,一种是敌方角色,本篇文章将实现敌我双方1V1的回合制战斗。

思路

实现一个回合制战斗,我们需要有以下2个类:
  • 角色数据类
  • 战斗管理类

角色数据类型

在角色数据中,我们这里就做一些比较简单属性:

战斗管理类

战斗管理中,会略微复杂一点,如下图所示:
注,这里默认我方角色先手

角色类

既然思路已经捋清楚了,那就开始愉快的写代码吧~
首先,我们来设计角色类,这里我们将使用ScriptableObject来存数据,在Unity项目中新建CharacterData.cs,并用以下代码覆盖即可:
using UnityEngine; [CreateAssetMenu(fileName = "CharacterName", menuName = "创建战斗角色", order = 1)] public class CharacterData : ScriptableObject { /// <summary> /// 角色名 /// </summary> public string characterName; /// <summary> /// 角色血量 /// </summary> public int hp; /// <summary> /// 角色攻击力 /// </summary> public int atk; /// <summary> /// 角色百分比防御力 /// </summary> [Range(0, 100)] public int defRate; /// <summary> /// 角色百分比闪避率 /// </summary> [Range(0, 100)] public int duckRate; public Character ToCharacter() { return new Character() { Name = characterName, Hp = hp, Atk = atk, DefRate = defRate, DuckRate = duckRate }; } } public class Character { /// <summary> /// 角色名 /// </summary> public string Name; /// <summary> /// 角色血量 /// </summary> public int Hp; /// <summary> /// 角色攻击力 /// </summary> public int Atk; /// <summary> /// 角色百分比防御力 /// </summary> public int DefRate; /// <summary> /// 角色百分比闪避率 /// </summary> public int DuckRate; /// <summary> /// 角色是否死亡 /// </summary> public bool Dead => Hp <= 0; }
在这里我们用字段代表了玩家的一些属性,通过Range(0,100)这个标签限制,同时因为继承了ScriptableObject,我们可以很方便的创建数据并保存下来。
注,我们还做了个ToCharacter方法,可以通过配置文件返回战斗时需要用到的角色类型(这个涉及到深拷贝和浅拷贝,可以自行了解)
我们只需在Unity内,在Project窗口下右键,即可看到创造战斗角色的选项,点击后就会生成一个新的文件:
创建后,我们将其命名为主角,然后我们便可以在Inspector上看到主角的数据:
接着,我们对其的数据进行一些修改:
  • Character Name 我们将其命名为主角(或你喜欢的名字)
  • Hp 我们将其设置为100
  • Atk 我们将其设置为15
  • Def Rate 我们将其设置为20,既无视20%的伤害
  • Duck Rate 我们将其设置为10,既10%概率可以闪避
最后主角的属性面板看起来像这样:
接着我们创建一个怪物:
还是同样的操作,Project下右键创建新的战斗角色数据,命名为小怪1,然后选择,进入Inspector面板,配置数据,
这里的数据如下:
  • Character Name 我们将其命名为小怪1(或你喜欢的名字)
  • Hp 我们将其设置为70
  • Atk 我们将其设置为10
  • Def Rate 我们将其设置为10,既无视10%的伤害
  • Duck Rate 我们将其设置为3,既3%概率可以闪避
最后效果如下:
恭喜,角色类及其配置已经完成,接下来开始写战斗管理类的代码吧~

战斗管理类

在这里,我们会运用到协程,来帮助我们完成回合制战斗,
结尾会提供战斗管理类完整代码
我们需要3个字段:
public CharacterData playerData; public CharacterData enemyData; private readonly WaitForSeconds _delaySeconds = new WaitForSeconds(0.3f);
第一个字段是我方角色配置,第二个字段是敌方角色配置,第三个字段是每个回合的间隔
因为我们将使用协程来实现回合制战斗,而WaitForSeconds是协程里等待固定时长的一个方法,既yield return new WaitForSeconds(1)就是等待一秒,但是这边每次等待都会new一个WaitForSeconds对象,会造成GC,所以我们直接定义一个等待0.3秒的WaitForSeconds对象,这样我们只需要yield return _delaySeconds即可,不会造成GC
如果不了解GC,可以了解一下,或者忽略上面提到的内容
接下来我们需要实现协程,只需要根据之前提到的思路设计即可,回合战斗的大致是这样的:
private IEnumerator Fight() { int round = 1; bool playerTurn = true; while (!player.Dead && !enemy.Dead) { Debug.Log($"第{round}回合开始"); //TODO 战斗逻辑 round++; if (!player.Dead && !enemy.Dead) { yield return _delaySeconds; } } }
当玩家和敌方都没死亡时,持续循环战斗,如果双方都没阵亡的话每次回合间隔0.3秒(上方提到了如何等待的问题),这里round是回合数,playerTurn代表是否是玩家的回合(这里默认玩家先手),需要注意的是我们在这还没定义player和enemy,我们需要将配置数据转为角色数据
现在我们来实现循环内部的部分战斗逻辑,同时定义一下player和enemy,具体如下:
private IEnumerator Fight() { int round = 1; bool playerTurn = true; Character player = playerData.ToCharacter(); Character enemy = enemyData.ToCharacter(); while (!player.Dead && !enemy.Dead) { Debug.Log($"第{round}回合开始"); Character attacker = playerTurn ? player : enemy; Character opponent = playerTurn ? enemy : player; playerTurn = !playerTurn; Debug.Log($"{attacker.Name}{opponent.Name}发起了进攻"); //TODO 具体战斗逻辑 round++; if (!player.Dead && !enemy.Dead) { yield return _delaySeconds; } } }
这里我们定义了player是playerData数据的深拷贝,enemy是enemyData数据的深拷贝。
同时我们定义了attacker和opponent,既攻击方和被攻击方,且我们实现了切换攻击方。
这里对攻击方和被攻击方赋值是因为不这么做需要针对2种if情况写2次相同的代码,完全没必要这么做,所以直接对引用类型赋值即可
到这里,我们可以开始实现完整的战斗逻辑了,我们先实现判断闪避:
bool canDuck = Random.Range(0, 101) <= opponent.DuckRate; if (canDuck) { Debug.Log($"{opponent.Name}闪避了!"); } else { //TODO 受到攻击 }
UnityEngine.Random.Range(0,101)的范围是0~100,既如果随机出来的0~100的数小于等于闪避几率,判定为闪避成功
接着,我们来实现受到伤害的逻辑:
int dmg = attacker.Atk * (100 - opponent.DefRate) / 100; opponent.Hp -= dmg; Debug.Log($"{attacker.Name}{opponent.Name}造成了{dmg}点伤害,{opponent.Name}剩余{opponent.Hp}点血量!");
到这里,战斗逻辑就算完成了,我们改一下Log的样式,在Unity项目内创建FightMgr.cs,将以下代码覆盖,同时在Hierarchy中创建FightMgr的游戏物体,挂上FightMgr这个脚本,然后对PlayerData和EnemyData赋值(用之前创建的数据配置)
using UnityEngine; using System.Collections; public class FightMgr : MonoBehaviour { public CharacterData playerData; public CharacterData enemyData; private readonly WaitForSeconds _delaySeconds = new WaitForSeconds(0.3f); private void Start() { StartCoroutine(Fight()); } private IEnumerator Fight() { int round = 1; bool playerTurn = true; Character player = playerData.ToCharacter(); Character enemy = enemyData.ToCharacter(); while (!player.Dead && !enemy.Dead) { Debug.LogWarning($"第{round}回合开始"); Character attacker = playerTurn ? player : enemy; Character opponent = playerTurn ? enemy : player; playerTurn = !playerTurn; Debug.Log($"{attacker.Name}{opponent.Name}发起了进攻"); bool canDuck = Random.Range(0, 101) <= opponent.DuckRate; if (canDuck) { Debug.LogError($"{opponent.Name}闪避了!"); } else { int dmg = attacker.Atk * (100 - opponent.DefRate) / 100; opponent.Hp -= dmg; Debug.LogError($"{attacker.Name}{opponent.Name}造成了{dmg}点伤害,{opponent.Name}剩余{opponent.Hp}点血量!"); } round++; if (!player.Dead && !enemy.Dead) { yield return _delaySeconds; } } } }
让我们运行一下看看效果:
可以看到,我们的回合制战斗实现的很成功,会进行回合之间的等待,会进行防御力计算,也有概率闪避,同时当有一方阵亡时,循环就会退出。
但是我们少写了一个部分,那就是战斗结果结算,现在我们补一下,将以下代码覆盖到FightMgr.cs即可:
using System; using UnityEngine; using System.Collections; using Random = UnityEngine.Random; public class FightMgr : MonoBehaviour { public CharacterData playerData; public CharacterData enemyData; private readonly WaitForSeconds _delaySeconds = new WaitForSeconds(0.3f); private void Start() { StartCoroutine(Fight(() => { Debug.Log("玩家胜利"); //TODO 胜利回调 }, () => { Debug.Log("玩家失败"); //TODO 失败回调 })); } private IEnumerator Fight(Action sucCallback,Action lostCallback) { int round = 1; bool playerTurn = true; Character player = playerData.ToCharacter(); Character enemy = enemyData.ToCharacter(); while (!player.Dead && !enemy.Dead) { Debug.LogWarning($"第{round}回合开始"); Character attacker = playerTurn ? player : enemy; Character opponent = playerTurn ? enemy : player; playerTurn = !playerTurn; Debug.Log($"{attacker.Name}{opponent.Name}发起了进攻"); bool canDuck = Random.Range(0, 101) <= opponent.DuckRate; if (canDuck) { Debug.LogError($"{opponent.Name}闪避了!"); } else { int dmg = attacker.Atk * (100 - opponent.DefRate) / 100; opponent.Hp -= dmg; Debug.LogError($"{attacker.Name}{opponent.Name}造成了{dmg}点伤害,{opponent.Name}剩余{opponent.Hp}点血量!"); } round++; if (!player.Dead && !enemy.Dead) { yield return _delaySeconds; } else { if (player.Dead) { lostCallback?.Invoke(); } else { sucCallback?.Invoke(); } } } } }
在这里我们用2个Action完成了不同结局的回调,在调用的时候对回调事件传参即可
至此,1V1回合制战斗结束了!大家也可以尝试修改配置数据~

补充

这里计算防御力就很简易,直接用百分比去控制,还可以用别的算法计算出一个基于防御力影响伤害的曲线(取曲线上某个点的值做伤害),这样计算出来的伤害会更有趣一点(因为每次都不一样),有兴趣的小伙伴可以自己搜搜伤害计算公式,然后套入目前计算伤害的地方

结尾

感谢阅读本篇文章,后续我还会写出1V多,和多V多的文章,以及更复杂的回合制战斗(武器、技能等),感谢大家的支持,如果觉得本篇文章有帮助,请给我点个赞,或给我个关注~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值