类似魔兽世界,moba这种技能极其复杂,灵活性要求极高的技能系统,必须需要一套及其灵活的数值结构来搭配。数值结构设计好了,实现技能系统就会非常简单,否则就是一场灾难。比如魔兽世界,一个人物的数值属性非常之多,移动速度,力量,怒气,能量,集中值,魔法值,血量,最大血量,物理攻击,物理防御,法术攻击,法术防御,等等多达几十种之多。属性跟属性之间又相互影响,buff又会给属性增加绝对值,增加百分比,或者某种buff又会在算完所有的增加值之后再来给你翻个倍。

普通的做法:

一般就是写个数值类:

class Numeric
{
public int Hp;
public int MaxHp;
public int Speed;
// 能量
public int Energy;
public int MaxEnergy;
// 魔法
public int Mp;
public int MaxMp;
.....
}

仔细一想,我一个盗贼使用的是能量,为什么要有一个Mp的值?我一个法师使用的是魔法为什么要有能量的字段?纠结这个搞毛,当作没看见不就行了吗?实在不行,我来个继承?

// 法师数值
calss MageNumeric: Numeric
{
// 魔法
public int Mp;
public int MaxMp;
} // 盗贼数值
calss RougeNumeric: Numeric
{
// 能量
public int Energy;
public int MaxEnergy;
}

10个种族,每个种族7,8种英雄,光这些数值类继承关系,你就得懵逼了吧。面向对象是难以适应这种灵活的复杂的需求的。

再来看看Numeric类,每种数值可不能只设计一个字段,比如说,我有个buff会增加10点Speed,还有种buff增加50%的speed,那我至少还得加三个二级属性字段

class Numeric
{
// 速度最终值
public int Speed;
// 速度初始值
public int SpeedInit;
// 速度增加值
public int SpeedAdd;
// 速度增加百分比值
public int SpeedPct;
}

SpeedAdd跟SpeedPct改变后,进行一次计算,就可以算出最终的速度值。buff只需要去修改SpeedAdd跟SpeedPct就行了。

Speed = (SpeedInit + SpeedAdd) * (100 + SpeedPct) / 100

每种属性都可能有好几种间接影响值,可以想想这个类是多么庞大,初略估计得有100多个字段。麻烦的是计算公式基本一样,但是就是无法统一成一个函数,例如MaxHp,也有buff影响

class Numeric
{
public int Speed;
public int SpeedInit;
public int SpeedAdd;
public int SpeedPct; public int MaxHp;
public int MaxHpInit;
public int MaxHpAdd;
public int MaxHpPct;
}

也得写个Hp的计算公式

MaxHp=(MaxHpInit + MaxHpAdd) * (100  + MaxHpPct) / 100

几十种属性,就要写几十遍,并且每个二级属性改变都要正确调用对应的公式计算. 非常麻烦! 这样设计还有个很大的问题,buff配置表填对应的属性字段不是很好填,例如疾跑buff(增加速度50%),在buff表中怎么配置才能让程序简单的找到并操作SpeedPct字段呢?不好搞。

ET框架采用了Key Value形式保存数值属性

using System.Collections.Generic;

namespace Model
{
public enum NumericType
{
Max = 10000, Speed = 1000,
SpeedBase = Speed * 10 + 1,
SpeedAdd = Speed * 10 + 2,
SpeedPct = Speed * 10 + 3,
SpeedFinalAdd = Speed * 10 + 4,
SpeedFinalPct = Speed * 10 + 5, Hp = 1001,
HpBase = Hp * 10 + 1, MaxHp = 1002,
MaxHpBase = MaxHp * 10 + 1,
MaxHpAdd = MaxHp * 10 + 2,
MaxHpPct = MaxHp * 10 + 3,
MaxHpFinalAdd = MaxHp * 10 + 4,
MaxHpFinalPct = MaxHp * 10 + 5,
} public class NumericComponent: Component
{
public readonly Dictionary<int, int> NumericDic = new Dictionary<int, int>(); public void Awake()
{
// 这里初始化base值
} public float GetAsFloat(NumericType numericType)
{
return (float)GetByKey((int)numericType) / 10000;
} public int GetAsInt(NumericType numericType)
{
return GetByKey((int)numericType);
} public void Set(NumericType nt, float value)
{
this[nt] = (int) (value * 10000);
} public void Set(NumericType nt, int value)
{
this[nt] = value;
} public int this[NumericType numericType]
{
get
{
return this.GetByKey((int) numericType);
}
set
{
int v = this.GetByKey((int) numericType);
if (v == value)
{
return;
} NumericDic[(int)numericType] = value; Update(numericType);
}
} private int GetByKey(int key)
{
int value = 0;
this.NumericDic.TryGetValue(key, out value);
return value;
} public void Update(NumericType numericType)
{
if (numericType > NumericType.Max)
{
return;
}
int final = (int) numericType / 10;
int bas = final * 10 + 1;
int add = final * 10 + 2;
int pct = final * 10 + 3;
int finalAdd = final * 10 + 4;
int finalPct = final * 10 + 5; // 一个数值可能会多种情况影响,比如速度,加个buff可能增加速度绝对值100,也有些buff增加10%速度,所以一个值可以由5个值进行控制其最终结果
// final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;
this.NumericDic[final] = ((this.GetByKey(bas) + this.GetByKey(add)) * (100 + this.GetByKey(pct)) / 100 + this.GetByKey(finalAdd)) * (100 + this.GetByKey(finalPct)) / 100;
Game.EventSystem.Run(EventIdType.NumbericChange, this.Entity.Id, numericType, final);
}
}
}

1.数值都用key value来保存,key是数值的类型,由NumericType来定义,value都是整数,float型也可以转成整数,例如乘以1000;key value保存属性会变得非常灵活,例如法师没有能量属性,那么初始化法师对象不加能量的key value就好了。盗贼没有法力值,没有法术伤害等等,初始化就不用加这些。

2.魔兽世界中,一个数值由5个值来影响,可以统一使用一条公式:

final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;

比如说速度值speed,有个初始值speedbase,有个buff1增加10点绝对速度,那么buff1创建的时候会给speedadd加10,buff1删除的时候给speedadd减10,buff2增加20%的速度,那么buff2创建的时候给speedpct加20,buff2删除的时候给speedpct减20.甚至可能有buff3,会在最终值上再加100%,那么buff3将影响speedfinalpct。这5个值发生改变,统一使用Update函数就可以重新计算对应的属性了。buff配置中对应数值字段相当简单,buff配置中填上相应的NumericType,程序很轻松就能操作对应的数值。

3.属性的改变可以统一抛出事件给其它模块订阅,写一个属性变化监视器变得非常简单。例如成就模块需要开发一个成就生命值超过1000,会获得长寿大师的成就。那么开发成就模块的人将订阅HP的变化:

    /// 监视hp数值变化
[NumericWatcher(NumericType.Hp)]
public class NumericWatcher_Hp : INumericWatcher
{
public void Run(long id, int value)
{
if (value > 1000)
{
//获得成就长寿大师成就
}
}
}

同理,记录一次金币变化大于10000的异常日志等等都可以这样做。

有了这个数值组件,一个moba技能系统可以说已经完成了一半。

ET开源地址地址:egametang/ET: Unity3D Client And C# Server Framework (github.com)   qq群:474643097

ET介绍——数值组件设计的更多相关文章

  1. HTML5拓扑图形组件设计之道(一)

    HT for Web(http://www.hightopo.com/guide/readme.html)提供了涵盖通用组件.2D拓扑图形组件以及3D引擎的一站式解决方案,正如Hightopo官网所表 ...

  2. HT图形组件设计之道(一)

    HT for Web简称HT提供了涵盖通用组件.2D拓扑图形组件以及3D引擎的一站式解决方案,正如Hightopo官网所表达的我们希望提供:Everything you need to create ...

  3. 【转载】COM 组件设计与应用(六)——用 ATL 写第一个组件

    原文:http://vckbase.com/index.php/wv/1216.html 一.前言 1.与 <COM 组件设计与应用(五)>的内容基本一致.但本回讲解的是在 vc.net ...

  4. 【转载】COM 组件设计与应用(五)——用 ATL 写第一个组件

    原文:http://vckbase.com/index.php/wv/1215.html 一.前言 1.如果你在使用 vc5.0 及以前的版本,请你升级为 vc6.0 或 vc.net 2003: 2 ...

  5. 【转载】COM 组件设计与应用(三)——数据类型

    原文:http://vckbase.com/index.php/wv/1206.html COM 组件设计与应用 系列文章:http://vckbase.com/index.php/piwz?& ...

  6. 【Web技术】314- 前端组件设计原则

    点击上方"前端自习课"关注,学习起来~ 译者:@没有好名字了译文:https://github.com/lightningminers/article/issues/36,http ...

  7. xmlplus 组件设计系列之八 - 分隔框(DividedBox)

    分隔框(DividedBox)是一种布局类组件,可以分为两类,其中一类叫水平分隔框(HDividedBox),另一类叫垂直分隔框(VDividedBox).水平分隔框会将其子级分为两列,而垂直分隔框则 ...

  8. 【转载】COM 组件设计与应用(十七)——持续性

    原文:http://vckbase.com/index.php/wv/1264.html 一.前言 我们写程序,经常需要实现这样的需求: 例一.程序运行产生一个窗口,用户关闭的时候需要记录窗口的位置, ...

  9. 【转载】COM 组件设计与应用(十一)—— IDispatch 及双接口的调用

    原文:http://vckbase.com/index.php/wv/1236.html 一.前言 前段时间,由于工作比较忙,没有能及时地写作.其间收到了很多网友的来信询问和鼓励,在此一并表示感谢.咳 ...

  10. 【转载】COM 组件设计与应用(十)——IDispatch 接口 for VC.NET

    原文:http://vckbase.com/index.php/wv/1225.html 一.前言 终于写到了第十回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常用 ...

随机推荐

  1. std::string std::wstring char w_char 内部中文编码

    最近在处理一个字符串转码问题,故记录一下过程 该需求是外部 sdk 的一个 api 需要一个 char* 字符串路径入参,我以往是将宽字符串转为 UTF8 后再传给 sdk 这次这个 api 似乎不接 ...

  2. RabbitMQ零碎整理,总有一天在你工作中会用到

    概念 MQ:消息队列(消息中间件),开源的基于AMQP协议的消息中间件,异步 解耦 削峰 核心作用:1.异步 2.解耦 3.消息通信 RabbitMQ 消息的类型:1.点对点 2.Worker 3.发 ...

  3. 【Azure 存储服务】使用POST方式向Azure Storage Queue中插入Message的办法

    问题描述 使用POST HTTP Request, 如何向Azure Storage Queue中写入Message呢?例如使用CURL发送POST指令是否可以呢? CURL -H "Con ...

  4. 数据库运维 | 携程分布式图数据库NebulaGraph运维治理实践

    作者简介:Patrick Yu,携程云原生研发专家,关注非关系型分布式数据存储及相关技术. 背景 随着互联网世界产生的数据越来越多,数据之间的联系越来越复杂层次越来越深,人们希望从这些纷乱复杂的数据中 ...

  5. 2.UML类图基本介绍

    1. UML 基本介绍 UML--Unified modeling language UML(统一建模语言),是一种用于软件系统分析和设计的语言工具,它用于帮助软件开发人员进行思考和记录思路的结果 U ...

  6. 使用go语言开发自动化API测试工具

    前言 上一篇文章说到我还开发了一个独立的自动测试工具,可以根据 OpenAPI 的文档来测试,并且在测试完成后输出测试报告,报告内容包括每个接口是否测试通过和响应时间等. 这个工具我使用了 go 语言 ...

  7. Java基础全程复习笔记(值得参考)

    Java基础复习笔记 第01章:Java语言概述 1. Java基础学习的章节划分 第1阶段:Java基本语法 Java语言概述.Java的变量与进制.运算符.流程控制语句(条件判断.循环结构).br ...

  8. vue3 markdown 读取文件的两种方法 有gitee发布地址

    方法一: markdown-loader html-loader import的时候就转换成html了,每次需要build,但是可以本地双击就能看,放哪个目录页不限制 方法二: axios + mar ...

  9. pod的拉取和重启策略

    在Kubernetes中,Pod的拉取策略和重启策略可以通过YAML配置文件来定义. Pod的拉取策略 Pod的拉取策略指的是Kubernetes在创建或重启Pod时,如何获取Pod所需的容器镜像.这 ...

  10. stm32 使用多串口通信调试总结

    前记:   stm32使用多个串口通信,这个项目遇到了不少问题,值得反思和深入总结一下.   提纲:  这次的问题,主要有几个部分组成: A 多串口的DMA配置,这个需要注意,尽量不要使用同一个DMA ...