更新日期:2024年6月18日。
项目源码:后续章节发布
索引
- 简介
- 角色数据集(RoleDataSet)
- 一、定义角色数据集类
- 二、角色基础数据(公共数据)
- 角色(Role)
- 一、定义角色类
- 二、角色其他数据(专有数据)
- 三、角色状态
- 四、角色移动与停留
- 五、角色攻击与被击
- 六、角色剧情对话
- 七、角色经验与升级
- 六、角色死亡
简介
本章我们将实现角色
类,角色
类主要用以装载角色数据集
,从而用以驱动角色(移动、剧情对话、攻击等),不过角色驱动
逻辑的主场在寻路系统
及战斗系统
模块,这里我们就不将跨度拉得太大了,以免闪了腰。
角色数据集(RoleDataSet)
在考虑角色之前,我们必须先考虑角色数据集(可以理解为角色的一些重要、公共数据的存储集)
。
试想一下,某一关敌方出现了20个士兵,这些士兵的基础属性需要我们每一个都为其单独定义吗?
肯定不能,这不科学!所以,对于士兵的一些公共数据,我们应当单独定义为角色数据集
,然后给所有士兵使用相同的数据集即可。
一、定义角色数据集类
首先,我们定义角色数据集类RoleDataSet
:
/// <summary>
/// 角色数据集
/// </summary>
[Serializable]
[CreateAssetMenu(menuName = "HTFramework/★ GameComponent/RPG2D/Role Asset", order = 300)]
public sealed class RoleDataSet : DataSetBase
{
}
CreateAssetMenu
使得他可以通过指定菜单路径创建。
二、角色基础数据(公共数据)
角色的基础数据(公共数据:不同的角色之间共用
)我们决定采用六边形属性:速度、生命、攻击、防御、敏捷、会心
。
- 速度:决定了角色每一次移动跑得有多远。
- 生命:决定了角色是否活着;
- 攻击:决定了角色攻击别人时产生的伤害值;
- 防御:决定了角色被别人攻击时受到的伤害值;
- 敏捷:决定了攻击别人时的命中率;
- 会心:决定了攻击别人时的暴击率。
public sealed class RoleDataSet : DataSetBase
{
/// <summary>
/// 速度
/// </summary>
public int Speed;
/// <summary>
/// 生命
/// </summary>
public int HP;
/// <summary>
/// 攻击
/// </summary>
public int ATK;
/// <summary>
/// 防御
/// </summary>
public int DEF;
/// <summary>
/// 敏捷
/// </summary>
public int DEX;
/// <summary>
/// 会心
/// </summary>
public int CRI;
}
不过,思索了2分钟后,我们发觉这样的基础属性好像拉不开等级差距啊,100级跟1级竟然是一样的生命值!打起架来不分上下!?
不行不行,上面这些只能算是初始属性,还得定义相应的随等级增加的属性才行(成长属性
),唰唰唰敲完代码:
public sealed class RoleDataSet : DataSetBase
{
/// <summary>
/// 初始速度
/// </summary>
[Label("初始速度"), Drawer("初始属性", true)] public int BasicSpeed;
/// <summary>
/// 初始生命
/// </summary>
[Label("初始生命")] public int BasicHP;
/// <summary>
/// 初始攻击
/// </summary>
[Label("初始攻击")] public int BasicATK;
/// <summary>
/// 初始防御
/// </summary>
[Label("初始防御")] public int BasicDEF;
/// <summary>
/// 初始敏捷
/// </summary>
[Label("初始敏捷")] public int BasicDEX;
/// <summary>
/// 初始会心
/// </summary>
[Label("初始会心")] public int BasicCRI;
/// <summary>
/// 生命成长值
/// </summary>
[Label("生命成长值"), Drawer("属性成长值", true)] public int GrowthHP;
/// <summary>
/// 攻击成长值
/// </summary>
[Label("攻击成长值")] public int GrowthATK;
/// <summary>
/// 防御成长值
/// </summary>
[Label("防御成长值")] public int GrowthDEF;
/// <summary>
/// 敏捷成长值
/// </summary>
[Label("敏捷成长值")] public int GrowthDEX;
/// <summary>
/// 会心成长值
/// </summary>
[Label("会心成长值")] public int GrowthCRI;
}
这下看起来合理了,我们将初始属性
与成长属性
分开,使得不同类型的角色、不同等级的角色拥有了各自的属性成长曲线。
考虑到速度
属性的独特性,比如速度也有成长的话,哪怕是最小值1,50级后也能一回合移动50格,这是要逆天啊!所以必须剥夺他的成长权利。
然后注意,使用Drawer
特性可以将序列化字段分段整合进可折叠的抽屉中
,大大提升了可读性,他的检视面板将是这样的:
角色(Role)
一、定义角色类
然后,我们定义角色类Role
(很明显一个Role
将持有一个RoleDataSet
数据集):
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 角色数据集
/// </summary>
[Label("角色数据集"), SerializeField] internal RoleDataSet DataSet;
}
二、角色其他数据(专有数据)
角色的其他数据(专有数据:每一个角色专有的
)我们先定义如下这些:
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 头像顶部(一个位置标记,用以标记角色顶部,一些功能可能会用到,比如剧情对话、头顶飘字)
/// </summary>
[Label("头像顶部")] public Transform Top;
/// <summary>
/// 头像渲染器(角色在场景中显示为一个头像方块,这就是渲染器)
/// </summary>
[Label("头像渲染器")] public SpriteRenderer Head;
/// <summary>
/// 边框渲染器(角色头像的边框,用以区分阵营,比如青色为己方,红色为敌方)
/// </summary>
[Label("边框渲染器")] public SpriteRenderer Border;
/// <summary>
/// 阴影渲染器(角色移动时,身下显示的阴影,用以识别角色当前所在地块)
/// </summary>
[Label("阴影渲染器")] public SpriteRenderer Shadow;
/// <summary>
/// 角色头像(角色默认显示头像)
/// </summary>
[Label("角色头像")] public Sprite HeadImage;
/// <summary>
/// 角色头像(灰色)(角色禁用时显示头像,比如本回合已行动)
/// </summary>
[Label("角色头像(灰色)")] public Sprite HeadImage_Gray;
/// <summary>
/// 角色ID(角色唯一标识符,不能重复)
/// </summary>
[Label("角色ID")] public string ID;
/// <summary>
/// 角色姓名
/// </summary>
[Label("角色姓名")] public string Name;
/// <summary>
/// 角色等级
/// </summary>
[Label("角色等级")] public int Grade = 1;
/// <summary>
/// 角色阵营
/// </summary>
[Label("角色阵营")] public RoleCamp Camp = RoleCamp.Player;
}
/// <summary>
/// 角色阵营
/// </summary>
public enum RoleCamp
{
/// <summary>
/// 玩家
/// </summary>
Player = 0,
/// <summary>
/// 敌人
/// </summary>
Enemy = 1
}
三、角色状态
然后考虑到某些角色一开始并不存在于场景中(可能存在但不可见),到达指定回合后才支援
登场,所以再定义如下属性:
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 角色状态(还未登场的不可进行交互,活动中的可以进行交互,即便死亡的角色,也属于活动中)
/// </summary>
[Label("角色状态")] public RoleState State = RoleState.Actived;
/// <summary>
/// 登场回合(初始状态为NotYetOnStage的角色)
/// </summary>
[Label("登场回合")] public int ComeOnStageRound = 2;
/// <summary>
/// 登场地块(初始状态为NotYetOnStage的角色)
/// </summary>
[Label("登场地块")] public Block ComeOnStageBlock;
/// <summary>
/// 是否限制移动(如果为true,角色将无法移动,比如某些大BOSS,强大的实力不允许他们下场参战:你们这些喽啰尽管上前送死便是!)
/// </summary>
[Label("是否限制移动")] public bool IsRestrictMove = false;
}
/// <summary>
/// 角色状态
/// </summary>
public enum RoleState
{
/// <summary>
/// 还未登场
/// </summary>
NotYetOnStage = 0,
/// <summary>
/// 活动中
/// </summary>
Actived = 1
}
四、角色移动与停留
很明显的是角色当前停留在哪个地块
是一个重要的数据,即便我们还没有规划战斗系统
和寻路系统
该怎么写,仅仅考虑到角色按地块移动、按地块距离攻击等逻辑,也必须知道角色当前在哪里(也即是角色坐标
):
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 停留的地块
/// </summary>
[Label("停留的地块")] public Block StayBlock;
}
不过,回想起上一篇的地块类(Block),我想如果我们拿到一个地块,也应该能够知道他上面是否站有角色
(一个地块只能站一个角色)才对,这应当也是一个重要的数据,所以我们添加代码到Block:
/// <summary>
/// 地块
/// </summary>
[DisallowMultipleComponent]
public class Block : HTBehaviour
{
/// <summary>
/// 停留的角色
/// </summary>
[Label("停留的角色")] public Role StayRole;
}
如此便将Role
与Block
建立双向关联,无论我们拿到其中哪一个,都能进一步判断到角色当前的位置。
五、角色攻击与被击
经过深思,这里不再需要相关属性,角色数据集
已完成所有定义。
六、角色剧情对话
经过深思,这里不再需要相关属性(因为剧情对话应该与关卡绑定,不属于任何一个角色)。
七、角色经验与升级
角色应当能够积累经验值,用以升级(至于如何升级,这也应当不是角色自身能管的,他只管积累经验,如何升级定然是交给升级系统
):
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 拥有经验值
/// </summary>
[Label("拥有经验值")] public int HaveExp;
}
六、角色死亡
我们来理一理角色的业务逻辑:
1.还未登场的时候是看不见的。
2.登场后,活着的时候可以:移动、攻击、剧情对话等。
3.如果死了(生命小于等于0),角色也将不可见,且不再执行活着时候的逻辑(也即是需要知道角色是否已经死亡)。
那么加入如下属性(这里定义为property
,一是这些属性无需序列化,二是他们的值在改变时方便做出一些操作):
[DisallowMultipleComponent]
public class Role : HTBehaviour
{
/// <summary>
/// 死亡音效(死亡时,将如何惨叫)
/// </summary>
[Label("死亡音效")] public AudioClip DeadAudio;
/// <summary>
/// 是否死亡
/// </summary>
[PropertyDisplay("是否死亡")]
public bool IsDead
{
get
{
return _isDead;
}
private set
{
_isDead = value;
if (_isDead)
{
//死亡后,应当与站立的地块断开关联
if (StayBlock != null)
{
StayBlock.StayRole = null;
StayBlock = null;
}
//且不再显示角色
IsShow = false;
//_isTriggerDeadEvent,在某些时候我们不想让角色死亡时触发事件和惨叫
//,比如重载存档,已经被打死的角色不可能再让他们全部惨叫一遍
if (_isTriggerDeadEvent)
{
//如果可以惨叫,则惨叫
if (DeadAudio != null) Main.m_Audio.PlayOneShoot(DeadAudio);
//抛出角色死亡事件
Main.m_Event.Throw(Main.m_ReferencePool.Spawn<EventRoleDead>().Fill(LastOpponent, this));
}
}
}
}
/// <summary>
/// 是否显示角色(未登场角色,死亡角色,都使用此属性隐藏自己)
/// </summary>
[PropertyDisplay("是否显示角色")]
public bool IsShow
{
get
{
return gameObject.activeSelf;
}
set
{
gameObject.SetActive(value);
}
}
/// <summary>
/// 最后一次攻击自己的对手(让别人知道是谁kill了你,以免报仇时杀错人)
/// </summary>
public Role LastOpponent { get; set; }
}
然后是一个简单的角色死亡事件
定义:
/// <summary>
/// 角色死亡事件
/// </summary>
public sealed class EventRoleDead : EventHandlerBase
{
/// <summary>
/// 击杀他的角色
/// </summary>
public Role Killer { get; private set; }
/// <summary>
/// 死亡的目标角色
/// </summary>
public Role Target { get; private set; }
/// <summary>
/// 填充数据,所有属性、字段的初始化工作可以在这里完成
/// </summary>
public EventRoleDead Fill(Role killer, Role target)
{
Killer = killer;
Target = target;
return this;
}
/// <summary>
/// 重置引用,当被引用池回收时调用
/// </summary>
public override void Reset()
{
}
}
需要注意的是,目前我们仅在数据结构设计阶段,先不用考虑这些数据如何赋值,我想我们后面总会实现他的。
不过,回想上一章的结尾,我们计划中准备写的回合制逻辑依然不知如何下手,好吧,正所谓计划赶不上变化,而且凡事不能一蹴而就,这俩理由足够给自己一个交代了,闪人便是。