我理解的游戏数据模型

本文探讨游戏数据的组织和管理,包括系统、个人和服务器数据的划分,客户端数据模型的生命周期,数据更新原则,模块间的依赖以及数据变化如何通知显示更新。重点关注数据对象的构造、初始化、更新和清理时机,以及中间变量和缓存优化策略。
摘要由CSDN通过智能技术生成

(本文由近年随记整理而成)

数据对于游戏意义非凡。
游戏从实质上,可以看作是数据的集合。
玩游戏,就是在与服务器交换数据,最后改变这些数据存档。

一、数据划分

1、按所属划分

    系统数据:系统本身的配置数据(如主线有哪些章、地图是怎样的、有什么NPC等)
    个人数据:包括,账号数据(账号ID,token等)、角色基本数据(如,昵称、头像、角色等级经验等)、角色在各系统下产生的数据(如,拥有的道具资源、签到时间、Boss挑战次数,好友列表等)
    服务器数据:由服务器生成和维护的数据(如,排行榜列表、公会列表等)、各系统由服务器托管的数据(活动开关时间等)。

2、按存储划分

    服务器存储数据:重要存档。
    本地持久化数据:记录在本地硬盘的数据,不保证安全。对于网游,常记录一些非重要存档(如红点今日是否展示过等)
    临时数据(对象):某时刻临时生成,用完即释放的数据(如,奖励字符串转成的临时奖励对象、英雄升级预览时的临时英雄对象)

3、按动静分

    静态数据:又可分为 直接静态数据(策划配置)和 间接静态数据(基于静态数据二次处理的数据 或 基于动态数据后查找的数据(如当前等级的配置))
    动态数据:又可分为 直接动态数据(服务器存储,发给客户端的字段)和 间接动态数据(经过二次计算的,如推算的等级(只发经验时)、推算的战力(根据攻防血等计算))

4、按功能模块划分

    各模块按数据层面的功能归纳,需要注意 父子模块、紧密相关的模块(如,公会和公会签到、英雄和佣兵、英雄和共鸣水晶(《剑与远征》中)等)。
    还需注意 “数据概念的模块” 和 “显示概念的模块” 的区别,有时一个界面可能会杂糅引用多个数据模块(如在同一个背包中存放道具、装备、碎片、宠物等)。

二、客户端数据模型的生命周期

0_0、数据模型目录结构

    按数据层的功能模块创建 XXModel(单例),自顶向下组合/聚合下层实体类,形成该功能模块的数据模型。

    --XX(Dir)
    ---XXModel
    ---entitys(Dir)(实体对象)

0_1、构造函数和初始化方法的特点和区别

    ⑴、参考享元模式,构造负责确定“内部状态”(静态的、配死不变的);初始化负责设置最初的“外部状态”(动态的、可变的)。
    ⑵、构造函数只调用一次;初始化方法可重复调用(重新初始化);另外,局部刷新方法也可重复调用。

--------------------------------------------------- NRatel割 ---------------------------------------------------

1、数据对象构造和初始化时机:

    ⑴、游戏启动时,在XXModel单例的构造函数中构造,无需初始化或等待后续服务器消息为其补充初始化和删减多余构造。
        (适用:无uid,用本地策划表即可完成构造,多属于系统数据对象)。
        (构造时可能得先为所有动态属性赋予默认值)(因为服务器记的数据可能是增量的,如商店,服务器只记玩家买了哪几个档位,没买的档位无需初始化,默认是0)。
        (下述⑵⑶⑷⑸均可作为补充和删减)。
    ⑵、账号或角色登录时,用 “服务器返回的模块初始化消息(所有应在登录时就初始化的模块)” 构造并初始化。
        (适用①:无uid,但需要初始化消息才能进行构造(可能和当前角色或服务器状态有关)(如,商店共有10档商品,而受玩家目前的进度或服务器日期限制,只展示其中5档))。
        (适用②:有uid,多属于玩家或服务器后期生成的数据对象(如重复获得的英雄卡牌/装备;好友、收到的邮件等))。
    ⑶、功能解锁时,用 “服务器返回的模块初始化消息” 构造并初始化。
    ⑷、活动时间开启时,用 “服务器返回的活动初始化消息” 构造并初始化。
    ⑸、点击某入口进入该功能时,向服务器请求模块初始化数据构造并初始化(懒构造初始化)。

--------------------------------------------------- NRatel割 ---------------------------------------------------

注意点:

    ⑴、除懒构造初始化外,务必保证客户端从服务器较及时的拿到数据,若在这个拿数据的中间态判断功能是否开放,应认为是未开放。
    ⑵、功能开放要做数据层和显示层的区分(比如背包,数据层从创角时就开放,但显示上可能要通过X关后才展示入口或移除入口的锁标志)(考虑数据层是否可以完全忽略功能开放?)。
    ⑶、一个功能的解锁必定(目前没发现特例)是由某个玩家操作请求导致的。
    ⑷、由时间变化引起活动开放时,应由客户端主动请求(略延时)该活动的数据(短连时)或 服务器推(长连时)。
    ⑸、一个功能模块,无论是否产生用户/服务器数据,只要它开放了,服务器就应该下发初始化消息(可无内容),主要为了告诉客户端该数据模块已经初始化。
    ⑹、服务器应保证更新消息发送之前已发过初始化消息。

2、数据对象重新初始化时机:

    ⑴、切换账号或角色(重登)时。
    ⑵、任意时刻重新收到“服务器返回的模块初始化消息”时。

    ⑶、跨天时(自然天和游戏自定刷新天(如每日5点)),请求服务器进行重置(仅当数据含与跨天有关的数据内容时,如 “每日挑战次数”)。

3、数据对象更新时机:

    ⑴、玩家操作时。
    ⑵、其他玩家操作时(可能影响好友、公会、共斗副本等)。
    ⑶、系统自操作时(如,系统AI调整当前规则等)。

4、数据对象清理时机:

    ⑴、离开功能界面时(一些进入界面时临时创建或获取的数据)。
    ⑵、活动到期时。
    ⑶、退出登录时。


三、数据更新原则

0、前提

    ⑴、一个操作可能使多个数据模块发生变化。
    ⑵、一个操作可能使数据模块整体重新初始化 或 数据模块内部局部发生增/删/改。

--------------------------------------------------- NRatel割 ---------------------------------------------------

1、服务器仅确认客户端操作 还是 为客户端返回数据?

    有些操作,客户端在请求时本身知道将要修改什么数据,可以由客户端自行修改(比如,挑战副本后剩余次数=原次数-1)。
    但有些操作,客户端在请求时不知道将要发生什么,必须由服务器返回(比如,打开一个随机宝箱,不知道可能开出什么)。
    建议均由服务器返回数据,一方面为了统一,另一方面前者更易出错且不太安全。

2、覆盖更新还是差异更新(服务器为客户端通知最新值还是差异量)?

    对于一个原子性单元(值类型或一个较小较独立的对象),大部分时候应该覆盖更新。
    对于一个杂糅对象(较大、较分散数据的打包)需要定义针对性修改某个值的协议(避免修改时对其他内容产生影响)(尤其是部分数据属于个人,部分数据属于服务器时)。
    对于一个集合,尤其是较大集合,应该差异更新(增/删/改其中的某些项)(需要注意,这不是幂等的)。
    对于一个深层嵌套的集合,应该每层都可更新。

3、数据更新协议制定

    对于数据模块整体重新初始化,可直接使用初始化消息,命名为 {#ModuleName}_Info。
    对于数据模块内部集合中对象的增/删/改,可制定每层对应的变化消息,命名为 {#ModuleName_#Layer1_Layer2...}_Change。
    (游戏中所有增/删/改的枚举值可统一为1,2,3)
    (同一层的多个对象变化消息,服务器应该合并,否则可能导致客户端多次计算和刷新)
    一个请求将带回一堆数据变化的推送消息和一个返回


四、数据模块间依赖

各数据模块是否按手动配置的顺序进行初始化? 

实例如:
    道具模块逻辑依赖了角色模块(是否新获得保存时需要按角色uid);
    英雄模块逻辑依赖了道具模块(是否可升阶取决于升阶道具数量是否足够);
    功能开放模块依赖了主线、英雄等模块(是否解锁取决于主线关卡进度、英雄星级等);
    抽卡功能依赖了功能解锁模块(通关X关后解锁抽卡);
    功能解锁模块又反过来依赖了抽卡功能(基础抽卡达到N次作为一个解锁条件);
    限时签到活动依赖了活动开放模块;
    红点数据树依赖了各个对应模块;
    ......

可以看到,各模块之间的依赖关系是非常杂乱的,像一个网,根本不能理清谁先谁后。
但,还是可以根据其特征,将其分为两大类:

    ⑴、基础模块(可由“服务器初始化消息”直接初始化的模块);
    ⑵、上层模块(可能获取或缓存下层模块的数据结果 亦可能 对下层模块的数据进行加工处理并缓存结果))。

分类举例:
    ⑴、基础模块:角色模块、道具模块、主线模块、英雄模块、抽卡模块...
    ⑵、上层模块:功能解锁模块、红点数据树...

--------------------------------------------------- NRatel割 ---------------------------------------------------

要求!:
    ⑴、模块间应互相独立,初始化时完全不分先后顺序。
    ⑵、各基础模块在初始化时,不要调取其它模块的数据(不能保证其他模块已经初始化完成)。 
    ⑶、各基础模块在初始化时,不要直接缓存自身模块内的计算结果为变量(具体处理参考下节:中间变量和缓存优化)。
    ⑷、上层模块在初始化时,不要缓存任何其他模块的结果,只需要建立空壳(具体处理参考下节:中间变量和缓存优化)。
    ⑸、上层模块的变化由基础模块的变化通知。


五、中间变量和缓存优化

1、定义

中间变量(自定名字),含义:不由服务器直接存储维护,而是由客户端根据基础数据进行二次推导计算和缓存的变量。
实例如:
    英雄等级由英雄经验从1级开始逐级扣除计算得到。
    英雄战斗力由英雄八维属性、所穿装备、携带宠物、全局称号等计算得到。
    共鸣水晶的等级取战力前五英雄的最低等级(等级相同时按品质、uid降序)(《剑与远征》中)。
    项目后期发现启动/登入游戏慢,原因是初始化中做了太多事,想把部分变量改为懒初始,但引用处太多难以重构。
    ...

2、三种方案的维护成本对比:

    ⑴、不维护中间变量(不缓存结果),只定义一个Get方法,获取时现算。 
        读成本:高; 写成本:低; 维护成本:低
        读成本取决于获取频次,若获取频次高,则计算次数多,可能有性能问题;若获取频次低,如仅1次时,则读成本也不高。

    ⑵、维护中间变量(缓存结果),每次引起中间变量变化时重新计算,获取时返回缓存值。
        读成本:低; 写成本:高; 维护成本:适中
        写成本取决于引起中间变量变化的频次,若变化频次高,则计算次数多,可能有性能问题;若变化频次低,如仅1次时,则写成本也不高。

    ⑶、维护中间变量(缓存结果)和脏标记,变化时标记为脏,获取时若为脏则重新计算,否则直接返回缓存值。
        读成本:适中; 写成本:接近0(只是在写脏标记); 维护成本:较高
        在变化频率和读取频率都高或不确定时适用。最智能,将自动决定重新计算的次数。

3、注意点和选择考量

    ⑴、总是应该先定义一个Get方法,在方法内部再去决定是否缓存结果(利于后期调整策略)。
    ⑵、主要考量读写频率(推荐)。
    ⑶、也可考量计算复杂度(不推荐)。若本身计算复杂度很低,用方案⑴即可,即使频繁计算也问题不大。
    

六、数据变化应该如何通知显示更新?

1、首先要明确:

    ⑴、一切数据变化的源头都来自操作。可能是玩家操作、系统操作(AI 或 时间变化等)或 服务器操作。
    ⑵、对于刷新UI界面来说,大部分都是瞬时完成的,没有异步过程。当有异步动画时,重要的可锁屏等待,不重要的可直接打断,这里不细讨论。
    ⑶、一处显示可能同时引用多个数据模块的数据。
    ⑷、多处显示也可能同时引用同一数据模块的数据。

2、界面刷新可通过两种基础方式(如图中步骤 5_1 和 5_2):

 

    ⑴、(步骤 5_1) 修改数据后,由操作直接调用界面提供的刷新方法(界面存在时)。
    ⑵、(步骤 5_2) 修改数据后,由派发数据改变的事件,界面上注册的此事件被触发,调用其刷新方法(界面打开时注册事件,界面关闭时移除)。

    优缺点对比:

    ⑴、基于操作的刷新,需要判断当前存在哪些需要刷新的界面(通常较难判断且耦合性强(不利于修改)),但能保证时序(各数据模块都更新完成后才执行更新显示)。
    ⑵、基于数据的刷新,可以解耦,但无法保证时序、丢了操作时的环境、还可能有刷新效率问题(意外的界面刷新 或 相同位置多次刷新,见下方注意⑴)。

    设想有没有更理想的方式?:
    一个操作,等所有数据模块修改完成后统一触发一次显示刷新,再统一触发一次红点刷新。
    todo...

    初始化 -> Model1, Model2, ...
          -> View1, View2, ...
          -> Red1, Red2, ...

    操作 -> TheModel1, TheModel2, ...
         -> TheView
         -> Red1, Red2, ...

3、注意:

    ⑴、刷新界面事件的粒度会影响界面刷新效率。
        若太大,可能带来意外的界面刷新(数据A、B属于同一对象,本想刷A处显示,B处也被刷新了);
        若太小,相同位置可能刷新多次(对象X、Y被同一处显示引用,当X、Y变化时此处显示将被刷新两次)。
    ⑵、刷新界面事件应基于协议自动生成,避免手动定义,若有性能瓶颈,考虑可针对性手动处理一部分。
    ⑶、界面的刷新接口应有粗有细,应该尽量调用较小的刷新方法,而不是偷懒全部刷新。
    ⑷、有时,操作可能不带来任何数据变化,如只是打开/关闭某界面;
    ⑸、有时,操作可能只改变界面内临时数据,不影响存档中的数据模型;


七、其他注意点:

1、类的对象直接用协议定义的还是重新定义?

    重新定义。
    协议数据只可读,就像静态表也一样。但类的对象是可修改的。
    若直接使用,可能引起错误。

2、数据类/显示类对外提供接口的原则?

    接口应该细碎,在使用处组合,而不是直接提供组合接口。
    因为组合的可能性太多,会导致模块庞杂。
    若提供组合接口,仅从方法命名上看,就可能出现很多近义的方法名,会让使用者难以辨认和挑选。
    如有必要,可以提供一个 XXXHelp 类,用来提供各种组合方法。

    大对象接口函数的形参列表应尽量短小,避免各种组合重载。
    可以直接以“可配置对象”为参数进行传递(微信小程序大量接口均如此设计),扩展起来会特别方便,但它有内存代价。

    大对象包含较多显示信息时(有必选、有可选)
    不要将可选参数放在构造或刷新接口,而是提供独立的、对称的显示方法。
    (注意,构造或刷新时需要以默认方式调用这些可选方法)
    如 英雄小卡牌,有必选信息:背景、框、头像; 有可选信息:品质、种族、等级、星级。
    提供可选接口 showQuality(bool)、showCamp(bool)、showLevel(bool)、showStar(bool);
---------------------------------------

3、pb 消息是否应该包含可选值?

    不应该。
    因为接受者分不清发过来的是“数值上等于默认值的真实值”还是不愿赋值的可选值。 
    解决:要么将这个接口的数据完整更新覆盖,要么拆分成小接口。

4、数据结构的选择?

    集合数据应考虑增删查改的频率,若不确定,可先用字典。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值