首页 > 其他分享 >【Unity百宝箱】游戏中的观察者模式

【Unity百宝箱】游戏中的观察者模式

时间:2023-12-28 20:34:28浏览次数:20  
标签:name 百宝箱 eventDic 观察者 actions Unity action public EventInfo

【Unity百宝箱】游戏中的观察者模式

原创 打工人小棋 打工人小棋 2023-04-27 13:35 发表于广东
  • Hi,大家好,我是游戏区“bug主”打工人小棋!

图片

 

今天我想和大家聊一聊游戏中的观察者模式~

近两期视频,都是在为下一期视频做准备,在下期视频中,大家将会看到用户数据存储、以及观察者模式在游戏中的大量应用和实践,希望能让你们彻底学会这些设计思想。

  • • bilibili: Json用户数据存档

  • • bilibili: 观察者模式

介绍

什么是观察者模式?

我给举个简单的例子,当同学们想要第一时间得知小棋更新视频了,最好的办法是什么?

给你们5秒钟思考:5 4 3 2 1

最好的办法当然是点个大大的关注,最好是特别关注,因为这样B站会在第一时间把我更新了的消息通知到你们。

有的同学说,我偏不,我就要守着你的主页,每隔1秒钟刷新下页面,我就不信有人比我快!

这样做,不是说不行,甚至在编程领域,还经常这么做。

比如我之前在 8. 阳光拾取 + 僵尸生成 这期视频中,我们在Update函数中,每一帧都去查看阳光数量是否满足需求,满足时才解锁卡片。

public class Card
{
    public int useSun = 25;

    void Update()
    {
        // 每一帧都检查:太阳数量是否足够
        if (GameManager.instance.sunNum >= useSun)
        {
            darkBg.SetActive(false);
        }
        else
        {
            darkBg.SetActive(true);
        }
    }
}

public class GameManager
{
    // 当前阳光数据量
    public int sunNum = 0;
    // 增加阳光
    public void AddSunNum(int value)
    {
        sunNum += value;
    }
}

这样做同样满足我们的需求,也圆满完成了当时任务。

但是大家想想,如果是一个特大型项目,无数个update 都在执行,那将会是一笔巨大的性能开销。

而且数据发生变化时,往往只有一瞬间,比如小棋发了新视频,这个消息的量级是很小的,而你却需要二十四小时全天候守护,这未免付出太大了。

如果你真的这么做,你真的,我哭死。o(╥﹏╥)o

设计

大家可以把B站的这种特别关注,理解成观察者模式的实现。

他包含两大基本要素:订阅和通知。

粉丝们通过关注提前订阅Up主的更新消息,B站在Up主们更新时通知给订阅的粉丝。

这样做的好处在于双方的耦合性极低,发送方只需要通知下去,订阅方就一定会收到消息,至于收到消息后怎么做,发送方不清楚也不关心。

将来有更多人关注小棋了,也就是无脑通知就完事了,粉丝们想要点进来一键三连,或者评论转发都可以。(不点赞的不许走)

代码

废话少说,直接上代码。

  1. 1. 单例

首先,我们的事件中心作为所有事件的管理器,在游戏中是唯一的,因为我们将其设置为单例模式。

单例的介绍和写法网上教程有很多,我这里就举个最简单的写法。

// 事件中心
public class EventCenter
{
    private static EventCenter instance;

    public static EventCenter Instance
    {
        get
        {
            if(instance == null)
            {
                instance = new EventCenter();
            }
            return instance;
        }
    }

    public void test()
    {
        print("test test test")
    }
}

使用时只需要这么写:

EventCenter().Instance.test()

即可看到运行结果:

输出: test test test

  1. 1. 响应事件

当事件发生时,事件中心会把消息通知给感兴趣的订阅者。

这里的订阅者可以使用Unity为我们封装的UnityAction,大家可以简单将其理解为代码中的一个个方法。

当事件触发时,就调用这些方法。

每个事件可能有多个订阅者,比如小棋就有不少粉丝订阅。

因此这里我们使用字典存储所有的事件,字典的key对应订阅名,字典的value对应一个订阅列表。

下面我们对订阅列表进行下封装:

// 使用接口,方便拓展有参事件和无参事件
public interface IEventInfo
{
}

// 无参数事件响应
public class EventInfo : IEventInfo
{
    public UnityAction actions;
    public EventInfo(UnityAction action)
    {
        actions += action;
    }
}
  1. 1. 框架设计

有了响应事件后,我们就可以搭建起事件中心的整体框架了。

在事件中心里,最重要的两个方法:

  • • 订阅消息

  • • 通知消息

public class EventCenter
{
    // 存储所有事件 
    private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>();

    // 订阅消息
    public void AddEventListener(string name, UnityAction action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo).actions += action;
        else
            _eventDic.Add(name, new EventInfo(action));
    }

    // 通知消息
    public void EventTrigger(string name)
    {
        if (_eventDic.ContainsKey(name))
            if ((_eventDic[name] as EventInfo).actions != null)
                (_eventDic[name] as EventInfo).actions.Invoke();
    }
}

在某些时候,我们还需要对无用的消息进行清除,比如取消关注。

    // 移除无参数事件的监听
    public void RemoveEventListener(string name, UnityAction action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo).actions -= action;
    }

    // 清空事件监听
    public void Clear()
    {
        _eventDic.Clear();
    }

以上所提的方法都是无参方法,相当于up主更新视频后,B站只通知粉丝:

小棋更新视频啦~!

但是更新了什么视频却不得而知。

想要在通知的消息里得到更新的视频内容,还需要添加有参方法,在通知时把视频信息一起发送过去。

这里用到了C#语法中的泛型,具体逻辑和无参方法几乎一样。

// 带参数事件响应
public class EventInfo<T> : IEventInfo
{
    public UnityAction<T> actions;

    public EventInfo(UnityAction<T> action)
    {
        actions += action;
    }
}

public class EventCenter
{
    // 数据结构
    private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>();

    // 添加带参数事件的监听
    public void AddEventListener<T>(string name, UnityAction<T> action)
    {
        // 旧事件
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo<T>).actions += action;
        // 新事件
        else
            _eventDic.Add(name, new EventInfo<T>(action));
    }

    // 移除带参数事件的监听
    public void RemoveEventListener<T>(string name, UnityAction<T> action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo<T>).actions -= action;
    }

    // 分发带参数的事件
    public void EventTrigger<T>(string name, T info)
    {
        if (_eventDic.ContainsKey(name))
            if ((_eventDic[name] as EventInfo<T>).actions != null)
                (_eventDic[name] as EventInfo<T>).actions.Invoke(info);
    }
}

使用

还是以刚刚卡片的这个逻辑来举例,我们一起思考下:如何以观察者模式重构这段代码。

public class Card
{
    public int useSun = 25;

    void Update()
    {
        // 每一帧都检查:太阳数量是否足够
        if (GameManager.instance.sunNum >= useSun)
        {
            darkBg.SetActive(false);
        }
        else
        {
            darkBg.SetActive(true);
        }
    }
}

public class GameManager
{
    // 当前阳光数据量
    public int sunNum = 0;
    // 增加阳光
    public void AddSunNum(int value)
    {
        sunNum += value;
    }
}

首先分清两件最关键的事情:

  1. 1. 谁订阅消息

  2. 2. 谁触发消息

在上述情境中,不难分析得到:

  1. 1. 卡片需要订阅消息,当阳光改变时,他需要检测下卡片是否解锁。

  2. 2. GameManager需要发布消息,当阳光数量改变时,他需要广播这个消息。

经过上述分析后,我们可以得到重构后的代码:

public class Card
{
    public int useSun = 25;

    void Start()
    {
        // Start中添加消息监听,消息命名为: EventSunNumChange
        EventCenter.Instance.AddEventListener<int>("EventSunNumChange", CheckUnLock);
    }


    public void CheckUnLock(int num)
    {
        print(">>>>> Listen EventSunNumChange");
        // 只在阳光改变时检查:太阳数量是否足够
        if (num >= useSun)
        {
            darkBg.SetActive(false);
        }
        else
        {
            darkBg.SetActive(true);
        }
    }
}

public class GameManager
{
    public int sunNum = 0;
    // 当阳光数量发生改变时,触发消息通知
    public void AddSunNum(int value)
    {
        sunNum += value;
        EventCenter.Instance.EventTrigger<int>("EventSunNumChange", sunNum);
        print(">>>>> Trigger EventSunNumChange");
    }
}

总结

最后来测试下效果,在植物大战僵尸游戏中:拾取阳光会增加阳光的数量,而卡片需要阳光数量满足条件时才会解锁。

经过验证,消息得到了正确的触发和响应。

代码中的Log也打印出来了:

>>>>> Trigger EventSunNumChange
>>>>> Listen EventSunNumChange

最后把框架的主体代码贴出来:

using System.Collections.Generic;
using UnityEngine.Events;

// 事件响应空接口,用于支持可有可无的参数类型
public interface IEventInfo
{
}

// 带参数事件响应
public class EventInfo<T> : IEventInfo
{
    public UnityAction<T> actions;

    public EventInfo(UnityAction<T> action)
    {
        actions += action;
    }
}

// 无参数事件响应
public class EventInfo : IEventInfo
{
    public UnityAction actions;
    public EventInfo(UnityAction action)
    {
        actions += action;
    }
}

// 事件中心
// 负责注册(监听)事件、分发(触发)事件
// 事件支持 带参数 和 无参数 两种
// 带参数事件使用 EventInfo<T> 数据类型
public class EventCenter
{
    // 数据结构
    private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>();
    private static EventCenter instance;

    public static EventCenter Instance
    {
        get
        {
            if(instance == null)
            {
                instance = new EventCenter();
            }
            return instance;
        }
    }

    // 添加带参数事件的监听
    public void AddEventListener<T>(string name, UnityAction<T> action)
    {
        // 旧事件
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo<T>).actions += action;
        // 新事件
        else
            _eventDic.Add(name, new EventInfo<T>(action));
    }

    // 添加无参数事件的监听
    public void AddEventListener(string name, UnityAction action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo).actions += action;
        else
            _eventDic.Add(name, new EventInfo(action));
    }

    // 移除带参数事件的监听
    public void RemoveEventListener<T>(string name, UnityAction<T> action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo<T>).actions -= action;
    }

    // 移除无参数事件的监听
    public void RemoveEventListener(string name, UnityAction action)
    {
        if (_eventDic.ContainsKey(name))
            (_eventDic[name] as EventInfo).actions -= action;
    }

    // 分发带参数的事件
    public void EventTrigger<T>(string name, T info)
    {
        if (_eventDic.ContainsKey(name))
            if ((_eventDic[name] as EventInfo<T>).actions != null)
                (_eventDic[name] as EventInfo<T>).actions.Invoke(info);
    }

    // 分发无参数的事件
    public void EventTrigger(string name)
    {
        if (_eventDic.ContainsKey(name))
            if ((_eventDic[name] as EventInfo).actions != null)
                (_eventDic[name] as EventInfo).actions.Invoke();
    }

    // 清空事件监听
    // 主要用于场景切换时防止内存泄漏
    public void Clear()
    {
        _eventDic.Clear();
    }
}

其他更多平台:

  • • bilibili 打工人小棋

  • • 知乎 打工人小棋

  • • CSDN 打工人小棋


一起加油 :)

 

Unity百宝箱5 Unity百宝箱 · 目录 上一篇【Unity百宝箱】游戏中的用户数据存档下一篇【泰裤辣 の Unity百宝箱】分享一套简单易用的游戏UI框架 阅读 184      

人划线

 

标签:name,百宝箱,eventDic,观察者,actions,Unity,action,public,EventInfo
From: https://www.cnblogs.com/Jimmy104/p/17933502.html

相关文章

  • 【Unity百宝箱】游戏中的用户数据存档
    【Unity百宝箱】游戏中的用户数据存档原创 打工人小棋 打工人小棋 2023-04-1700:04 发表于广东Hi大家好,我是游戏区Bug打工人小棋。在游戏开发过程中,我们经常有存储用户数据的这一需求,比方说:游戏音量、关卡进度、任务进度等等。在联网游戏中,往往会把一些用户核心......
  • 【泰裤辣 の Unity百宝箱】Canvas组件四件套讲解
    【泰裤辣のUnity百宝箱】Canvas组件四件套讲解原创 打工人小棋 打工人小棋 2023-05-1613:24 发表于广东1.介绍在上一期内容中,我分享了一套简单易用的UI框架。没想到大家的学习热情这么高,讨论度是目前所有内容最高的。由此可见,天下苦UI(秦)久已!!!接下去,我们继续......
  • Unity解析key不确定的Json
    遇到Json的key不固定时,只需要解析value,如下Jsondata下的key(1和2)是变化的:{"status":1,"msg":"success","data":["1:":{"atitle":"test",......
  • Unity_U_OP1 ScriptableObject 替代单例
    核心思想:解耦GameManager单例模式,不再由一个单例管理所有事件触发,拆分成无数个小单例,各自管理优点:更加灵活的事件管理模式复用性高,对于相关类型的事件,只需要写一遍代码,剩下的拖拖拖就可以实现相同的功能。缺点:管理起来相对麻烦不利于维护,除非对这个系统非常了解,要不然排......
  • Maya与Unity模型尺度统一
    Maya与Unity模型尺度统一Maya建模默认使用的单位是cm,Unity使用的是m,有时候可能需要把Maya中建好的模型导入到Unity中,因此这篇文章介绍如何修改Maya的默认建模单位,从而使得二者的尺度统一。进入窗口,设置,首选项。修改为m......
  • Unity引擎2D游戏开发,敌人追击状态的转换
    思路:从敌人的位置发射一道射线或者一片区域来对玩家实体进行检测,如果检测倒玩家,则进行追击进攻利用BoxCast()即可实现BoxCast()官方文档:https://docs.unity3d.com/cn/2022.3/ScriptReference/Physics2D.BoxCast.html创建检测区域由于BoxCast需要众多参数,所以在Enemy中创建......
  • Unity3D 如何提升游戏运行效率详解
    前言Unity3D是一款非常强大的游戏引擎,但是在处理复杂场景和大量资源时,游戏运行效率可能会遇到一些问题。本文将详细介绍如何提升Unity3D游戏的运行效率,包括技术详解和代码实现。对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀使用合适的资源压缩......
  • Unity3D Shader在GPU上是如何执行的详解
    Unity3D是一款广泛应用于游戏开发的跨平台开发引擎,它提供了丰富的功能和工具来帮助开发者创建高质量的游戏。其中一个重要的功能就是Shader,它可以用来控制对象的渲染效果。在Unity3D中,Shader是在GPU上执行的,那么它是如何工作的呢?本文将详细解释Unity3DShader在GPU上的执行过程,并......
  • Unity3D Shader Compute Shader基于GPU的并发计算详解
    在游戏开发中,计算密集型的任务通常需要耗费大量的CPU资源,这可能导致游戏性能下降,影响玩家的游戏体验。为了解决这个问题,Unity3D引入了ShaderComputeShader技术,它使用GPU进行并发计算,将一些计算密集型任务从CPU转移到GPU上执行,以提高游戏的性能和效率。本文将详细介绍Unity3DSha......
  • Unity3D 基类脚本怎么分别获取多个子类脚本的组件详解
    Unity3D是一款非常流行的游戏开发引擎,它提供了丰富的功能和工具,使得开发者可以轻松地创建高质量的游戏。在Unity3D中,脚本是游戏对象的一部分,它们通过附加到游戏对象上的组件来实现特定的功能。本文将详细介绍在Unity3D中如何分别获取多个子类脚本的组件,并提供相应的代码实现。对......