文章目錄
- 奇美拉和隊列
- 奇美拉被動技能
- 多對多觀察者關系實現
- 自定義元類
- 奇美拉基類
- 管理奇美拉的隊列
- 奇美拉隊列類
- 心得體會
- 擴展
- 規則定義
- 工作相關
- 奇美拉相關
- 奇美拉屬性
在本篇博文,我將介紹本項目的整體框架,以及“編碼規則”,這些規則保證了本項目的結果和游戲中的實際結果的一致性。
關于游戲規則和奇美拉檔案見:
游戲開發實戰(一):Python復刻「崩壞星穹鐵道」嗷嗚嗷嗚事務所—源碼級解析該小游戲背后的算法與設計模式【純原創】-CSDN博客‘
后續見:
游戲開發實戰(三):Python復刻「崩壞星穹鐵道」嗷嗚嗷嗚事務所—源碼級解析該小游戲背后的算法與設計模式【純原創】-CSDN博客
項目githu地址 https://github.com/Hanachirusat/Chimeras
奇美拉和隊列
奇美拉被動技能
奇美拉的技能可以抽象為一種形式:滿足條件,觸發技能。因此我們可以非常自然的采用觀察者模式來實現被動技能方法的自動調用。奇美拉行動后,如果某個條件得到滿足,則通知該奇美拉的所有觀察者。由于每個奇美拉行動后都有可能使得某個可以觸發其他奇美拉被動技能的條件得到滿足,因此每個奇美拉是subject,也是observer,這是一個多對多的觀察關系。
通過對奇美拉的技能觸發條件進行分析,我們可以把技能分成兩類:自身觸發和隊友觸發。自身觸發條件中包含生命值降低這種屬性(變化)觸發,也包含同伴工作或追加工作這種動作觸發,此外還有登場技能和回合技。隊友觸發條件同理。因此從代碼實現層面,技能觸發可以分為屬性觸發和動作觸發。
屬性觸發很簡單,如果奇美拉的屬性發生了變化,則同時他的所有觀察者,由觀察者判斷該屬性的變化是否會觸發自身的技能。而動作觸發反映在代碼層面可能是奇美拉執行完某個方法后,通知他所有的觀察者。人都是喜歡偷懶的,那么有沒有把所有觸發條件統一起來的方法呢?
答案顯而易見,我們可以把動作觸發轉變為屬性觸發,例如同伴工作時觸發的技能,我們可以對奇美拉增加一個屬性:工作次數,當奇美拉工作后,該工作次數+1。這樣就可以把工作觸發(動作觸發)轉變為屬性觸發。同時我們把工作邏輯剝離出來(后續將解釋為什么),把每個奇美拉抽象為一個只有被動技能方法(用eval表示)的類(最終實現版本還是有其他方法的,在后面我們將一一介紹why,what以及how),并且該被動技能是自動觸發的。
最后,我們繼續簡化,不同奇美拉的技能觸發條件不同,這是不是意味著觀察者模式的實現很復雜?實則不然,我們可以讓奇美拉監控所有的屬性,然后在eval方法中判斷變化的屬性是否符合奇美拉技能觸發的條件。
多對多觀察者關系實現
基于上一小結的介紹,在本小節,我們將詳細描述如何利用元類來實現多對多的觀察者關系。
自定義元類
由于我們把所有的觸發條件都轉換為了屬性觸發,那么當奇美拉的任意屬性(可能觸發其他奇美拉被動的屬性)發生變化時,都應該通知觀察該奇美拉的觀察者。換句話說,我們需要控制每個屬性的set過程。自然而然的方法就是property裝飾器。我們在元類中遍歷所有的屬性,處理這些屬性get和set時的邏輯。即set時將通知所有的觀察者,屬性發生了變化。
class ObservableMeta(type):"""元類用于自動創建屬性觀察邏輯"""def __new__(cls, name, bases, dct):# 自動為可能觸發其他奇美拉技能的屬性創建觀察邏輯if '__observed_attrs__' in dct:attrs = dct['__observed_attrs__']for attr in attrs:# 為每個被觀察屬性生成屬性描述符private_attr = f"_{name}__{attr}"#下面的操作相當于對繼承該元類的類的私有屬性設置@property和@attr.setter(def getter(self, name=private_attr):return getattr(self, name)def setter(self, value, name=private_attr, attr=attr):old_value = getattr(self, name)if old_value != value:setattr(self, name, value)#修改屬性的時候,調用類的notify_observers方法# 通知所有的觀察者屬性發生了變化self.notify_observers(attr, old_value, value)dct[attr] = property(getter, setter)return super().__new__(cls, name, bases, dct)
在上述代碼中,由于我們把奇美拉的屬性定義為了私有屬性,因此我們需要修改訪問私有屬性的形式private_attr = f"_{name}__{attr}"
其中attr是私有屬性,private_attr是可以直接訪問私有屬性的形式(注:python中沒有真正意義上的私有屬性,私有屬性只不過實際換了個名字存儲而已,有興趣的可以詳細了解下)。我們在修改屬性值的時候,會記錄變化的屬性,舊值和新值。然后把這些值傳遞給觀察者。
奇美拉基類
接下來我們實現所有奇美拉的基類,在該基類中我們定義了增加和移除觀察者的方法,通知觀察者的方法以及析構函數
class ChimerasEntity(metaclass=ObservableMeta):"""所有可觀察對象的基類"""__observed_attrs__ = () # 需要子類指定要觀察的屬性def __init__(self, name):self.name = nameself._observers = weakref.WeakSet()self.queue = None # 所屬隊列的弱引用def add_observer(self, observer):if not isinstance(observer, ChimerasEntity):raise TypeError("觀察者必須是ObservedEntity類型")self._observers.add(observer)def remove_observer(self, observer):self._observers.discard(observer)#該方法在setter的時候被自動調用,get和set是所做的特殊操作由元類來實現def notify_observers(self, changed_attr, old_value, new_value):for observer in self._observers:observer.eval(self, changed_attr, old_value, new_value)def eval(self, changed_obj, changed_attr, old_value, new_value):#打印檢測信息print(f"[類名:{self.__class__.__name__},實例名:{self.name}] 檢測到變化:"f"【{changed_obj.__class__.__name__}】{changed_obj.name} 的"f"{changed_attr} 屬性從 {old_value} 變為 {new_value}")def __del__(self):# print(f"執行{self.__class__.__name__}的析構函數")queue_ = getattr(self, 'queue', None)if queue_ is not None:# print("從隊列中刪除")queue_.remove_member(self)self.queue=None
在上述代碼中,我們保存所有觀察者的弱引用,這個弱引用是一個集合的形式,無法保證順序。這可能就是本項目為什么和實際游戲中奇美拉的行動順序不一致的原因(注意:只是行動順序不一致,每回合的結果是一致的!后面我們會講為什么)。基類中的queue屬性表示的是奇美拉所屬的隊列(隊列的含義詳細請參考上一篇博文:游戲開發實戰(一):Python復刻「崩壞星穹鐵道」嗷嗚嗷嗚事務所—源碼級解析該小游戲背后的算法與設計模式【純原創】-CSDN博客,隊列的實現我將在后面詳細介紹)
在上述代碼中eval方法打印的是輔助判斷的信息,實際每個奇美拉都會有自己的eval方法(繼承重寫該方法)。由于后面我們在奇美拉隊列類(InteractionQueue)中保存的是所有奇美拉的強引用,因此該析構函數當且僅當該奇美拉已經從隊列中移除后才會調用。如果奇美拉隊列還保留著當前奇美拉對象的引用,直接del奇美拉對象,python解釋器并不會立刻執行該奇美拉對象的析構函數之后銷毀該對象。
也就是說,我們最終還是需要手動調用奇美拉隊列類的方法,把當前奇美拉對象從隊列中移除。我們在析構函數中所做的操作似乎是多余的!是的至少目前為止是多余的,該析構函數的執行只有當該析構函數中的操作都做了之后才會執行。為什么還要這么寫?
這是因為我們后續優化的時候需要把queue中的強引用列表和奇美拉基類中的弱引用集合都變為有序弱引用。優化后析構函數中的操作相當于一層保險,如果沒有主動調用奇美拉隊列中的移除方法,也不會造成內存泄漏(因為奇美拉隊列中是弱引用,并不會增加引用計數,del奇美拉對象的時候會正常執行奇美拉對象的析構函數從而從隊列中移除該奇美拉)。同時用有序弱引用也可以調整奇美拉行動順序從而和游戲中的順序保持一致。
管理奇美拉的隊列
奇美拉隊列類
我們用一個奇美拉隊列來維護當前奇美拉隊列,處理不同奇美拉的觀察邏輯。當每一個奇美拉入隊的時候,根據奇美拉技能觸發類型(自身觸發還是隊友觸發),構建這些奇美拉的觀察者序列。奇美拉隊列中記錄了奇美拉在隊伍中共的順序,奇美拉領隊和待完成的工作。
- 添加成員方法中將根據當前成員的觀察模式(也就是技能觸發的類型,觀察模式為2代表觀察所有包括自己,1代表進觀察同伴,0代表僅觀察自身),對不同的奇美拉對象添加管擦或者。注意處理完當前待添加的奇美拉對象后,還要處理已有奇美拉對象。
- 移除隊列方法遍歷當前所有奇美拉對象,并移除雙向觀察邏輯。
- 清空隊列方法,調用移除隊列方法清空對象。
- 添加和刪除領隊和添加和刪除奇美拉對象類似。
class InteractionQueue:"""管理相互觀察的隊列"""#1 觀察所有 2#2 觀察除了自己外的所有 1#3 只觀察自己 0def __init__(self):self.members_list =[]self.leader=Noneself.work=Noneself.episode=0def add_member(self, member):if not isinstance(member, ChimerasEntity):raise TypeError("成員必須是ObservedEntity類型")# 建立新成員和已有成員的相互觀察關系。# 判斷新成員是否觀察已有成員if member.mode>0:for existing_member in self.members_list:#觀察其他所有成員,所有要把當前成員添加到其他所有成員的觀察者列表中existing_member.add_observer(member)#2和0 判斷新成員是否觀察自身if member.mode%2==0:# 觀察自己和加入隊列member.add_observer(member) # 觀察自己#如果其他的成員的mode>0則表示其他成員要觀察新成員,即新成員的觀察者列表中要添加已有的成員for existing_member in self.members_list:if existing_member.mode > 0:member.add_observer(existing_member)self.members_list.append(member)#我們在每個奇美拉對象中記錄所在隊列的對象,方便后續的技能處理邏輯的實現。member.queue = selfdef remove_member(self, member):"""安全移除成員并清理觀察關系"""if member not in self.members_list:return# 清理雙向觀察關系,# current_member隊列中包含所有成員,即包含memberfor existing_member in self.members_list:# 其他成員不再觀察被移除者 existing_member.remove_observer(member)# 被移除者不再觀察其他成員member.remove_observer(existing_member)member.queue = None # 清除隊列引用# # 移除自我觀察# member.remove_observer(member)# 從隊列中移除self.members_list.remove(member)def clear(self):"""清空整個隊列"""for member in list(self.members_list):self.remove_member(member)# # 新成員觀察所有現有成員# for existing_member in self.members:# member.add_observer(existing_member)def add_leader(self,leader):if not isinstance(leader, ChimerasEntity):raise TypeError("成員必須是ObservedEntity類型")# 建立新成員和已有成員的相互觀察關系。# 判斷新成員是否觀察已有成員if leader.mode>0:for existing_member in self.members_list:#觀察其他所有成員,所有要把當前成員添加到其他所有成員的觀察者列表中existing_member.add_observer(leader)#2和0 判斷新成員是否觀察自身if leader.mode%2==0:# 觀察自己和加入隊列leader.add_observer(leader) # 觀察自己self.leader=leaderleader.queue = selfdef remove_leader(self,leader):"""安全移除成員并清理觀察關系"""if self.leader!=leader:print("領隊不在當前隊列中")returnfor existing_member in self.members_list:# 刪除領隊的觀察者身份(清空隊列中其他成員的觀察者弱引用)existing_member.remove_observer(leader)leader.queue = None # 清除隊列引用# 清空自身觀察,因為自身不在member_list中leader.remove_observer(leader)# 從隊列中移除self.leader=None
在上述奇美拉隊列類中,我們把領隊和工作奇美拉分開管理,方便后續奇美拉技能導致的隊列變動。
心得體會
用一個類對象來管理奇美拉對象有很多好處,剛開始沒考慮到的事情后面可以直接添加到奇美拉隊列類中,比如搶功勞奇美拉中需要判斷當前工作的進度,而在奇美拉對象中共無法直接獲得工作進度。可以選擇修改eval函數的接口,修改eval函數的接口就要修改基類的接口,導致需要修改其他已實現的代碼。因此我最后選擇直接在奇美拉隊列類中新增一個屬性,保存當前待完成的工作,因為每個奇美拉都保留了當前隊列的引用,可以直接獲得當前工作的進度。
擴展
用奇美拉隊列類來管理奇美拉隊列,開始時我們創建奇美拉對象然后添加到奇美拉隊列對象中,注意添加順序,最左面的奇美拉應該先添加。最后當任務完成后,我們需要調用奇美拉隊列對象的移除方法移除所有的奇美拉對象,然后del所有的奇美拉對象。對象刪除之類的操作在實際生產環境中是非常重要的。而在本項目中,所有奇美拉對象在主程序執行完畢后銷毀。
規則定義
這里的很多規則是在實現過程中不斷完善的,有了方便讀者閱讀和理解,在此我先給出所有的規則,之后再給出所有奇美拉類的具體實現。
工作相關
-
采用回合制,第一回合開始的時候統一設置奇美拉登場相關的屬性從而觸發奇美拉登場技能。后續的每回合都做如下操作:
- 每回合開始時回合數加1,以便觸發奇美拉每回合的技能。
- 固定奇美拉攻擊力,這點非常重要!!這保證了本項目的結果和游戲結果一致!!因為在實際游戲中,對于攻擊力的buff最后才會執行,因此我們固定攻擊力快照后,即使增加攻擊力的buff提前執行,也并不影響實際的結果!!!
- 第一個奇美拉工作。工作邏輯為先處理工作對象的hp,再處理當前對象的hp,判斷當前工作是否已經被完成,工作數量+1。先判斷工作是否被完成,再工作數+1是正確綁定工作完成關系的關鍵。因為當前奇美拉已經完成工作,并且先工作+1,那可能會觸發其他奇美拉的追加攻擊,導致程序判定觸發被動的奇美拉完成工作。(這涉及到后面的規則,為了和游戲結果保持一致,工作已經被完成也會繼續追加攻擊以便繼續加buff)。
- 判斷奇美拉hp是否小于1,如果小于1則離場。如果奇美拉的hp小于1并且代表登場的屬性為False,則奇美拉也離場(其他奇美拉的技能可能會導致奇美拉hp>0但是直接離場)
奇美拉相關
-
工作已經被完成也會繼續追加攻擊以便繼續加buff
-
奇美拉如果導致其他奇美拉直接離場,并不會修改離場奇美拉的hp,也不會從隊列中移除該奇美拉,而是修改該奇美拉對象中表示登場的屬性為False
這么做可能會出現一個問題,那就是游戲中奇美拉離場的動作優先級很高,而在本項目實現中,奇美拉離場邏輯在每回合最后才處理。這可能導致奇美拉離場和交換位置相關技能之間的聯動和游戲中的結果不一致。因此在本項目中,我們做出一致性規定:奇美拉一定在本回合工作結束,所有技能都觸發后才離場。交換位置的技能發動后,奇美拉可以和將要離場但是還未離場的奇美拉交換位置。
做出如上規定后,與交換位置相關的技能的代碼邏輯就變得簡單多了,我們只需要考慮當前位置即可,不需要考慮當前位置前后的奇美拉是否已經離場。
-
奇美拉技能發動的大前提一定是奇美拉在場!!這點和上面的2有聯動。
-
工作已經被完成后,奇美拉會繼續追加攻擊當前被完成的工作,以便吃到buff加成。
-
奇美拉技能執行期間,如果可能觸發其他技能,那應該保證當前需要處理的邏輯處理完成后再修改可能觸發技能的屬性。由于這種情況很少,因此我用這種簡單的方法保證”原子性“(這么說并不準確!!只是方便理解把)。比如如果奇美拉追加攻擊后,應該先判斷工作是否被完成再增加奇美拉追加工作的次數。因為增加追加工作次數可能觸發其他奇美拉的被動(同伴工作或者追加工作觸發的被動)
-
奇美拉體力降低到0時不會立刻退場,等到回合結束后統一離場=其他奇美拉技能可以增加hp)。同伴累到的判斷條件為奇美拉的hp小于0。因此可能會出現這么一種情況,同伴累到時的觸發的被動可能會被同一個奇美拉觸發多次,這點是否在游戲中也是如此本人并沒有驗證,在此指出)
-
在eval中,如果我們操作可能被監控的屬性,應該用非下劃線的形式,否則不會被監控到。在本項目中,只要屬性變化一定會調用觀察者的eval,因此我們需要準確處理eval中的邏輯,如果不滿足條件應該提前推出eval函數。(在最后的優化討論中,我將給出優化方向,奇美拉僅監控其他奇美拉會觸發技能的屬性,而不是監控所有屬性)
-
技能對同伴的操作一律不包含自身,例如使得同伴體力全部+8,不包括自身。我并沒有驗證游戲結果,而是僅從字面意思判斷,同伴肯定指的是自己之外的奇美拉。我自己是這么理解的,如果游戲中同伴也包括自身,則對應修改即可,本項目則堅持同伴指的是自己之外的奇美拉。
奇美拉屬性
self.name=name #奇美拉名字
self.__atk=1
self.__hp=1
self.__episode=0 #回合數,用于觸發回合技
self.__on=False #是否在場,True為登場,剛開始的時候為False
self.__work_num=0 #工作次數,用于觸發 【同伴工作后】的技能
self.__append_work_num=0 #追加攻擊次數,用于觸發 【同伴追加工作后】的技能
self.__mode=0 #監控規則,詳情見正文描述
self.__skill=True #奇美拉是否有技能
self.__complete_work_num = 0 #奇美拉完成工作次數,用于觸發【同伴完成工作】的技能
self.__fixed_atk=0 #每回合開始奇美拉的攻擊快照。保證了本項目結果和游戲結果的一致性。