group类的封装使用经验分享

:video_game: 游戏开发中游戏对象分组的一些经验分享

分享一下我自己在开发游戏过程中,对游戏对象分组的一些实践经验。

:jigsaw: 先看需求,再谈封装

首先说明:一切封装都要看情况,看你是否有大量需求。这个分享的内容从我从自己实践中遇到的,对于一些有复杂分组需求的情况才要做封装使用。

如果只是偶尔出现一两个这种需求,你完全可以想怎么写就怎么写,不需要特意搞什么封装。封装是为了解决重复问题,不是为了封装而封装。

:package: 配置分组

分组,我在写类时通常命名为 Group。它不是什么复杂概念,本质上就是对数组的一层封装。

最开始,这种分组操作是用在配置上的。

比如一个手游卡牌游戏,里面有大量卡牌配置,经常会有某个功能需要从中取出部分配置。

例如,抽卡功能有 a% 概率抽出 SR。这时就应该把 SR 专门分一个卡池,而不是每次都遍历全部配置再筛选。

配置分组其实比较简单,因为配置是静态的,不会动态改变。不过即使是这么简单的系统,有时也没人去处理。我见过一些项目里堆了不少“屎山代码”,每次获取配置都是一串手写遍历,实在让人头大。

:flower_playing_cards: 游戏对象分组

更复杂一点的是游戏对象的分组。

例如一个卡牌游戏,如果有一张卡牌,它的效果是让场上所有白卡攻击力 +1。这种时候,你需要筛选获取场上卡牌,再筛选获取白卡。

你可以设计一个数组,存下场上所有白卡。但问题在于:每次场上卡牌登场、离场,你都需要维护和更新这个数组。相关代码写起来会很麻烦,尤其是这类数组多的时候。

这类数组,我的封装方式是这样的:

首先,对象需要具有数据变更时的监听响应,这是我已经封装好的。例如:

card.zone = "field" // 表示卡牌在场上

当卡牌离场,例如进入墓地时,我会直接修改:

card.zone = "grave"

有了这个基础之后,我建立了数据对象 Group 类和管理器。

管理器中默认会创建一个键值为 allGroup,所有数据对象都会自动加入 all。然后开发者可以编写 Group 的子类。

例如 CardGroup

// 示意
CardGroup:
1. 设定源分组为 all:
   source_group = 'all'

2. 设置筛选方法:
   function(dataObject) {
     return dataObject.obj_type == "card"
   }

3. 设置监听依赖属性键值:
   'obj_type'

这样就搞定了“所有卡牌”这个分组。所有加入到 all 的对象,会自动根据 obj_type 判断是否加入 CardGroup

当数据对象的 obj_type 变更时,也会自动检测判断是否要从 CardGroup 中移除。当然,这个例子里的 obj_type 在实际场景中一般不会变,但后面的 zone 在实际场景中就会变。

再写一个 FieldCardGroup,表示场上的卡牌:

// 示意
FieldCardGroup:
1. 设定源分组为 CardGroup:
   source_group = "card_group"

2. 设置筛选方法:
   function(card) {
     return card.zone == "field"
   }

3. 设置监听依赖属性键值:
   'zone'

:link: 组合分组:筛选、合并、交集

实际使用时,我发现光靠筛选不太够用,于是又增加了一些合并、交集类型的自动组合方式。

例如现在有了 FieldCardGroupGraveCardGroup

然后有一些卡牌效果,会对场上和墓地中的卡起效。游戏王中常见这类效果,例如根据场上 + 墓地中龙族怪物的数量增加攻击力。

所以我再写一个 FieldAndGraveCardGroup

// 示意
FieldAndGraveCardGroup:
1. 设置源分组为 FieldCardGroup 和 GraveCardGroup:
   source_groups = ['field_card_group', 'grave_card_group']

2. 设置 group 类型为 OR:
   group_type = OR

   // 上面那种筛选类型为 FILTER

这就行了。这个 Group 会自动将上面两个 Group 中的对象都放进自己体内。

:abacus: 分组统计与响应式接口

这是分组的处理方式。然后还可以为分组加上一些常用接口方法,例如统计某些数据。

实际场景例如:这张卡会根据自己场上 + 墓地中的龙族怪物的等级,每一级提升 3 点攻击力。

这个接口,不仅要做成一个单纯获取数据的接口,还要考虑做成响应式的。这样在写逻辑时,只要设置好这个统计值,再监听统计值的变化即可:

  1. 对“己方场上与墓地中的龙族怪物分组”设置一个统计数值,键值为 level
  2. 使用逻辑中,对这个分组的统计 level 进行监听。

除了统计外,还可以封装:

  • 分组中是否存在某类数据;
  • 某个对象的某项数据是否达到某个阈值;
  • 其他类似的常用查询接口。

:white_check_mark: 小结

以上属于对逻辑比较复杂的游戏中的封装经验。有实际需求的可以看一下,参考一下。封装起来其实没有太大难度,关键还是看需求是否足够多、是否值得抽象。

2赞