《游戏编程模式》学习笔记(四) 观察者模式 Observer Pattern

news2025/1/18 19:08:12

定义

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

这是定义,看不懂就看不懂吧,我接下来举个例子慢慢说

为什么我们需要观察者模式

我们看一个很简单的需求,现在要你在游戏中加入成就系统,在物体坠落1000米的时候给玩家发一个成就勋章,你要这么做?

最直观的方法就是,在游戏的物理系统那一部分中,加入这么一段代码:

void Physics::updateEntity(Entity& entity)
{
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface())
    {
        if (surface.height - entity.height > 1000)
        {
            //解锁成就
            unlockFallOffBridge();
        }
    }
}

咋一看是不是还行?就加了几行而已。
那么如果我还要求你播放坠落音效呢?是不是还得这样写:

void Physics::updateEntity(Entity& entity)
{
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface())
    {
        if (surface.height - entity.height > 1000)
        {
            //解锁成就
            unlockFallOffBridge();
            
            //播放音效
            playfallmusic();
        }
    }
}

这样看也还行,那如果组长让你根据物体撞击不同的地面,播放不同的地面音效,那这段代码是不是又得膨胀了:

void Physics::updateEntity(Entity& entity)
{
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface())
    {
        if (surface.height - entity.height > 1000)
        {
            //解锁成就
            unlockFallOffBridge();
            
            //播放音效
            if (hitground)
            {
                playhitgroundmusic();
            }
            if (hitwater)
            {
                playhitwatermusic();
            }
            //.....
        }
    }
}

要知道,这可是在你的游戏的物理引擎中,我们并不想看到在处理撞击代码的线性代数时, 有出现关于成就系统,音效系统的调用是不?我们喜欢的是,照旧,让关注游戏一部分的所有代码集成到一块。我们想要解耦物理系统和这些不相关的东西。

这就是观察者模式出现的原因。 这让代码宣称有趣的事情发生了,而不必关心到底是谁接受了通知。

一旦你使用了观察者模式,你的代码就会变成这样:

void Physics::updateEntity(Entity& entity)
{
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface())
    {
        notify(entity, EVENT_START_FALL);
    }
}

是不是简洁了很多很多?比刚才那一大堆丑陋的代码好看多了。

观察者模式做的就是声称,“额,我不知道有谁感兴趣,但是这个东西刚刚掉下去了。做你想做的事吧。”

可能有人会说,诶,这也没有完全解耦啊。的确,物理引擎确实决定了要发送什么通知,所以这并没有完全解耦。但在架构这个领域,通常只能让系统变得更好,而不是完美。

如何构建观察者模式?

最传统的构建方式就是这样,使用对象模式构建观察者

我们先写一个基础的观察者抽象基类

class Observer
{
public:
    virtual ~Observer() {}
    virtual void onNotify(const Entity& entity, Event event) = 0;
};

然后让我们的成就系统和音效系统等想成为观察者的系统都继承这个基类:

class Achievements : public Observer
{
public:
    virtual void onNotify(const Entity& entity, Event event)
    {
        switch (event)
        {
        case EVENT_ENTITY_FELL:
            if (entity.isHero() && heroIsOnBridge_)
            {
                unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
            }
            break;

            // 处理其他事件,更新heroIsOnBridge_变量……
        }
    }


private:
    void unlock(Achievement achievement)
    {
        // 如果还没有解锁,那就解锁成就……
    }

    bool heroIsOnBridge_;
};

对于被观察者,如物理系统中,我们只要让它持有这个observer的指针就好了,一旦出现了某些事件,我们就给这些指针指向的observer发消息。
为了正式一点,让所有可能的系统都成为被观察者,我们写一个叫subject的基类,让所有想成为被观察者的系统都可以继承这个基类来成为被观察者。

class Subject
{
public:
  void addObserver(Observer* observer)
  {
    // 添加到数组中……
  }

  void removeObserver(Observer* observer)
  {
    // 从数组中移除……
  }

  void removeObserver(Observer* observer)
  {
    // 从数组中移除……
  }
protected:
  void notify(const Entity& entity, Event event)
  {
    for (int i = 0; i < numObservers_; i++)
    {
      observers_[i]->onNotify(entity, event);
    }
  }

private:
  Observer* observers_[MAX_OBSERVERS];
  int numObservers_;
};

我们可以看见,这里写了一个观察者数组,存了许多观察者的指针,这是因为大部分情况下,被观察者可能会有好多个观察者观察着它。然后我们也写了一些方法来增删这个数组。

然后就是面向对象的东西了,我们让物理系统继承这个基类

class Physics : public Subject
{
public:
  void updateEntity(Entity& entity);
};

现在,当物理引擎做了些值得关注的事情,它调用notify(),就像之前的例子。 它遍历了观察者列表,通知所有观察者。
在这里插入图片描述
恭喜你已经掌握了如何写一个观察者模式,你所看到的就是一个观察者模式的全部。现在来回顾一下定义:

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

是不是有点明白了?

**

观察者模式的使用场合

**

当一个抽象模式有两个方面,其中一个方面依赖于另一个方面,需要将这两个方面分别封装到独立的对象中,彼此独立地改变和复用的时候。
当一个系统中一个对象的改变需要同时改变其他对象内容,但是又不知道待改变的对象到底有多少个的时候。
当一个对象的改变必须通知其他对象作出相应的变化,但是不能确定通知的对象是谁的时候。

观察者模式的缺点:

  1. 由于观察者模式调用了一些虚方法,终究会比静态调用慢一些。
  2. 观察者模式是同步的。 被观察者直接调用了观察者,这意味着直到所有观察者的通知方法返回后, 被观察者才会继续自己的工作。观察者会阻塞被观察者的运行。
  3. 由于被观察者维护了一个数组来存储观察者指针,在实际情况中一般会用动态数组而不是这次例子中的静态数组。这样就会做出太多的动态分配。解决方法还是有的,那就是使用链表而不是数组来存储观察者指针(反正你都得遍历发通知,这俩差不多)。

原文链接:https://gpp.tkchu.me/observer.html

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

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

相关文章

二叉树的构建及遍历

题目链接:https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef?tpId60&&tqId29483&rp1&ru/activity/oj&qru/ta/tsing-kaoyan/question-ranking 题目 编一个程序&#xff0c;读入用户输入的一串先序遍历字符串&#xff0c;根据此字符串建…

视频播放相关记录

一、场景 App应用测试在二次回归时&#xff0c;提出了安卓端视频定位不准的问题。 二、分析 代码层面使用了安卓原始的MediaPlayer中的seekTo接口来定位&#xff1a; mp.setOnInfoListener(new MediaPlayer.OnInfoListener() {Overridepublic boolean onInfo(MediaPlayer mp, i…

多旋翼飞控底层算法开发系列实验 | 多旋翼动力系统设计实验1

多旋翼飞控底层算法开发系列实验 | 多旋翼动力系统设计实验1 01 多旋翼动力系统简介 多旋翼无人机的动力系统通常包括螺旋桨、电机、电调以及电池。动力系统是多旋翼最重要的组成部分&#xff0c;它决定了多旋翼的主要性能&#xff0c;如悬停时间、载重能力、飞行速度和飞行距…

The kernel appears to have died. It will restart automatically.

在使用pytorch运行一个小案例时&#xff0c;绘制图像时出现了 The kernel appears to have died. It will restart automatically. 查看 anaconda prompt OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized. OMP: Hint This means…

NestJs 中使用 mongoose

在 NestJS 中链接 MongoDB 有两种方法。一种方法就是使用TypeORM来进行连接&#xff0c;另外一种方法就是使用Mongoose。 此笔记主要是记录使用Mongoose的。所以我们先安装所需的依赖&#xff1a; npm i nestjs/mongoose mongoose安装完成后&#xff0c;需要在AppModule中引入…

前端基础学习笔记

目录 1.HTML部分1.HTML简介2.实例介绍3.标题4.段落5.链接6.图像7.表格8.速查列表1.基本文档2.基本标签3.文本格式化4.链接5.图片6.无序列表7.有序列表8.定义列表9.表格10.框架11.表单 2.CSS部分1.CSS简介1.什么是CSS2.为什么使用CSS3.CSS作用 2.基本用法1.CSS语法2.CSS应用方式…

Failed to resolve component: v-data-table“. vue3 + vuefity 使用 v-data-table 报错解决

在使用 vue3 vuetify 开发项目的过程中用到了 v-data-table 组件&#xff0c;结果在使用的过程中发现加载失败控制台报错。 [Vue warn]: Failed to resolve component: VDataTable解决方案&#xff1a; import { VDataTable } from vuetify/labs/VDataTable参考文档: https:…

双碳目标下基于“遥感+”多技术融合在碳储量、碳排放、碳循环、温室气体教程

详情点击链接&#xff1a;双碳目标下基于“遥感”多技术融合在碳储量、碳排放、碳循环、温室气体教程 一&#xff1a;双碳视角下遥感技术的研究方向 1.双碳背景及遥感的现实需求 2.全球碳库、碳收支及碳循环现状 3.碳储量、碳收支与碳循环中的遥感技术 4.ENVI及ArcGIS软件一体…

(三) CUDA 硬件实现

一组带有on-chip 共享内存的SIMD多处理器 GPU可以被看作一组多处理器, 每个多处理器使用单一指令&#xff0c;多数据架构(SIMD)【单指令流多数据流】 在任何给定的时钟周期内&#xff0c;多处理器的每个处理器执行同一指令&#xff0c;但操作不同的数据 每个多处理器使用以下…

广告ROI可洞察到订单转化率啦

toB广告营销人的一日三问&#xff1a; 如何实现线索增长&#xff1f;如何获取更多高质量线索&#xff1f;如何能用更少的钱拿到更多高质量的线索&#xff1f; < 广告营销的终极目标&#xff0c;就是提升ROI > 从ROI公式中&#xff0c;可以找到提升广告营销ROI的路径&…

Linux:shell脚本循环语句

目录 一、循环含义 二、echo命令 三、for 3.1.将1到100累加求和 3.2批量添加用户 3.3 根据IP地址检查主机状态 四、 while 和 until 4.1 猜价格 4.2 1-100求和 一、循环含义 循环含义 将某代码段重复运行多次&#xff0c;通常有进入循环的条件和退出循环的条件 重复…

kali搭建vulhub漏洞靶场

安装kali 下载kali作为虚拟环境&#xff0c; Get Kali | Kali Linux 通过vmvare打开&#xff0c;默认账号密码kali/kali 修改root密码 su passwd root 如果一些配置普通用户做不了就切换kali&#xff0c;或sudo 命令 kali配置 apt换源 echo > /etc/apt/sources.list v…

java对大文件分片上传

这里记录一下&#xff0c;Java对大文件的切分&#xff0c;和后端接口分片上传的实现逻辑 正常&#xff0c;前后端分离的项目其实是前端去切分文件&#xff0c;后端接口接收到切分后的分片文件去合并&#xff0c;这里都用java来记录一下。特别说明&#xff1a;我这里用的是zip包…

Codeforces Round 888 (Div. 3)ABC

Codeforces Round 888 (Div. 3) 目录 A. Escalator Conversations题目大意思路代码 B. Parity Sort题目大意思路代码 C. Tiles Comeback题目大意思路代码 A. Escalator Conversations 题目大意 判断有多少个人能够通过站在楼梯上的操作和VLAD一样高或者是VLAD通过站在楼梯上的…

6.1 MyBatis基础

1.MyBatis概述 MyBatis本是apache的一个开源项目iBatis&#xff0c;2010年这个项目由apache software foundation迁移到了google code&#xff0c;并且改名为MyBatis&#xff0c;2013年11月MyBatis又被迁移到Github。 MyBatis是一个支持普通SQL查询、存储过程以及高级映射的持…

欧拉算法与埃氏筛法比较

#include<iostream> using namespace std; bool data[100000005]; // zhishu用于存储质数的数组 &#xff0c;cnt下标 int zhishu[100000000],cnt0;int main() {data[1] 1;// 1表示素数 int n;cin >> n;// 循环遍历for(int i2;i<n;i){if(data[i] 0){// 表明是…

嵌入式学习之C语言指针部分复习

今天主要把C语言的指针部分再次认真的复习了一下&#xff0c;对于指针的整体框架有了更加深刻的理解&#xff0c;特别要重点区分函数指针&#xff0c;指针函数&#xff0c;数组指针&#xff0c;指针数组部分&#xff0c;对于这部分的应用回非常的重要&#xff0c;而且C语言指针…

海龟绘图——长方体(包含建立和销毁的过程)

运行结果&#xff1a; 代码&#xff1a; import turtle turtle.setup(1024,1024,200,200)# 移动画笔 turtle.penup() turtle.goto(-200,200) turtle.pendown() # 绘制第一条实线 turtle.setheading(0) turtle.forward(300) # 绘制第二条实线 turtle.setheading(18045) turtle.…

Python语法基础--条件选择

学习目标 使用比较运算符编写布尔表达式。使用random.randint(a,b)或者random.random()函数来生成随机数。编写布尔表达式(AdditionQuiz)。使用单向if语句实现选择控制。使用单向if语句编程。使用双向if-else语句实现选择控制。使用嵌套if和多向if-elif-else语句实现选择控制。…