V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yiouejv
V2EX  ›  Lua

为什么游戏架构要用事件来驱动?

  •  
  •   yiouejv · 2021-03-20 17:16:44 +08:00 · 4726 次点击
    这是一个创建于 1351 天前的主题,其中的信息可能已经有所发展或是发生改变。

    今天总结一下游戏架构中的事件触发机制,游戏架构中为什么需要由事件驱动。

    主要是为了解耦,所谓高内聚,低耦合,如果不采用事件驱动的方式,则会像下面这样来写代码。

    比如说,游戏内有多个玩法模块,”玩家“在打造装备的时候,可能触发”装备打造 xx 阶的成就“,也可能达成某种条件获得了时装。

    这种情景的话,如果没有采用事件驱动的方式来写代码,则需要在装备模块的”升级装备“函数内调用 成就模块 的检查成就达成的函数,还需要调用 时装模块 的检查获得时装的函数。

    function equipStrengthen()
    	-- 装备强化逻辑
    
    	checkAchievement()  -- 成就模块检查成就
    	checkObtainFashion()  -- 时装模块检查获得时装
    end
    

    如果装备关联的模块越来越多的话,就要记得去相关的函数内添加相关的调用。

    事件驱动的方式就比较好的处理了这种情况。 如果是用事件驱动的方式来处理以上问题,则我们会这么做,由装备模块发出“装备强化”的事件,成就模块和时装模块只需要监听”装备强化“事件做相应的处理就好了。

    在装备强化的模块内只需要一行代码,发出事件,后续如果需要增加关联的模块时,装备模块完全不用动,新模块只要增加监听事件就可以了。

    下面我用 lua 实现一个例子:

    ------------------------------------------------------ 事件触发器
    local Listener = {}
    function Listener:new(channel, callback)
        local obj = {
            callback = callback,
            channel = channel,
        }
        setmetatable(obj, self)
        self.__index = self
        return obj
    end
    
    
    local Channel = {}
    function Channel:new(event)
        assert(event)
        local obj = {
            listeners = {},
            event = event,
        }
        setmetatable(obj, self)
        self.__index = self
        return obj
    end
    
    function Channel:on(callback)
        listener = Listener:new(self, callback)
        table.insert(self.listeners, listener)
    end
    
    
    local EventEmitter = {}
    function EventEmitter:new()
        local obj = {
            events = {},  -- 监听的所有事件
            channels = {}, -- event: channel
        }
        setmetatable(obj, self)
        self.__index = self
        return obj
    end
    
    function EventEmitter:setEvents(events)
        self.events = events
    end
    
    function EventEmitter:on(event, callback)
        assert(event)
        assert(callback)
        if not self.events[event] then
            error("not register event: "..event)
        end
        local channel = self.channels[event]
        if not channel then
            channel = Channel:new(event)
            self.channels[event] = channel
        end
        channel:on(callback)
    end
    
    function EventEmitter:emit(event)
        if not self.events[event] then
            error("not register event: "..event)
        end
    
        local channel = self.channels[event]
        if not channel then return end
    
        for _, listener in ipairs(channel.listeners) do
            listener.callback()
        end
    end
    ----------------------------------------------------- 装备模块
    local eventEmitter = EventEmitter:new()
    eventEmitter:setEvents({
        ["equipStrengthen"] = "装备强化",
    })
    
    
    function equipStrengthen()
        -- 装备强化逻辑
        eventEmitter:emit("equipStrengthen")
    end
    
    ------------------------------------------------------ 成就模块
    function checkAchievement()
        print('checkAchievement')
    end
    
    eventEmitter:on("equipStrengthen", checkAchievement)  -- 成就模块注册监听
    
    ------------------------------------------------------ 时装模块
    function checkObtainFashion()
        print('checkObtainFashion')
    end
    
    eventEmitter:on("equipStrengthen", checkObtainFashion)  -- 时装模块注册监听
    ------------------------------------------------------------------------------------
    function main()
        equipStrengthen()
    end
    
    main()
    

    最后输出:

    checkAchievement
    checkObtainFashion
    

    下面这个图可以有助于理解,

    在这里插入图片描述

    上述的实现比较简单,主要意思表达出来了,具体的细节可以结合需要再添加就好了。

    如果觉得对你有帮助的话请 @程序员杨小哥 点个赞,谢谢!

    7 条回复    2021-07-28 16:40:30 +08:00
    secondwtq
        1
    secondwtq  
       2021-03-20 17:26:14 +08:00
    感觉像是 push 和 poll 的区别
    说起来最近 ECS 好像 hype 蛮多,楼主怎么看 ECS 和事件之间的关系?
    yiouejv
        2
    yiouejv  
    OP
       2021-03-20 19:10:49 +08:00
    @secondwtq 这个我不是很清楚哦
    no1xsyzy
        3
    no1xsyzy  
       2021-03-20 23:22:48 +08:00
    @secondwtq Entity component system ?
    瞄了一眼,似乎跟 Trello 的设计差不多。Entity = Card,System = Powerups,本体论上的 Component 作为实现细节被隐藏了,但总体而言可以理解为 Powerups 为 Card 带来的字段组合。
    no1xsyzy
        4
    no1xsyzy  
       2021-03-20 23:45:12 +08:00   ❤️ 1
    @yiouejv @secondwtq 更仔细的想法:事件模型是可以用于实现 ECS 的一种方式。
    wiki,CC BY-SA 3.0:
    > A solution could be to use the observer pattern. All systems that depend on an event subscribe to it.
    (当然,事件决非局限于 ECS,ECS 也并不是只有事件一种解决方式)

    至于二者的不兼容性,事件模型是有竞态、甚至可以说常常发生竞态的:如何确定同一个事件的多个监听者的响应顺序?这很可能是一个启动时发生的竞态(谁先监听上)。而对于游戏这个连复现都是操作上比较繁琐的东西,有这种问题存在将会让 debug 成本成倍增加
    而游戏通常会固定游戏帧时间( tick ),那么每 tick 进行一次运算是相当可预期的,性能上也不会捉鸡(当然,如果不对运算量作限制,can't keep up 也是存在的 x-o )。
    codehz
        5
    codehz  
       2021-03-21 09:21:26 +08:00   ❤️ 1
    ECS 里的 component 根本不能放逻辑,就是一个普通数据,所有逻辑都应该在 system 中处理,而 Entity 则作为实体的索引,通常可以实现成一个整数。
    系统通过按一定条件遍历组件和观测组件增删状态来实现逻辑,每一个系统单独描述一个功能。
    ECS 里的事件系统,要做的话,会做成类似资源的模式,(其实说白了就是全局变量),但是和楼主说的这里有所不同的是,事件触发并非是 push 模式,而是会做成在 system 里 pull,这样的好处很明显,就是规避了回调这种“扭曲”的控制流,使得你可以在正确的上下文处理事件,也不会由于并发导致触发事件处理器时的竞争状态(一个合理的 ecs 系统,应该能够根据 system 的需求(即需要读写哪些组件,用到了哪些资源)合理的安排并发,既不会导致竞争状态,也能最大程度利用多个核心,这在传统事件驱动里就很难做了,要么做成单线程处理一切,要么就是重新实现一大堆同步控制原语,保证事件处理不会有竞争)
    dreamstart
        6
    dreamstart  
       2021-03-21 09:38:05 +08:00   ❤️ 1
    我现在就在写 ECS 我觉得跟之前写事件还是挺不一样的 (起码代码思路都不一样,代码思路看楼上就可)理论上是不用新写类的,但是在实际开发过程中还是不可避免的在某些需求上要写某些类,所以一般还有个 uility 用来做工具包。而且确实如楼上所说在实际开发中要搞一个事件触发的话就写一个全局的事件队列就完事了,到了对应的 system 再处理这个队列
    hmxxmh
        7
    hmxxmh  
       2021-07-28 16:40:30 +08:00
    观察者模式?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2538 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 05:04 · PVG 13:04 · LAX 21:04 · JFK 00:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.