您好,登錄后才能下訂單哦!
0.背景
在游戲的UI中,往往會出現這樣的情況:
在某個戰斗副本中獲得了某個道具A,那么當進入主界面的時候,你會看到你的背包UI上有個小紅點(意思是有新道具),點擊進入背包后,發現新增了道具A,顯示個數為1,并且在下個界面中有個使用的按鈕由灰色不可使用變成橙色的可使用狀態
圖1. 事件觸發說明圖
其中這里是由道具獲得這個事件,觸發了上述的三個行為。如果使用顯示調用行為,會使得代碼難擴展,易出錯,邏輯混亂等問題,如果使用Event回調機制,就會變得十分方便。
其實Event回調機制就是觀察者模式,如下圖:
圖2. 觀察者模式
在C#中存在(delegate & event)的語義來實現Event回調機制:具體使用如下:
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,可以在注冊事件的回調時,帶入一個參數callback,在注冊函數實體內,存在一個list將callback添加進去,形如:
def register_callback(self, cb): self.callbacks.append(cb)
但是這樣是一個最為普遍的做法,既然是Python,這里我們有更Pythonic的做法,而且相比于上述的觀察者模式,它的做法更加簡潔,使用更加方便,接下來我們來解析一下Python實現Event callback的步驟。
1. UML類圖
上述案例中,是針對游戲客戶端UI的案例。所以我們呈現出的UML圖也是與UI相關。如圖3所示,它顯示了Python中實現Event回調的機制。
圖3. UML關系圖
如上圖所示,此機制主要由三個類及他們的實例(instance)組成:UIBase, UIScene, UIDataEvent。
1 . UIBase: 所有UIScene的基類,其實例有scene_id變量,包含兩個必要的方法, __init__ 是初始化方法,init_data_listeners方法是將實例中的某些方法, 例如ui_updata_func中包含的UIDataEvent實例(所有的UIDataEvent實例都是單例)遍歷,并把ui_update_func注冊在每一個UIDataEvent實例中。
2 . UIScene: 場景類,管理某個場景的UI渲染。在其實例中,存在某些方法,例如ui_update_func需要在某些UIDataEvent實例觸發時候,也被同時觸發調用。ui_update_func在Python中一個bound method object, 它會擁有一個特殊的屬性events,即所有需要觸發此方法的UIDataEvent實例集合。這個通過裝飾器(decorator)來實現,即圖中的:
“ui_update_func” is a Python object which add a amount of UIDataEvent instances by Python decorator named “data_listener”
3 . UIDataEvent: 事件類,該類有個類變量_events, 記錄了所有的UIDataEvent實例,每一個UIDataEvent實例都是單例,而且都有一個名字,和一個回調方法集合_callbacks, 里面的每一個方法都是在本事件觸發后需要回調的方法。實例還有個__iadd__方法,將需要回調的函數cb注冊進去。__call__事件觸發是實際觸發的函數。
2. 代碼
上一步講述了三個類之間的聯系與各自的作用,此步展示代碼實現相關功能。
a) UIBase.py
首先列出來的是UIBase的類,除了上述的__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): """為所有標有@data_listener的成員函數注冊事件監聽器""" 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
其實源碼的意思就是,在dir(object)的value中找,找到能夠滿足predicate(value) == True的value,然后將(key, value)收集,進行排序后返回。
放在代碼的意思是:
for listener_name, listener in inspect.getmembers(self, lambda f: hasattr(f, 'events')): for event in listener.events: event += listener
在dir(scene)中找,找到value中存在名叫events的屬性, 返回得到是一個list,每個list的元素是一個二元tuple: (key, value),其中key,即listener_name是dir(scene)的屬性名,而value, 即listener就是屬性對象,這里其實就是包含事件的函數對象,然后遍歷listener中的每一個UIDataEvent實例,并將listener注冊到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中只是要填寫對于OnItemAdded這個事件觸發之后,需要回調的函數,上述代碼中寫了三個函數。注意需要在函數上加上裝飾器@data_listener(OnItemAdded),這樣此函數就會添加一個特殊的屬性events,具體裝飾器的代碼見UIDataNotifier.py。
最后新建一個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): """清空所有事件上的所有監聽器,在銷毀一個界面的時候調用""" for event in cls._events: event._cb = [] OnItemAdded = UIDataEvent('OnItemAdded')
data_listener裝飾器其實就是聲明一個特殊的events屬性,并將所有在UIScene中填寫的UIDataEvent實例元組集合賦值給它。
__iadd__是將參數cb添加到實例的變量中_callbacks中,此方法在UIBase的init_data_listeners中使用。
__call__是當UIDataEvent實例自調用時,例如OnItemAdded(item),實際調用的函數,在函數體里,會回調_callbacks中的每個方法,這也就是Event回調機制的核心部分,相當于觀察者模式的notify方法
最后新建一個OnItemAdded事件。
c) client.py
創建上述幾個類之后,使用Event回調就非常簡單了,代碼如下:
# -*- coding: utf-8 -*- from UIScene import UIScene from UIDataNotifier import * OnItemAdded('liu_xin_biao') #新道具流星鏢獲得事件發生了
輸出:
ui_render_avaliable_use_btn ui_render_new_tool: liu_xin_biao ui_render_red_point
3.使用方法
1. 在本模塊內增加一個事件定義,并在注釋中寫明事件的參數及意義。
如果要監聽一個事件,請仔細閱讀相關注釋。
2. 在ui類最頂端import需要的事件及data_listener。
3. 在需要響應該事件的方法(監聽器方法)前增加裝飾器@data_listener,參數內列出要監聽的所有事件。
如:
@data_listener(OnEventA, OnEventB) def my_listener_method(arg1): ...
注意保持監聽器方法的參數個數及意義與事件觸發的地方一致。
4. 在邏輯代碼中適當的位置對事件進行觸發。如OnEventA(arg1, ...)
注意:并不是所有與UI的交互都必須使用事件,事件機制是為了方便多對多的交互。比如背包物品改變事件,有多個UI都會監聽背包物品的變化,而有多種邏輯都會導致背包物品變化,這時使用事件就比較方便。
4. 總結
本文主要講述了如何使用Python實現Event回調機制,上述的示例代碼參考我的[github-EventCallBack] (https://github.com/csdz/SnapToSnap/tree/master/EventCallBack)。
以上這篇Python實現Event回調機制的方法就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。