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、付费专栏及课程。

余额充值