相信大家都玩过放置类或挂机类游戏,这些游戏普遍都有回合制战斗,本篇文章将会使用协程来复刻出这种玩法
对于一个回合制游戏,我们需要有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多的文章,以及更复杂的回合制战斗(武器、技能等),感谢大家的支持,如果觉得本篇文章有帮助,请给我点个赞,或给我个关注~