离散式道具系统设计

通常情况下在设计游戏的道具系统时都会遇到一个问题,如何分类才能让道具数据处理更加灵活。很多时候一些糟糕的设计会导致新增和修改道具种类相当繁琐,稍有不慎就会遗漏一些逻辑处理。如果你的项目中每新增一个道具类型,家需要修改至少3 处代码,那么恭喜你,你成功接手了一个糟糕的道具系统。

很多人认为所谓道具系统就是玩家打开的那个背包,或者展示在英雄面板上的装备面板。实际上我们应该考虑更深层次的内容即数据与逻辑如何组织。这是纯架构层面的。仔细回想一下你所接触过的所有相关业务逻辑,如果现在游戏系统中对于道具的定义是有分类,可以使用,那么这里面会隐含哪些深的业务逻辑或者模型?

在没有任何项目玩法参考的情况下,我们仅凭借逻辑关系来梳理要关注的几个底层核心点。

  1. 每个道具都要有一个分类
  2. 每个道具要有一个自己的 id,用于索引对应的配置
  3. 每个道具都有“使用”这个行为,但因为类型不同,使用的方法与效果也不同
  4. 因为使用,每个道具在实际运行时需要一个唯一的 UUID
  5. 需要玩家在游戏中能够“看到”这个道具,所以道具都会有一些通用的“表现层”的属性
  6. 不同类型的道具,获取方式不同,所以存储的位置可能不一样,对于道具数据的操作逻辑也不同

根据以上几点,详细拆解一下

道具分类,有消耗型的道具,如各种碎片,药剂,执行任务所提交给 npc 的各种道具。装备也是道具,有些积分也是道具。这些数据所对应的业务模块也不同。最显而易见的是装备一定是装备模块来负责管理维护。这些数据不可能存放在一个大的道具系统中,因为不同类型的道具对于单一系统来说存储和开放其业务相关接口是一个庞杂而复杂的工程。例如,在道具系统中,添加一个“将装备添加到英雄身上”的接口显然是不合适的。这种特有业务接口放在对应模块里是最为合适的。

道具 id,顾名思义,通过这个 id可以检索到道具的配置。

使用行为,就像刚才说的,如果是一个消耗类型的道具,“使用”这个接口没有任何问题,但放到装备身上就显得相当诡异。后面我们会细说。

唯一 uuid,无论是单机游戏还是网络游戏,这都是必要的,否则系统无法判定当前玩家操作的到底是哪一个道具。

表现层,正如你所想,第一联想到的应该是道具的 Icon,如果道具能“看到”,那么就会有“看不到”的道具。例如一些游戏中,经常将一些“积分”用道具来实现。这会带来一些方便之处。这些积分在游戏画面中并不会看到,而仅仅是显示数量。以此类推,此类的属性就会很多,通过配置可以切换他们的表现层行为。

操作逻辑与存储位置,我在第一条拆解已经说过,不同的道具类型应该存放到自身模块中,道具系统可以不做任何存储功能。这里的好处不止一点。如果游戏中某一个模块被关闭了,那么对应的数据也不会消耗任何内存。

下面我通过一些伪代码来解释下如何实现“离散式道具系统结构”。

先来定义一个接口,道具系统全局通过这个接口来处理所有的道具

public interface IItem: IItemStyle { public int Id { get; } public long Uuid { get; } }

对于表现层,可以实现另外的接口。

public interface IItemStyle { public string Icon { get; } //根据游戏需求,添加一些其他的通用属性 }

现在有了最基础的数据接口,我们通过两个道具数据的包装来解释下数据如何封装。

public class Goods: IItem { //实现IItem的接口 //一些Goods数据的自身特殊属性与逻辑 }

public classs Equipment: IItem { //实现IItem的接口 //一些装备数据自身特殊的属性与逻辑 }

有了两个完全不同的数据,现在来创建对应的业务模块。

public class GoodsModule { public bool Use(long uuid) { //道具的使用逻辑 } }

public class EquipmentModule { public bool EquipItemToHero(long equipUuid, long heroUuid){ { //将道具穿在英雄身上的逻辑 } }

对应的业务模块也有了,现在来最重要的道具系统,道具系统负责维护所有道具的关系链,以及获取查找接口,接下来先接口相关接口,然后实现对应的系统。

public interface IItemFinder { public IItem Find(long uuid); public IItem FindByType(int type); }

当然,你也可以根据业务需求扩展次接口。

public class ItemSystem { public void RegisterFinder(int type, IItemFinder finder) { //将注册的Finder存放到系统中 } }

有了系统,我们看如何对模块进行改造,只需要将对应的模块实现IItemFinder即可。

``` public class GoodsModule: IItemFinder { //接口实现 }

```

这样即可,当系统启动时,我们需要将所有实现IItemFinder的模块注册到ItemSystem即可。例如:System<ItemSystem>().RegisterFinder(5, Mod<GoodsModule>())

在实际业务中,我们可以通过System<ItemSystem>().Find(itemUuid);来获取道具。此时道具均为IItem。如果当前是业务模块内部的一些逻辑比如道具模块,那么数据存储在EquipmentModule,直接通过内部接口查找数据操作即可。无需通过ItemSystem来得到装备数据。

好了,以上就是离散式装备系统的经典结构,希望你永远不会使用switch case这类代码处理不同类型的道具了。