游戏中的设计模式一

news2025/1/22 16:09:19

游戏开发是一个快速迭代的过程,代码复杂度也很高,借助于设计模式,可以帮助我们降低复杂度,降低系统间的耦合,从而高效高质的做出交付。

最近读了这本书:《游戏编程模式》[1],很受启发,所以结合书本知识以及自己的理解,写一写游戏中常用的设计模式。

模式


单例模式

先说要点:这是唯一一个不推荐使用的模式,因为它害处多于好处。

尽管这个模式出现在了 GoF 的书[2]中,但是它弊大于利,并不是一个好的模式。因为它制造了全局变量,而全局变量是有害的。不推荐全局变量的理由也差不多构成了不推荐单例模式的理由。

CppCoreGuidelines[7] 也提到了避免使用单例模式,并指出 Reason 是 “Singletons are basically complicated global objects in disguise”,翻译过来就是:单例本质上就是一些复杂的全局对象。

全局变量会导致这几件事情变得很困难:可测试性、重构、优化、并发。

除了全局变量的原因,在游戏中,还有一个特别的点:游戏对延迟是很敏感的,而单例支持 “延迟初始化”,这反而可能带来卡顿,对游戏是不利的,所以游戏基本上不需要延迟初始化,反而是在一开始就把一些初始化耗时高的模块都先初始化了。

既然不推荐单例,但有些时候也不得不使用全局变量,怎么办?

没办法,该用全局变量的地方还继续用着吧。但是,尽量通过一些办法减少全局变量的数量。比如游戏客户端中,往往不得不定义一个全局变量 world 来表示整个游戏世界的,这个全局变量会被用得到处是,基于这一基本现实,我们可以把一些其他的需要全局访问的变量也放在这个 world 变量里。


状态机模式

状态机出现很频繁,游戏里面的 AI 大多都是用状态机实现的,计算机网络中的 TCP 协议,其实现也是典型的状态机。

以前我觉得状态机平平无奇,没有什么特别的。直到看到《游戏编程模式》[1]里介绍的例子,才惊觉状态机模式真是神奇,化繁为简,使一切变得很有秩序。

如果不使用状态机,要根据输入控制一个英雄的行为,可能会写出这样复杂的,不好维护的代码:

void Heroine::handleInput(Input input)
{
    if (input == PRESS_B)
    {
        if (! isJumping_ && ! isDucking_)
        {
            // Jump...
        }
    }
    else if (input == PRESS_DOWN)
    {
        if (! isJumping_)
        {
            isDucking_ = true;
            setGraphics(IMAGE_DUCK);
        }
        else
        {
            isJumping_ = false;
            setGraphics(IMAGE_DIVE);
        }
    }
    else if (input == RELEASE_DOWN)
    {
        if (isDucking_)
        {
            // Stand...
        }
    }
}

上面的代码,不单复杂难维护,而且还容易出 bug,比如会有很多这类逻辑约束:“主角在跳跃状态的时候不能再跳,但是在俯冲攻击的时候却可以跳跃”,为了实现这类约束,需要加更多的状态变量,更多的判断。

但是如果引入状态机,一切都将变得简单有序。首先,要先写出一个状态机,之后再把它实现出来。状态机有以下几个特征:

  • 你拥有一组状态,并且可以在这组状态之间进行切换
  • 状态机同一时刻只能处于一种状态
  • 状态机会接收一组输入或者事件
  • 每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态

准确的说,我们这里需要的是 DFA(有限自动机),如果是 NFA,肯定会超过我们脑子负载的。有限状态机(FSM)可以分为 DFA 和 NFA[4]:

FSM is further distinguished by Deterministic Finite Automata (DFA) and Nondeterministic Finite Automata (NFA). In DFA, for each pair of state and input symbol there is only one transition to a next state whereas, in NFA, there may be several possible next states. Often NFA refers to NFA‐epsilon which allows a transition to a next state without consuming any input symbol. That is, the transition function of NFA is usually defined as T: Q x (ΣU{ε}) → P(Q) where P means power set.Theoretically, DFA and NFA are equivalent as there is an algorithm to transform NFA into DFA.

以上英雄行为的例子画出的状态机如下:

在这里插入图片描述

图1:英雄行为状态机

依据状态机,实现的代码如下:

enum State
{
    STATE_STANDING,
    STATE_JUMPING,
    STATE_DUCKING,
    STATE_DIVING
};

void Heroine::handleInput(Input input)
{
    switch (state_)
    {
    case STATE_STANDING:
        if (input == PRESS_B)
        {
            state_ = STATE_JUMPING;
            yVelocity_ = JUMP_VELOCITY;
            setGraphics(IMAGE_JUMP);
        }
        else if (input == PRESS_DOWN)
        {
            state_ = STATE_DUCKING;
            setGraphics(IMAGE_DUCK);
        }
        break;

    case STATE_JUMPING:
        if (input == PRESS_DOWN)
        {
            state_ = STATE_DIVING;
            setGraphics(IMAGE_DIVE);
        }
        break;

    case STATE_DUCKING:
        if (input == RELEASE_DOWN)
        {
            state_ = STATE_STANDING;
            setGraphics(IMAGE_STAND);
        }
        break;
    }
}

看起来仍然是普普通通的代码,但是却让一切井井有条。这里面最重要的是我们明确了英雄的状态,确定英雄只能处于某种确定的状态,这让逻辑变得有序。


黑板模式

想不到这也是一种模式吧,unity 里的行为树,就使用了 blackboard 来记录数据。

它本质上就是一个提供数据共享的 key value store,实现了解耦。但也是有缺点[5],比如:

  • 读写比较随意,容易造成数据损坏,或子系统竞争。
  • 可能会产生非法的数据。
  • 出问题的时候,如果是多个子系统共用,会比较难调试。

游戏开发中,行为树通常结合黑板来实现,黑板实现了行为树的节点间“通信”,就是共享数据而已。

黑板模式在《设计模式: 可复用面向对象软件的基础》[2] 和《游戏编程模式》[1] 都没有介绍,但在《面向模式的软件架构卷1模式系统》[6] 有详细介绍,具体可以看一下。


观察者模式

GOF 对它意图的定义是: “定义对象间的一种一对多的依赖关系,当一个对象的状态发生状态时,所有依赖于它的对象都得到通知并被自动更新”[2]。

在游戏中太常见了,对于解耦有特别大的帮助。比如成就系统,如果不使用观察者模式,那么几乎所有的子系统都要直接调用成就系统,这样一来对于业务的侵入性太强了。

通常的实现是这样的:


// 事件
class Event {
    EventType t;
};

// 观察者基类
class Observer {
public:
    void onNotify(Event e);
};

// 被观察者基类
class Subject {
public:
    void addObserver(Observer* o);
    void removeObserver(Observer* o);
protected:
    void notify(EventType et);
};

观察者模式的基本实现:
1、观察者继承 Observer 类,被观察者继承 Subject 类。
2、Subject 类内部会维护一个观察者列表,在事情发生的时候 notify,会直接遍历观察者列表,调用它们的 onNotify 函数。
3、通常来说,是一种同步的实现,即被观察者是直接调用观察者的函数的。

需要注意的是,观察者模式跟发布订阅模式是有区别的,虽然它们的思路相似,但也有明显的不同:
1、观察者模式中观察者跟被观察者是互相知道彼此存在的;而发布订阅模式中订阅者跟发布者往往是不知道对方存在的,它们通过一个 broker 来通讯。
2、观察者模式往往是一对多的,而发布订阅可以是一对多,也可以是多对多。
3、观察者模式往往是同步调用,而发布订阅是异步调用。

直接看图比较容易知道它们的区别。

观察者模式:

observer-pattern

图2:观察者模式

发布订阅模式:

publish-subscribe-pattern

图3:发布订阅模式[8]

参考

[1] [美]Robert Nystrom. 游戏编程模式[M]. GPP翻译组. 北京: 人民邮电出版社, 2016-09-01: 61, 125.

[2] [美]Erich Gramma, Richard Helm, Ralph Johnson, John Vlissides. 设计模式: 可复用面向对象软件的基础[M]. 李英军, 马晓星, 蔡敏, 刘建中, 等. 北京: 机械工业出版社, 2010(1):194.

[3] kevinan. 暴雪Tim Ford:《守望先锋》架构设计与网络同步. Available at https://www.sohu.com/a/148848770_466876, 2017-6.

[4] N.R. Satish. Finite State Machine. Available at https://patterns.eecs.berkeley.edu/?page_id=470.

[5] KillerAery. 游戏设计模式:黑板模式. Available at https://www.cnblogs.com/KillerAery/p/10054558.html, 2019-01-17.

[6] [德]Frank Buschmann, Regine Meunier, Hans Rohnert, et al. 面向模式的软件架构卷1模式系统. 袁国忠. 北京: 人民邮件出版社, 2013.11: 46.

[7] Bjarne Stroustrup, Herb Sutter. CppCoreGuidelines. Available at https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-singleton.

[8] Microsoft. Publisher-Subscriber pattern. Available at https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1673674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AC/DC电源模块的可靠性设计与测试方法

BOSHIDA AC/DC电源模块的可靠性设计与测试方法 AC/DC电源模块是一种将交流电能转换为直流电能的设备,广泛应用于各种电子设备中,如电脑、手机充电器、显示器等。由于其关系到设备的供电稳定性和安全性,因此可靠性设计和测试是非常重要的。下…

AI绘画Stable Diffusion换脸插件ReActor 不香了,新一代换脸神器 InstantID!

前 言 之前我介绍了 SD 中的一款换脸插件 ReActor,虽然好使,但是安装还是有些许麻烦的。 今天给小伙伴们介绍一款新型的换脸插件:InstantID,主要是使用 ControlNet 和 IP-Adapter 的组合来控制扩散过程中的面部特征。 一句话&a…

「JavaEE」多线程案例分析2:实现定时器

🎇个人主页:Ice_Sugar_7 🎇所属专栏:JavaEE 🎇欢迎点赞收藏加关注哦! 实现定时器 🍉简介🍉模拟实现定时器 🍉简介 定时器类似一个闹钟,时间到了之后就会执行…

【全开源】JAVA国际版多语言语聊大厅语音聊天APP系统源码

JAVA国际版多语言语聊大厅语音聊天APP系统源码——深度解析市场需求,打造全球化语音社交平台 随着全球化的推进和移动互联网的普及,人们对于语音聊天的需求日益增长。尤其是在国际交流日益频繁的今天,一个支持多语言、覆盖全球用户的语音聊天…

分布式搜索-elaticsearch基础 概念

什么是elaticsearch: 倒排索引:就是将要查询的内容分成一个个词条,在将词条文档id存入,词条是唯一的。 文档词条总结: mysql和Elasticsearch概念对比: 架构: 基本概念总结:

互联网盲盒小程序开发,提高商家在市场中的竞争力

随着人们生活水平的提高,对娱乐消费需要也在慢慢增加,潮玩市场也因此得到了快速发展,尤其是盲盒,深受大众的喜爱,现在在各大商场以及各种社交平台上都能看到盲盒的身影,市场影响力非常大! 在当…

FastAPI:Python打造高效API的终极武器

在Python的世界里,如果你想要一个既快速又现代的方式来构建API,那么FastAPI可能是你的首选。这个库基于Starlette(用于Web编程)和Pydantic(用于数据验证),专门为速度和易用性设计。 什么是FastA…

搞懂Docker(九)- 使用Docker Compose

获取示例程序 示例程序 或者 示例程序 获取示例程序程序结构如下├── getting-started-app/ │ ├── package.json │ ├── README.md │ ├── spec/ │ ├── src/ │ └── yarn.lock使用Docker Compose Docker Compose是一个帮助你定义和共享多容器应用程序的工具…

Linux系统编程:进程控制

1.进程创建 1.1 fork函数 fork()通过复制调用进程来创建一个新进程。新进程称为子进程,是调用进程的精确副本 进程,但以下几点除外: 子进程有自己的PID,此PID与任何现有进程组的ID不匹配子进程的父进程ID…

qt: undefined reference to `vtable for aaa‘

版本qt4.8.6&#xff0c;编译报错“main.cpp:(.text0x3b): undefined reference to vtable for aaa” 就一个main.cpp #include <QApplication> #include <QTimer> #include <QCursor> #include <QMouseEvent> #include <QDesktopWidget> #inc…

代驾+顺风车+货运app功能介绍

代驾货运顺风车同城拼车打车网约车系统源码app小程序是一个功能丰富的平台&#xff0c;支持二次开发和定制&#xff0c;以满足不同运营商的需求。以下是关于该系统的功能详情介绍&#xff1a; 一、核心功能 会员管理&#xff1a;包括用户注册、登录、个人信息管理等功能&…

yarn 安装以及报错处理

前一种报错是由于没有安装yarn导致的&#xff0c;使用以下命令即可安装&#xff1a; npm install -g yarn 如果成功安装&#xff0c;将显示Yarn的版本号。 yarn --version 第二种报错是因为系统上的执行策略限制导致的。执行策略是一种安全功能&#xff0c;用于控制在计算机…

第15节 编写shellcode加载器

我最近在做一个关于shellcode入门和开发的专题课&#x1f469;&#x1f3fb;‍&#x1f4bb;&#xff0c;主要面向对网络安全技术感兴趣的小伙伴。这是视频版内容对应的文字版材料&#xff0c;内容里面的每一个环境我都亲自测试实操过的记录&#xff0c;有需要的小伙伴可以参考…

HCIP的学习(17)

BGP基础配置 使用直连接口IP地址来建立EBGP对等体关系 1、启动BGP协议 [r1]bgp 100 ----启动BGP协议&#xff0c;并且规定其AS号2、配置设备的RID数值&#xff0c;一般选择设备的loopback接口的IP地址 [r1-bgp]router-id 1.1.1.13、配置BGP对等体信息&#xff0c;包含了对等体…

【声呐仿真】学习记录3-待续

【声呐仿真】学习记录3-后续 第五阶段-获取数据1.运行赫尔库勒斯沉船的世界&#xff1a;2.键盘操纵rov至合适的位置&#xff0c;调整Image topic&#xff0c;查看输出图像3.RVIZ SONAR 图像查看器插件&#xff08;没有对应的topic&#xff09;4.点云5.录制rosbag 第六阶段-查看…

问题—前端调用接口url多加一个/,本地可以调通,测试环境报错302,分开调两个接口

问题背景 接口url前面多加一个/ &#xff0c;npm run serve 起项目&#xff0c;本地调用正常 npm run build 打包到测试环境&#xff0c;接口出现问题&#xff0c;分开调用接口&#xff0c;且报302错误 问题原因&#xff1a; 本地开发环境和测试环境的URL处理方式不同 本地使…

专业的服贸会服务团队-媒体邀约宣传

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 2024服贸会开展在即&#xff0c;许多企业都做好了的参展的准备&#xff0c;北京麦塔文化提供专业的展览展会服务&#xff0c;下面做个简单介绍。 、会场搭建团队&#xff1a; 负责整个活…

Ansys Zemax|HUD 设计实例

说明 本文介绍了HUD设计实例。 实例说明 规格如下&#xff1a; 显示器尺寸&#xff1a;24*8mm 眼盒尺寸&#xff1a;100*40mm 放大倍率&#xff1a;5 &#xff08;虚像尺寸 120*40mm&#xff09; 虚像距离&#xff1a;1.8m 最终光学系统的整体布局如下图所示。 从HUD发出的…

478.8-480W 宽电压输入 AC/DC 导轨式开关电源——TPR/SDR-480-XS 系列

TPR/SDR-480-XS 系列导轨式开关电源&#xff0c;额定输出功率为478.8-480W&#xff0c;产品输入范围&#xff1a;85-264VAC。提供24V、36V、48V输出&#xff0c;具有短路保护&#xff0c;过载保护等功能&#xff0c;并具备高效率&#xff0c;高可靠性、高寿命、更安全、更稳定等…

GRFB-UNet:一种新的多尺度注意力网络,用于铺路分割

不同场景下的带注释的触觉铺装示例: GRFB-UNet网络结构: GRFB模块的结构: 铺路在视障人士的旅行中起着至关重要的作用。因此,识别铺装的形状和位置以支持视障人士的移动性是相当有意义的,而视觉分割技术就适合这项任务。为了有效提高触觉铺装分割的精度和鲁棒性,…