Unity沙盒建造游戏设计(3) - 状态模式

1,433 阅读6分钟

前言

在近期的公司项目中,需求项目开发一个类沙盒建造功能。我有幸负责项目中的关键部分,这一经历充满了挑战与收获。鉴于这个部分内容的丰富性和技术性,我决定将涉及相关设计以及本人的一部分思绪分成四期内容。本文是系列文章的第三期,将重点讨论建造中的状态模式设计。

运用状态模式主要目的还是为了解耦, 这种大型的系统设计如果还让if-else影响自己的代码判断能力, 那就太得不偿失了. 本篇将从每种状态设计开始, 再到整体的系统设计, 最后还会附上状态模式中一些笔者踩到的坑, 该篇同样为主要讲解篇章之一.

状态模式构成

状态模式(State Pattern)也是行为设计模式的一种, 他的出现通常都跟有限状态机强相关. 通常的需求中状态出现的其实也是非常频繁, 很多地方也能用到该模式. 用该模式会带来一下几个好处:

  • 单一职责原则: 将与特定状态相关的代码放在单独的类中。
  • 开闭原则: 无需修改已有状态类和上下文就能引入新状态。
  • 通过消除臃肿的状态机条件语句简化上下文代码。

但一定不要滥用, 比如说两个或者三个比较简单的状态下, 一个if-else就可以实现的效果就不要用该模式了, 毕竟是行为设计模式, 它同在前一章讨论的命令模式也会有一样的缺点, 解耦是肯定解耦了, 但是冗杂代码也一定会出现的.

该模式主要构成为三个部分: 上下文(Context)状态(State)具体状态(Concrete States)

他的命名就没有命令模式那么抽象, 基本能做到见文生意, 下面从概念到具体一一介绍

状态基类

同大部分GOF设计模式, State最基本的一些方法为InitEnterExitType

由于笔者这次需求的具体State的种类还是较多的, 达到了6种, 所以本次基类设计中也是加入了CanSwitch方法

同命令模式所诉内容, 这个内容可以有多种表现形式, 基类、接口、协议.

具体状态

这个部分也是对业务的抽象处理, 部分状态操作功能类似, 但内容完全不一样的方法没有放到基类里去实现, 两个方面, 一个是添加后会大大增加基类的复杂度, 另一个是现在用枚举、范型、类型转换能够很轻易的拿到方法内容现代语言的功能已足够强大.

笔者这里简单分享一下6个状态互相转换关系

当前状态可切换状态
12、3、4、5、6
23、4
31
41
51、3
61

虽然在这里表格呈现形式不太好, 但是用有限状态图以六边形画出来的画效果其实相当好看. 这里观察可见如3、4、6状态相对独立, 那么他们的EnterExit可以完全控制对应逻辑管理, 在编写他们详细的业务需求时会相当清晰.

上下文

最后来聊聊上下文内容, 上下文这个名字虽然它表明了意图, 其实实际上不那么清晰, 通常写代码中更喜欢称呼他们叫做ManagerSystem, 他们通常负责所有状态的初始化, 切换, 公共方法的调用入口. 并且实际上状态模式的大部分坑也是这个在上下文这个地方.

初始化

状态的生命周期基本上都是跟上下文同步的, 上下文创建时, 所有的具体状态应该会同时初始化, 并且在初始化这里笔者也是选择了将上下文传入具体命令中

为什么初始化传入上下文

状态切换

这部分比较简单, 有当前命令执行Exit, currentState更新, 并执行Enter方法.

笔者这个需求能还多了一个对当前命令能否转为目标命令的检查.

除此之外热烈的期望将DebugLog打到这个地方, 受益匪浅

坑啊坑

这部分内容其实应该算是较为脱离了一些状态模式最基本的范畴, 但是在设计大型项目中又不可忽视的问题.

UI交互设计

不同于命令模式, 状态模式大概率会和UI相关内容打交道. 所以如何优雅的设计与UI组件的交互内容是一个值得思考的问题.

在比较简单的交互下可以将当前的state传入UI组件并将其直接调用, 但是复杂的需要另寻他法.

笔者这里的选择是将所有可能UI的交互事件都在上下文中声明, 声明的方法就是调用当前的state的这个方法, 那么这样的情况下UI就只剩下与上下文交互的内容, 这样虽然相对简易但是依然会存在两个问题:

  1. 上下文变得fat, 一大堆差不多的变量声明, 陷入了类似MVC中的后期controller的困境中, 如果该语言支持分文件夹声明那么将会得到缓解, 但也避免不了变得fat的事实
  2. 逻辑条理不够清晰, 有一些接近黑盒的感觉, 有些状态实现有些状态不实现, 一旦出现了问题较难debug.

笔者也曾想过将其写入基类中,但最后也是放弃了(在具体状态那里简单说了下想法). 所以如果读者在这方面如果有更好的想法可以留言或者联系我沟通.

状态切换与监听周期

在调用链路没有那么清晰的大型需求中, 处理事件是非常正常的事情. 说到事件就需要监听与移除, 这个调用时机呼之欲出 -- EnterExit. 为了更好的解耦, 笔者也是毫不犹豫的做了这个事情, 但是在项目里却有问题...

有可能在执行事件的流程中发生了添加或移除监听的操作, 那么监听事件队列for循环迭代器就为nil了就执行不下去了....

显然这个是项目中事件队列写的有些问题, 但是没办法项目缺陷, 业务也需要买单.

所以我就讲所有有关监听的内容放到了上下文之中.

让本就雪上加霜的第一条再添沉重一笔.

尾语

同命令模式, 这篇文章主要讲编程思想, 在此过程中也聊一些大型需求中的设计经验, 本篇也应该是该系列中内容最丰富的一集了, 下一篇是本系列的最后一章, 聊一聊寻找系统.