Python實(shí)現(xiàn)Event回調(diào)機(jī)制的方法
0.背景
在游戲的UI中,往往會(huì)出現(xiàn)這樣的情況:
在某個(gè)戰(zhàn)斗副本中獲得了某個(gè)道具A,那么當(dāng)進(jìn)入主界面的時(shí)候,你會(huì)看到你的背包UI上有個(gè)小紅點(diǎn)(意思是有新道具),點(diǎn)擊進(jìn)入背包后,發(fā)現(xiàn)新增了道具A,顯示個(gè)數(shù)為1,并且在下個(gè)界面中有個(gè)使用的按鈕由灰色不可使用變成橙色的可使用狀態(tài)
圖1. 事件觸發(fā)說(shuō)明圖
其中這里是由道具獲得這個(gè)事件,觸發(fā)了上述的三個(gè)行為。如果使用顯示調(diào)用行為,會(huì)使得代碼難擴(kuò)展,易出錯(cuò),邏輯混亂等問(wèn)題,如果使用Event回調(diào)機(jī)制,就會(huì)變得十分方便。
其實(shí)Event回調(diào)機(jī)制就是觀察者模式,如下圖:
圖2. 觀察者模式
在C#中存在(delegate & event)的語(yǔ)義來(lái)實(shí)現(xiàn)Event回調(diào)機(jī)制:具體使用如下:
public delegate void NewToolGotEvent(); public class ToolBag { event NewToolGotEvent newToolGotHandler; void Start() { newToolGotHandler += renderRedPoint; newToolGotHandler += renderNewTool; newToolGotHandler += renderAvaliableUseBtn; } void renderRedPoint() { //TODO } void renderNewTool() { //TODO } void renderAvaliableUseBtn() { //TODO } void EventHappened() { newToolGotHandler(); // usage, fill args if necessary } }
如果在Python,可以在注冊(cè)事件的回調(diào)時(shí),帶入一個(gè)參數(shù)callback,在注冊(cè)函數(shù)實(shí)體內(nèi),存在一個(gè)list將callback添加進(jìn)去,形如:
def register_callback(self, cb): self.callbacks.append(cb)
但是這樣是一個(gè)最為普遍的做法,既然是Python,這里我們有更Pythonic的做法,而且相比于上述的觀察者模式,它的做法更加簡(jiǎn)潔,使用更加方便,接下來(lái)我們來(lái)解析一下Python實(shí)現(xiàn)Event callback的步驟。
1. UML類(lèi)圖
上述案例中,是針對(duì)游戲客戶(hù)端UI的案例。所以我們呈現(xiàn)出的UML圖也是與UI相關(guān)。如圖3所示,它顯示了Python中實(shí)現(xiàn)Event回調(diào)的機(jī)制。
圖3. UML關(guān)系圖
如上圖所示,此機(jī)制主要由三個(gè)類(lèi)及他們的實(shí)例(instance)組成:UIBase, UIScene, UIDataEvent。
1 . UIBase: 所有UIScene的基類(lèi),其實(shí)例有scene_id變量,包含兩個(gè)必要的方法, __init__ 是初始化方法,init_data_listeners方法是將實(shí)例中的某些方法, 例如ui_updata_func中包含的UIDataEvent實(shí)例(所有的UIDataEvent實(shí)例都是單例)遍歷,并把ui_update_func注冊(cè)在每一個(gè)UIDataEvent實(shí)例中。
2 . UIScene: 場(chǎng)景類(lèi),管理某個(gè)場(chǎng)景的UI渲染。在其實(shí)例中,存在某些方法,例如ui_update_func需要在某些UIDataEvent實(shí)例觸發(fā)時(shí)候,也被同時(shí)觸發(fā)調(diào)用。ui_update_func在Python中一個(gè)bound method object, 它會(huì)擁有一個(gè)特殊的屬性events,即所有需要觸發(fā)此方法的UIDataEvent實(shí)例集合。這個(gè)通過(guò)裝飾器(decorator)來(lái)實(shí)現(xiàn),即圖中的:
“ui_update_func” is a Python object which add a amount of UIDataEvent instances by Python decorator named “data_listener”
3 . UIDataEvent: 事件類(lèi),該類(lèi)有個(gè)類(lèi)變量_events, 記錄了所有的UIDataEvent實(shí)例,每一個(gè)UIDataEvent實(shí)例都是單例,而且都有一個(gè)名字,和一個(gè)回調(diào)方法集合_callbacks, 里面的每一個(gè)方法都是在本事件觸發(fā)后需要回調(diào)的方法。實(shí)例還有個(gè)__iadd__方法,將需要回調(diào)的函數(shù)cb注冊(cè)進(jìn)去。__call__事件觸發(fā)是實(shí)際觸發(fā)的函數(shù)。
2. 代碼
上一步講述了三個(gè)類(lèi)之間的聯(lián)系與各自的作用,此步展示代碼實(shí)現(xiàn)相關(guān)功能。
a) UIBase.py
首先列出來(lái)的是UIBase的類(lèi),除了上述的__init__與init_data_listeners方法,還多了destroy方法
# -*- coding: utf-8 -*- from UIDataNotifier import UIDataEvent import inspect class UIBase(object): def __init__(self, in_scene_id): self.id = in_scene_id self.init_data_listeners() def init_data_listeners(self): """為所有標(biāo)有@data_listener的成員函數(shù)注冊(cè)事件監(jiān)聽(tīng)器""" for listener_name, listener in inspect.getmembers(self, lambda f: hasattr(f, 'events')): for event in listener.events: event += listener def destroy(self): print '%s.destroy' % self.__class__.__name__ UIDataEvent.clear()
init_data_listener比較難理解,我們看一下built-in的inspect.getmembers的源碼:
def getmembers(object, predicate=None): """Return all members of an object as (name, value) pairs sorted by name. Optionally, only return members that satisfy a given predicate.""" results = [] for key in dir(object): try: value = getattr(object, key) except AttributeError: continue if not predicate or predicate(value): results.append((key, value)) results.sort() return results
其實(shí)源碼的意思就是,在dir(object)的value中找,找到能夠滿(mǎn)足predicate(value) == True的value,然后將(key, value)收集,進(jìn)行排序后返回。
放在代碼的意思是:
for listener_name, listener in inspect.getmembers(self, lambda f: hasattr(f, 'events')): for event in listener.events: event += listener
在dir(scene)中找,找到value中存在名叫events的屬性, 返回得到是一個(gè)list,每個(gè)list的元素是一個(gè)二元tuple: (key, value),其中key,即listener_name是dir(scene)的屬性名,而value, 即listener就是屬性對(duì)象,這里其實(shí)就是包含事件的函數(shù)對(duì)象,然后遍歷listener中的每一個(gè)UIDataEvent實(shí)例,并將listener注冊(cè)到event中(+= ==> __iadd__ )
b) UIScene.py
UIScene的代碼如下:
# -*- coding: utf-8 -*- from UIDataNotifier import * from UIBase import UIBase class UIScene(UIBase): def __init__(self, in_scene_id): super(UIScene, self).__init__(in_scene_id) @data_listener(OnItemAdded) def ui_render_red_point(self, item): print 'ui_render_red_point' @data_listener(OnItemAdded) def ui_render_new_tool(self, item): print 'ui_render_new_tool: ' + item @data_listener(OnItemAdded) def ui_render_avaliable_use_btn(self, item): print 'ui_render_avaliable_use_btn' bag_ui_scene = UIScene(123)
在UIScene中只是要填寫(xiě)對(duì)于OnItemAdded這個(gè)事件觸發(fā)之后,需要回調(diào)的函數(shù),上述代碼中寫(xiě)了三個(gè)函數(shù)。注意需要在函數(shù)上加上裝飾器@data_listener(OnItemAdded),這樣此函數(shù)就會(huì)添加一個(gè)特殊的屬性events,具體裝飾器的代碼見(jiàn)UIDataNotifier.py。
最后新建一個(gè)bag_ui_scene的scene。
c) UIDataNotifier.py
UIDataNotifier.py代碼如下:
# -*- coding: utf-8 -*- import sys def data_listener(*events): def wrapped_f(f): f.events = events return f return wrapped_f class UIDataEvent(object): _events = [] def __init__(self, name): self._name = name self._callbacks = [] UIDataEvent._events.append(self) def __iadd__(self, cb): self._callbacks.append(cb) return self def __call__(self, *args, **kwargs): for cb in self._callbacks: try: cb(*args, **kwargs) except: ex = sys.exc_info() print "UIDataNotifier cb error, function:", cb.__name__, ex def __repr__(self): return 'UIDataEvent %s' % self._name @classmethod def clear(cls): """清空所有事件上的所有監(jiān)聽(tīng)器,在銷(xiāo)毀一個(gè)界面的時(shí)候調(diào)用""" for event in cls._events: event._cb = [] OnItemAdded = UIDataEvent('OnItemAdded')
data_listener裝飾器其實(shí)就是聲明一個(gè)特殊的events屬性,并將所有在UIScene中填寫(xiě)的UIDataEvent實(shí)例元組集合賦值給它。
__iadd__是將參數(shù)cb添加到實(shí)例的變量中_callbacks中,此方法在UIBase的init_data_listeners中使用。
__call__是當(dāng)UIDataEvent實(shí)例自調(diào)用時(shí),例如OnItemAdded(item),實(shí)際調(diào)用的函數(shù),在函數(shù)體里,會(huì)回調(diào)_callbacks中的每個(gè)方法,這也就是Event回調(diào)機(jī)制的核心部分,相當(dāng)于觀察者模式的notify方法
最后新建一個(gè)OnItemAdded事件。
c) client.py
創(chuàng)建上述幾個(gè)類(lèi)之后,使用Event回調(diào)就非常簡(jiǎn)單了,代碼如下:
# -*- coding: utf-8 -*- from UIScene import UIScene from UIDataNotifier import * OnItemAdded('liu_xin_biao') #新道具流星鏢獲得事件發(fā)生了
輸出:
ui_render_avaliable_use_btn ui_render_new_tool: liu_xin_biao ui_render_red_point
3.使用方法
1. 在本模塊內(nèi)增加一個(gè)事件定義,并在注釋中寫(xiě)明事件的參數(shù)及意義。
如果要監(jiān)聽(tīng)一個(gè)事件,請(qǐng)仔細(xì)閱讀相關(guān)注釋。
2. 在ui類(lèi)最頂端import需要的事件及data_listener。
3. 在需要響應(yīng)該事件的方法(監(jiān)聽(tīng)器方法)前增加裝飾器@data_listener,參數(shù)內(nèi)列出要監(jiān)聽(tīng)的所有事件。
如:
@data_listener(OnEventA, OnEventB) def my_listener_method(arg1): ...
注意保持監(jiān)聽(tīng)器方法的參數(shù)個(gè)數(shù)及意義與事件觸發(fā)的地方一致。
4. 在邏輯代碼中適當(dāng)?shù)奈恢脤?duì)事件進(jìn)行觸發(fā)。如OnEventA(arg1, ...)
注意:并不是所有與UI的交互都必須使用事件,事件機(jī)制是為了方便多對(duì)多的交互。比如背包物品改變事件,有多個(gè)UI都會(huì)監(jiān)聽(tīng)背包物品的變化,而有多種邏輯都會(huì)導(dǎo)致背包物品變化,這時(shí)使用事件就比較方便。
4. 總結(jié)
本文主要講述了如何使用Python實(shí)現(xiàn)Event回調(diào)機(jī)制,上述的示例代碼參考我的[github-EventCallBack] (https://github.com/csdz/SnapToSnap/tree/master/EventCallBack)。
以上這篇Python實(shí)現(xiàn)Event回調(diào)機(jī)制的方法就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- 詳解Python 多線(xiàn)程 Timer定時(shí)器/延遲執(zhí)行、Event事件
- python Event事件、進(jìn)程池與線(xiàn)程池、協(xié)程解析
- python事件驅(qū)動(dòng)event實(shí)現(xiàn)詳解
- 詳解python多線(xiàn)程、鎖、event事件機(jī)制的簡(jiǎn)單使用
- python多線(xiàn)程之事件Event的使用詳解
- Python編程之event對(duì)象的用法實(shí)例分析
- Python的互斥鎖與信號(hào)量詳解
- Python3.X 線(xiàn)程中信號(hào)量的使用方法示例
- 分析Python感知線(xiàn)程狀態(tài)的解決方案之Event與信號(hào)量
相關(guān)文章
Python實(shí)現(xiàn)時(shí)鐘顯示效果思路詳解
這篇文章主要介紹了Python實(shí)現(xiàn)時(shí)鐘顯示,需要的朋友可以參考下2018-04-04對(duì)Python Pexpect 模塊的使用說(shuō)明詳解
今天小編就為大家分享一篇對(duì)Python Pexpect 模塊的使用說(shuō)明詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-02-02python 計(jì)算概率密度、累計(jì)分布、逆函數(shù)的例子
這篇文章主要介紹了python 計(jì)算概率密度、累計(jì)分布、逆函數(shù)的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02python thrift搭建服務(wù)端和客戶(hù)端測(cè)試程序
這篇文章主要為大家詳細(xì)介紹了python thrift搭建服務(wù)端和客戶(hù)端測(cè)試程序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Python 實(shí)現(xiàn)文件的全備份和差異備份詳解
這篇文章主要介紹了Python 實(shí)現(xiàn)文件的全備份和差異備份詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12關(guān)于AnacondaNavigator?Jupyter?Notebook更換Python內(nèi)核的問(wèn)題
因?yàn)樾掳惭b的Anaconda?Navigator默認(rèn)安裝了一個(gè)Python,Jupyter?Notebook默認(rèn)使用的內(nèi)核就是這個(gè)Python,跟我系統(tǒng)安裝好的Python沖突了,下面小編給大家介紹AnacondaNavigator?Jupyter?Notebook更換Python內(nèi)核的問(wèn)題,需要的朋友可以參考下2022-02-02Django csrf 兩種方法設(shè)置form的實(shí)例
今天小編就為大家分享一篇Django csrf 兩種方法設(shè)置form的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-02-02