游戏中的设计模式一

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

最近读了这本书:《游戏编程模式》[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.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索Playwright:Python下的Web自动化测试革命

在如今这个互联网技术迅速发展的时代,web应用的质量直接关系着企业的声誉和用户的体验。因此,自动化测试成为了保障软件质量的重要手段之一。今天,我将带大家详细了解一款在测试领域大放异彩的神器——Playwright,并通过Python语言…

【搬砖实战】2024年了,还有人不会搭建内网穿透吗?

😊你好,我是小航,一个正在变秃、变强的文艺倾年。 🔔本文讲解动手自己搭建内网穿透,期待与你一同探索、学习、进步,一起卷起来叭! 目录 一、前言内网穿透是什么?frp介绍&#xff1a…

Golang SDK安装

windows环境安装 1.链接: 下载地址 2.安装SDK 检查环境变量: 3.开启go modules,命令行执行一下命令: go env -w GO111MODULEon4.设置国内代理,命令行执行一下命令: go env -w GOPROXYhttps://proxy.golang.com.cn,https:/…

【Linux】进程信号(2万字)

目录 前言 一、生活角度的信号 1.1、我们来见一见信号: 1.2、模拟一下 kill 指令 二、信号的处理 三、产生信号的5种方法 3.1、kill命令 3.2、键盘可以产生信号 3.3、3种系统调用 3.4、软件条件 3.5、异常 四、比较 core 和 Term 五、键盘信号产生 六…

AI绘画如何变现?stablediffusion小白上手指南:2个工具+4个渠道

公司一计算机大佬,用AI画画一个月搞钱1w,画着画着都快超过本职工资了。费了点小心思整理出这篇文章,把搜刮来的一手经验全盘托出,觉得有用的友友记得点赞收藏哦! 我有个朋友学设计的,今年找了大半年工作&a…

【轮转数组】力扣python

1.python切片 这里nums[:]代表列表 class Solution:def rotate(self, nums: List[int], k: int) -> None:nlen(nums)nums[:]nums[-k%n:]nums[:-k%n] 2.边pop边push 0代表插入的位置 class Solution:def rotate(self, nums: List[int], k: int) -> None:nlen(nums)fo…

OpenAi 免费GPT-4o来袭,音频视觉文本实现「大一统」

今天凌晨,即北京时间5月14日1点整,OpenAI 召开了首场春季发布会,CTO Mira Murati 在台上和团队用短短不到30分钟的时间,揭开了最新旗舰模型 GPT-4o 的神秘面纱,以及基于 GPT-4o 的 ChatGPT,均为免费使用。 …

试试这四个AI论文工具和降重技术,低成本高回报

在科研领域,AI写作工具如同新一代的科研利器,它们能够极大提高文献查阅、思路整理和表达优化的效率,本质上促进了科研工作的进步。AI写作工具不仅快速获取并整理海量信息,还帮助我们精确提炼中心思想,显著提升论文写作…

智能终端RK3568主板在智慧公交条形屏项目的应用,支持鸿蒙,支持全国产化

基于AIoT-3568A的智慧公交条形屏,可支持公交线路动态展示,语音到站提醒,减少过乘、漏乘的情况,有效提高了公交服务效率和质量,为乘客提供了更舒适、更安全和更方便的出行体验,为城市的发展增添了新的活力。…

WS2812B-2020 智能控制LED集成光源芯片IC

一般说明 WS2812B-2020是一款智能控制LED光源,它的外部采用了最新的模压封装技术,控制电路和RGB芯片集成在一个2020组件中。其内部包括智能数字端口数据锁存器和信号整形放大驱动电路。还包括一个精密的内部振荡器和一个电压可编程恒流控制部分&…

从零开始学习Linux(8)----自定义shell

shell从用户读入字符串“ls”,shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序,并等待这个进程结束。所以要写一个shell&…

iZotope RX 11 for Mac:音频修复的终极利器

在音频制作的浩瀚星海中,每一份声音都是珍贵的宝石,但往往被各种噪音、杂音所掩盖。此刻,iZotope RX 11 for Mac犹如一位专业的匠人,以其精湛的技术,将每一份声音雕琢至完美。 iZotope RX 11 for Mac,这是…

【源码】二开 欧亚版自动抢单系统源码(中、英、泰、日四语言)

二开 欧亚版自动抢单系统源码(中、英、泰、日四语言) 这个版本除了前台界面做了一些改变,同时功能细节上也做了一些改动。 如增加订单、充值、提现假人管理;增加短信开关,增加短信宝、阿里云、云之讯短信接口;增加商品匹配区间设…

男士内裤有什么牌子?2024年公认男士内裤口碑最好的品牌

对于男性来说,一条内裤都是可以穿好久的存在。但实际上作为贴身衣物的内裤,最好是3-6月就更换一次是最好的,如果长期不更换会使内裤的舒适性透气性降低,而且残留细菌无法得到清除! 面对市面上琳琅满目的品牌和材质分类…

vscode调试Electron+ts

调试Electronjs 调试Electronjs: https://www.electronjs.org/zh/docs/latest/tutorial/debugging-vscode 调试Electronts 首先看一下,我的目录结构。目录结构决定了launch.json中的路径部分。我将在项目根目录下进行调试,项目根目录下包含electron代码…

3款游戏录屏软件,记录你的精彩瞬间!

随着科技的飞速发展,游戏行业迎来了前所未有的繁荣。在游戏世界中,录屏功能成为了一项重要的辅助功能,它不仅能够帮助玩家记录精彩的游戏瞬间,还可以用于游戏教学、解说、直播等多种场景。本文将详细介绍三款主流的游戏录屏软件&a…

如何把root账号的文件修改为tank账号

linux系统 以当前用户命令创建的目录都是root的用户并且是只读的: 用优盘拷贝的文件及文件夹的权限是: 解决方案是:把root账号的文件修改为tank账号 在Linux系统中,如果您需要将属于root用户的文件或目录的所有权更改到另一个用…

裸机工程开发调试

裸机工程开发调试 嵌入式系统的组成 嵌入式系统设备遵循自底向上的基本结构 嵌入式微处理器 嵌入式操作系统 外围硬件设备 用户应用程序 ###s5p6818系统主要资源 s5p6818是三星公司推出的64位RISC微处理器, 其CPU采用的是ARM CortexA53内核, 基于ARMv7和ARMv8指…

U盘打不开提示格式化怎么办?(含数据恢复及U盘修复教程)

引言: 随着数字化时代的发展,U盘已成为我们日常生活和工作中不可或缺的数据存储工具。然而,有时我们可能会遇到U盘突然无法打开,并提示需要格式化的问题。这不仅会打乱我们的工作节奏,还可能会导致重要数据丢失。本文…

深度解析:亚马逊自养号测评系统构建全攻略

在当今日益激烈的平台竞争环境下,众多商家纷纷感受到传统运营模式的挑战,常规的广告和营销手段逐渐显得力不从心。为了突破这一困境,商家们开始寻求新的推广途径,其中,自养号测评作为一种新兴且有效的推广方式&#xf…