網(wǎng)上有很多關(guān)于pos機(jī)代理找對(duì)象難,面向?qū)ο缶幊桃阉赖闹R(shí),也有很多人為大家解答關(guān)于pos機(jī)代理找對(duì)象難的問(wèn)題,今天pos機(jī)之家(www.yiliaobang.net)為大家整理了關(guān)于這方面的知識(shí),讓我們一起來(lái)看下吧!
本文目錄一覽:
pos機(jī)代理找對(duì)象難
【CSDN編者按】ECS(ECS,Entity–component–system,實(shí)體組件系統(tǒng),是一種主要用于游戲開(kāi)發(fā)的架構(gòu)模式),是在游戲開(kāi)發(fā)社區(qū)廣為流傳的偽模式,它基本上是關(guān)系模型的翻版,其中“實(shí)體”是ID,表示無(wú)形的對(duì)象,“組件”是特定表中的行,該行引用一個(gè)ID,而“系統(tǒng)”是更改組件的過(guò)程性的代碼。這種“模式”經(jīng)常會(huì)導(dǎo)致繼承的過(guò)度使用,而不會(huì)提及過(guò)度使用繼承,其實(shí)違反了OOP(OOP,Object Oriented Programming,面向?qū)ο缶幊蹋且环N計(jì)算機(jī)編程架構(gòu))原則。那么如何避免這種情況呢?本文作者,會(huì)給大家介紹下真正的設(shè)計(jì)指南。1.靈感這篇文章的靈感,來(lái)自最近Unity的知名工程師Aras Pranckevi?ius一次面向初級(jí)開(kāi)發(fā)者的公開(kāi)演講,演講的目的是讓他們熟悉新的“ECS”架構(gòu)的一些術(shù)語(yǔ)。
Aras使用了非常典型的模式,他展示了一些非常糟糕的OOP代碼,然后表示關(guān)系模型是個(gè)更好的方案(不過(guò)這里的關(guān)系模型稱為“ECS”)。我并不是要批評(píng)Aras,實(shí)際上我很喜歡他的作品,也非常贊賞他的演講!
我選擇他的演講而不是網(wǎng)上幾百篇關(guān)于ECS的其他帖子的原因是,他的演講給出了代碼,里面有個(gè)非常簡(jiǎn)單的小“游戲”用來(lái)演示各種不同的架構(gòu)。這個(gè)小項(xiàng)目節(jié)省了我很多精力,可以方便我闡述自己的觀點(diǎn),所以,謝謝Aras!
Aras幻燈片的鏈接:
http://aras-p.info/texts/files/2018Academy - ECS-DoD.pdf代碼鏈接:
https://github.com/aras-p/dod-playground我不想分析他的演講最后提出的ECS架構(gòu),只想就他批判的“壞的OOP”代碼來(lái)說(shuō)說(shuō)我的看法。我想論述的是,如果我們能改正所有違反OOD(面向?qū)ο笤O(shè)計(jì))原則的地方,會(huì)變成什么樣子。
劇透警告:改正違反OOD的代碼,能得到與Aras的ECS版本相似的性能改進(jìn),而且還能比ECS版本占用更少的內(nèi)存,代碼量也更少!
概括為一句話:如果你認(rèn)為OOP是垃圾、而ECS才是王道,那么先去了解一下OOD(即怎樣正確使用OOP),再學(xué)學(xué)關(guān)系模型(了解怎樣正確使用ECS)。
我一直很反感論壇上的許多關(guān)于ECS的帖子,部分原因是我覺(jué)得ECS夠不上單獨(dú)弄個(gè)術(shù)語(yǔ)的程度(劇透:它只不過(guò)是關(guān)系模型的專用版本),另一部分原因是所有宣揚(yáng)ECS模式的帖子、幻燈片或文章都有著同樣的結(jié)構(gòu):
展示一些很糟糕的OOP代碼,其設(shè)計(jì)很垃圾,通常是過(guò)度使用繼承(這一條就違反了許多OOD原則)。證明組合要比繼承更好(其實(shí)OOD早就這么說(shuō)過(guò))。證明關(guān)系模型很適合游戲開(kāi)發(fā)(只不過(guò)改名叫ECS)。這種結(jié)構(gòu)的文章很讓我惱火,因?yàn)椋?/p>偷換概念。它對(duì)比的對(duì)象風(fēng)馬牛不相及,這一點(diǎn)很難讓人信服,雖然可能是出于無(wú)意,卻也并不能證明它提出的新架構(gòu)更好。它會(huì)產(chǎn)生副作用,貶低知識(shí),并且無(wú)意間打擊讀者去學(xué)習(xí)該領(lǐng)域長(zhǎng)達(dá)五十多年的研究結(jié)果。關(guān)系模型第一次是在上世紀(jì)六十年代提出的。七八十年代深入研究了該模型的各個(gè)方面。新手經(jīng)常提出的問(wèn)題是“這個(gè)數(shù)據(jù)應(yīng)該放到哪個(gè)類里?”而該問(wèn)題的答案通常很模糊,“等你有了更多經(jīng)驗(yàn)以后自然而然就知道了”。但在七十年代,這個(gè)問(wèn)題深入地研究,并用通用的、正式的方式解決了,即數(shù)據(jù)庫(kù)的正規(guī)化(https://en.wikipedia.org/wiki/Database_normalization#Normal_forms)。忽略已有的研究成果把ECS當(dāng)作全新的方案來(lái)展示,就等于把這些知識(shí)藏起來(lái)不告訴新手程序員。
面向?qū)ο缶幊痰臍v史也同樣悠久(實(shí)際上比關(guān)系模型還要久,它的概念從上世紀(jì)五十年代就出現(xiàn)了)!但是,直到九十年代,OO才得到人們的關(guān)注,成了主流的編程范式。各種各樣的OO語(yǔ)言雨后春筍般地出現(xiàn),其中就包括Java和(標(biāo)準(zhǔn)版本的)C++。
但由于它是被炒作起來(lái)的,所以每個(gè)人只是把這個(gè)詞寫到自己的簡(jiǎn)歷上,真正懂得它的人少之又少。這些新語(yǔ)言引入了許多關(guān)鍵字來(lái)實(shí)現(xiàn)OO的功能,如CLASS、Virtual、extends、implements,我認(rèn)為自此OO分成了兩派。
后面我把擁有OO思想的編程語(yǔ)言稱為“OOP”,使用OO思想的設(shè)計(jì)和架構(gòu)技術(shù)稱為“OOD”。每個(gè)人學(xué)習(xí)OOP都很快,學(xué)校里也說(shuō)OO類非常高效,很適合新手程序員……但是,OOD的知識(shí)卻被拋在了后面。
我認(rèn)為,使用OOP的語(yǔ)言特性卻不遵循OOD設(shè)計(jì)規(guī)則的代碼,不是OO代碼。大多數(shù)反對(duì)OO的文章所攻擊的代碼都不是真正的OO代碼。
OOP代碼的名聲很差,其中部分原因就是大多數(shù)OOP代碼沒(méi)有遵循OOD原則,所以其實(shí)不是真正的OO代碼。
2.背景前面說(shuō)過(guò),上世紀(jì)九十年代是OO的大爆炸時(shí)代,那個(gè)時(shí)期的“壞OOP代碼”可能是最糟糕的。如果你在那個(gè)時(shí)期學(xué)習(xí)了OOP,那么你很可能學(xué)過(guò)下面的“OOP四大支柱”:
抽象封裝多態(tài)繼承我更傾向于稱他們?yōu)椤癘OP的四大工具”而不是四大支柱。這些工具可以用來(lái)解決問(wèn)題。但是,只學(xué)習(xí)工具的用法是不夠的,你必須知道什么時(shí)候應(yīng)該使用它們。
教育者只傳授工具的用法而不傳授工具的使用場(chǎng)景,是不負(fù)責(zé)任的表現(xiàn)。在二十一世紀(jì)初,第二波OOD思潮出現(xiàn),工具的濫用得到了一定的抑制。
當(dāng)時(shí)提出了SOLID(https://en.wikipedia.org/wiki/SOLID)思想體系來(lái)快速評(píng)價(jià)設(shè)計(jì)的質(zhì)量。注意其中的許多建議其實(shí)在上世紀(jì)九十年代就廣為流傳了,但當(dāng)時(shí)并沒(méi)有像“SOLID”這種簡(jiǎn)單好記的詞語(yǔ)將其提煉成五條核心原則……
單一職責(zé)原則(Single Responsibility Principle)。每個(gè)類應(yīng)該只有一個(gè)目的。如果類A有兩個(gè)目的,那么分別創(chuàng)建類B和類C來(lái)處理每個(gè)目的,再?gòu)腂和C中提煉出A。開(kāi)放/封閉原則(Open / Closed Principle)。軟件隨時(shí)都在變化(即維護(hù)很重要)。把可能會(huì)變化的部分放到實(shí)現(xiàn)(即具體的類)中,給不太可能會(huì)變化的東西建立接口(比如抽象基類)。里氏替換原則(Liskov Substitution Principle)。每個(gè)接口的實(shí)現(xiàn)都應(yīng)該100%遵循接口的要求,即任何能在接口上運(yùn)行的算法都應(yīng)該能在具體的實(shí)現(xiàn)上運(yùn)行。接口隔離原則(Interface Segregation Principle )。接口應(yīng)當(dāng)盡量小,保證每一部分代碼都“只需了解”最小量的代碼,也就是說(shuō)避免不必要的依賴。這一條建議對(duì)C++也很好用,因?yàn)椴蛔裱@條原則會(huì)讓編譯時(shí)間大幅增長(zhǎng)。依賴倒置原則(Dependency Inversion Principle)。兩個(gè)具體的實(shí)現(xiàn)直接通信并且互相依賴的模式,可以通過(guò)將兩者之間的通信接口正規(guī)化成第三個(gè)類,將這個(gè)類作為兩者之間的接口的方式解耦合。這第三個(gè)類可以是個(gè)抽象積累,定義兩者之間需要的調(diào)用,甚至可以只是個(gè)定義兩者間傳遞數(shù)據(jù)的簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)。這一條不在SOLID中,但我認(rèn)為這一條同樣重要:組合重用原則(Composite Reuse Principle)。默認(rèn)情況下應(yīng)當(dāng)使用組合,只有在必須時(shí)才使用繼承。這才是我們的SOLID C++。
接下來(lái)我用三字母的簡(jiǎn)稱來(lái)代表這些原則:SRP、OCP、LSP、ISP、DIP、CRP。
一點(diǎn)其他看法:
在OOD中,接口和實(shí)現(xiàn)并不對(duì)應(yīng)任何具體的OOP關(guān)鍵字。在C++中,接口通常用抽象類和虛函數(shù)建立,然后實(shí)現(xiàn)從基類繼承……但那只是實(shí)現(xiàn)接口的概念的一種方式而已。C++中能使用PIMPL(https://en.cppreference.com/w/cpp/language/pimpl)、不透明指針(https://en.wikipedia.org/wiki/Opaque_pointer)、鴨子類型(https://en.wikipedia.org/wiki/Duck_typing)、typedef等……你甚至可以創(chuàng)建OOD的設(shè)計(jì),然后用完全不支持OOP關(guān)鍵字的C語(yǔ)言實(shí)現(xiàn)!所以我這里說(shuō)的接口指的并不一定是虛函數(shù),而是隱藏實(shí)現(xiàn)的思想(https://en.wikipedia.org/wiki/Information_hiding)。接口可以是多態(tài)的(https://en.wikipedia.org/wiki/Polymorphism_(computer_science)),但大多數(shù)情況下并不是!好的多態(tài)非常罕見(jiàn),但任何軟件都會(huì)用到接口。上面說(shuō)過(guò),如果建立一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)負(fù)責(zé)從一個(gè)類傳遞數(shù)據(jù)到另一個(gè)類,那么該結(jié)構(gòu)就起到了接口的作用——用正式的語(yǔ)言來(lái)說(shuō),這叫數(shù)據(jù)定義(https://en.wikipedia.org/wiki/Data_definition_language)。即使只是將一個(gè)類分成了公有和私有兩部分,那么所有公有部分中的東西都是接口,而私有部分的都是實(shí)現(xiàn)。繼承實(shí)際上(至少)有兩種類型:接口繼承,實(shí)現(xiàn)繼承。在C++中,接口繼承包括:利用純虛函數(shù)實(shí)現(xiàn)的抽象基類、PIMPL、條件typedef。在Java中,接口繼承用implements關(guān)鍵字表示。在C++中,實(shí)現(xiàn)繼承發(fā)生在一切基類包含純虛函數(shù)以外的內(nèi)容的情況。在Java中,實(shí)現(xiàn)繼承用Extends關(guān)鍵字表示。OOD定義了許多關(guān)于接口繼承的規(guī)則,但實(shí)現(xiàn)繼承通常是不祥的預(yù)兆(https://en.wikipedia.org/wiki/Code_smell)。最后,我也許應(yīng)該給出一些糟糕的OOP教育的例子,以及這種教育導(dǎo)致的糟糕代碼(以及OOP的壞名聲)。
在學(xué)習(xí)層次結(jié)構(gòu)和繼承時(shí),你很可能學(xué)習(xí)過(guò)以下類似的例子:
假設(shè)我們有個(gè)學(xué)校的應(yīng)用,其中包括學(xué)生和教職工的名錄。于是我們可以用Person作為基類,然后從Person繼承出Student和Staff兩個(gè)類。這完全錯(cuò)了。先等一下。LSP(里氏替換原則)指出,類的層次結(jié)構(gòu)和操作它們的算法是共生(symbiotic)的。它們是一個(gè)完整程序的兩個(gè)部分。OOP是過(guò)程式編程的擴(kuò)展,它的主要結(jié)構(gòu)依然是過(guò)程。所以,如果不知道Student和Staff上的算法(以及哪些算法可以用多態(tài)來(lái)簡(jiǎn)化),那么設(shè)計(jì)類層次結(jié)構(gòu)是不負(fù)責(zé)任的。必須首先有算法和數(shù)據(jù)才能繼續(xù)。在學(xué)習(xí)層次結(jié)構(gòu)和繼承時(shí),你很可能學(xué)習(xí)過(guò)以下類似的例子:
假設(shè)你有個(gè)形狀的類。它的子類可以有正方形和矩形。那么,應(yīng)該是正方形is-a矩形,還是矩形is-a正方形?
這個(gè)例子其實(shí)很好地演示了實(shí)現(xiàn)繼承和接口繼承之間的區(qū)別。
如果你考慮的是實(shí)現(xiàn)繼承,那么你完全沒(méi)有考慮LSP,只不過(guò)是把繼承當(dāng)做復(fù)用代碼的工具而已。從這個(gè)觀點(diǎn)來(lái)看,下面的定義是完全合理的: struct Square { int width="360px",height="auto" />
你一定猜到了,OOD認(rèn)為這種設(shè)計(jì)(很可能)錯(cuò)了。我說(shuō)可能的原因是你還可以爭(zhēng)論其中暗含的接口……不過(guò)這無(wú)關(guān)緊要。
正方形的寬度和高度永遠(yuǎn)相同,所以從正方形的接口的角度來(lái)看,我們完全可以認(rèn)為它的面積是“寬度×寬度”。
如果矩形從正方形繼承,那么根據(jù)LSP,矩形必須遵守正方形接口的規(guī)則。所有能在正方形上正確工作的算法必須能在矩形上正確工作。
比如下面的算法:std::vector<Square*> shapes; int area = 0; for (auto s: shapes) area += s->width="360px",height="auto" />如果用接口繼承的方式來(lái)思考,那么無(wú)論是正方形還是矩形,都不應(yīng)該從對(duì)方繼承。正方形和矩形的接口實(shí)際上是不同的,誰(shuí)都不是誰(shuí)的超集。所以,OOD實(shí)際上并不鼓勵(lì)實(shí)現(xiàn)繼承。前面說(shuō)過(guò),如果你要復(fù)用代碼,OOD認(rèn)為應(yīng)該使用組合!所以,上面實(shí)現(xiàn)繼承的層次結(jié)構(gòu)代碼的正確版本,用C++來(lái)寫應(yīng)該是這樣:struct Shape { virtual int area() const = 0; };struct Square : public virtual Shape { virtual int area() const { return width="360px",height="auto" />
public virtual相當(dāng)于Java中的implements,在實(shí)現(xiàn)一個(gè)接口時(shí)使用。private可以讓你從基類繼承,而無(wú)需繼承它的接口。在本例中,Rectangle is-not-a Square,雖然它繼承了Square。我不推薦這樣寫代碼,但如果你真想使用實(shí)現(xiàn)繼承,那么這才是正確的寫法!總之一句話,OOP課程教給你什么是繼承,而你沒(méi)有學(xué)習(xí)的OOD課程本應(yīng)教給你在99%的情況下不要使用繼承!
3.實(shí)體 / 組件框架有了這些背景之后,我們來(lái)看看Aras開(kāi)頭提出的那些所謂的“常見(jiàn)的OOP”。
實(shí)際上我還要說(shuō)一句,Aras稱這些代碼為“傳統(tǒng)的OOP”,而我并不這樣認(rèn)為。這些代碼也許是人們常用的OOP,但如上所述,這些代碼破壞了所有核心的OO規(guī)則,所以它們完全不是傳統(tǒng)的OOP。
我們從最早的提交開(kāi)始——當(dāng)時(shí)他還沒(méi)有把設(shè)計(jì)修改成ECS:"Make it work on Windows again"(https://github.com/aras-p/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cpp):
class GameObject;class Component;typedef std::vector<Component*> ComponentVector;typedef std::vector<GameObject*> GameObjectVector;class Component{public: Component() : m_GameObject(nullptr) {} virtual ~Component() {} virtual void Start() {} virtual void Update(double time, float deltaTime) {} const GameObject& GetGameObject() const { return *m_GameObject; } GameObject& GetGameObject() { return *m_GameObject; } void SetGameObject(GameObject& go) { m_GameObject = &go; } bool HasGameObject() const { return m_GameObject != nullptr; }private: GameObject* m_GameObject;};class GameObject{public: GameObject(const std::string&& name) : m_Name(name) { } ~GameObject() { for (auto c : m_Components) delete c; } template<typename T> T* GetComponent() { for (auto i : m_Components) { T* c = dynamic_cast<T*>(i); if (c != nullptr) return c; } return nullptr; } void AddComponent(Component* c) { assert(!c->HasGameObject()); c->SetGameObject(*this); m_Components.emplace_back(c); } void Start() { for (auto c : m_Components) c->Start(); } void Update(double time, float deltaTime) { for (auto c : m_Components) c->Update(time, deltaTime); } private: std::string m_Name; ComponentVector m_Components;};static GameObjectVector s_Objects;template<typename T>static ComponentVector FindAllComponentsOfType(){ ComponentVector res; for (auto go : s_Objects) { T* c = go->GetComponent<T>(); if (c != nullptr) res.emplace_back(c); } return res;}template<typename T>static T* FindOfType(){ for (auto go : s_Objects) { T* c = go->GetComponent<T>(); if (c != nullptr) return c; } return nullptr;}
OK,代碼很難一下子看懂,所以我們來(lái)分析一下……不過(guò)還需要另一個(gè)背景:在上世紀(jì)九十年代,使用繼承解決所有代碼重用問(wèn)題,這在游戲界是通用的做法。首先有個(gè)Entity,然后擴(kuò)展成Character,再擴(kuò)展成Player和Monster等等……
如前所述,這是實(shí)現(xiàn)繼承,盡管一開(kāi)始看起來(lái)不錯(cuò),但最后會(huì)導(dǎo)致極其不靈活的代碼。因此,OOD才有“使用組合而不是繼承”的規(guī)則。因此,在本世紀(jì)初“使用組合而不是繼承”的規(guī)則變得流行后,游戲開(kāi)發(fā)才開(kāi)始寫這種代碼。
這段代碼實(shí)現(xiàn)了什么?總的來(lái)說(shuō)都不好,呵呵。
簡(jiǎn)單來(lái)說(shuō),這段代碼通過(guò)運(yùn)行時(shí)函數(shù)庫(kù)重新實(shí)現(xiàn)了組合的功能,而不是利用語(yǔ)言特性來(lái)實(shí)現(xiàn)。
你可以認(rèn)為,這段代碼在C++之上構(gòu)建了一種新的語(yǔ)言,以及運(yùn)行這種語(yǔ)言的編譯器。Aras的示例游戲并沒(méi)有用到這段代碼(我們一會(huì)兒就會(huì)把它都刪掉了!),它唯一的用途是將游戲的性能降低10倍。
它實(shí)際上做了什么?這是個(gè)“實(shí)體/組件”(Entity/Component)框架(有時(shí)候會(huì)被誤稱為“實(shí)體/組件系統(tǒng)”),但它跟“實(shí)體組件系統(tǒng)”(Entity Component System)框架完全沒(méi)關(guān)系(后者很顯然不會(huì)被稱為“實(shí)體組件系統(tǒng)”)。
游戲從一個(gè)無(wú)功能的“實(shí)體”開(kāi)始(本例中稱為GameObject),這些實(shí)體自身由“組件”(Component)構(gòu)成。GameObject實(shí)現(xiàn)了服務(wù)定位器模式(Service Locator Pattern,https://en.wikipedia.org/wiki/Service_locator_pattern),這種模式可以通過(guò)類型查詢子組件。Component知道自己屬于哪個(gè)GameObject,它們可以通過(guò)查詢父GameObject來(lái)定位兄弟組件。組合僅限于單層(Component不能擁有子組件,GameObject也不能擁有子GameObject)。GameObject只能有各種類型的組件各一個(gè)(有些框架要求這一點(diǎn),有些不要求)。所有組件(可能)都會(huì)以未知的方式改變,因此接口定義為“virtual void Update”。GameObject屬于場(chǎng)景,場(chǎng)景可以查詢所有GameObject(因此可以繼續(xù)查詢所有Component)。這種框架在本世紀(jì)初非常流行,盡管它很嚴(yán)格,但提供了足夠的靈活性來(lái)支持無(wú)數(shù)的游戲,直到今天依然如此。
但是,這種框架并不是必須的。編程語(yǔ)言的特性中已經(jīng)提供了組合,不需要再用框架實(shí)現(xiàn)一遍……那為什么還需要這些框架?那是因?yàn)榭蚣芸梢詫?shí)現(xiàn)動(dòng)態(tài)的、運(yùn)行時(shí)的組合。
GameObject無(wú)須硬編碼,可以從數(shù)據(jù)文件中加載。這樣游戲設(shè)計(jì)師和關(guān)卡設(shè)計(jì)師就可以創(chuàng)建自己的對(duì)象……但是,在大多數(shù)游戲項(xiàng)目中,項(xiàng)目的設(shè)計(jì)師都很少,而程序員很多,所以我認(rèn)為這并不是關(guān)鍵的功能。何況,還有許多其他方式來(lái)實(shí)現(xiàn)運(yùn)行時(shí)組合!
例如,Unity使用C#作為其“腳本語(yǔ)言”,許多其他游戲使用Lua等替代品,所以面向設(shè)計(jì)師的工具可以生成C#/Lua代碼來(lái)定義新的游戲?qū)ο螅恍枰@些框架!
我們會(huì)在以后的文章里重新加入運(yùn)行時(shí)組合的“功能”,但要同時(shí)避免10倍的性能開(kāi)銷……
如果我們用OOD的觀點(diǎn)評(píng)價(jià)這段代碼:
GameObject:GetComponent使用了dynamic_cast。大多數(shù)人都會(huì)告訴你,dynamic_cast是一種代碼異味——它強(qiáng)烈地暗示著代碼什么地方有問(wèn)題。我認(rèn)為,它預(yù)示著你的代碼違反了LSP——某個(gè)算法在操作基類的解耦,但它要求了解不同實(shí)現(xiàn)的細(xì)節(jié)。這正是代碼異味的原因。GameObject還算可以,如果認(rèn)為它實(shí)現(xiàn)了服務(wù)定位器模式的話……但是從OOD的觀點(diǎn)來(lái)看,這種模式在項(xiàng)目的不同部分之間建立了隱含的聯(lián)系,而且我認(rèn)為(我找不到能用計(jì)算機(jī)科學(xué)的知識(shí)支持我的維基鏈接)這種隱含的通信通道是一種反面模式(https://en.wikipedia.org/wiki/Anti-pattern),應(yīng)當(dāng)使用明示的通信通道。這種觀點(diǎn)同樣適用于一些游戲中使用的“事件框架”……我認(rèn)為,Component違反了SRP(單一責(zé)任原則),因?yàn)樗慕涌冢?virtual void Update(time))太寬泛了。“virtual void Update”在游戲開(kāi)發(fā)中非常普遍,但我還是要說(shuō)這是個(gè)反面模式。好的軟件應(yīng)該可以很容易地論證其控制流和數(shù)據(jù)流。將一切游戲代碼放在“virtual void Update”調(diào)用后面完全混淆了控制流和數(shù)據(jù)流。在我看來(lái),不可見(jiàn)的副作用(https://en.wikipedia.org/wiki/Side_effect_(computer_science))——也稱為“遠(yuǎn)隔作用”(https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)——是最常見(jiàn)的Bug來(lái)源,而“virtual void Update”使得一切都擁有不可見(jiàn)的副作用。盡管Component類的目的是實(shí)現(xiàn)組合,但它是通過(guò)繼承實(shí)現(xiàn)的,這違反了CRP(組合重用原則)。這段代碼好的一方面在于,它滿足了SRP和ISP(接口隔離原則),分割出了大量的簡(jiǎn)單組件,每個(gè)組件的責(zé)任非常小,這一點(diǎn)非常適合代碼重用。
但是,它在DIP(依賴反轉(zhuǎn)原則)方面做得不好,許多組件都互相了解對(duì)方。
所以,我上面貼出的所有代碼實(shí)際上都可以刪掉了。整個(gè)框架都可以刪掉。刪掉GameObject(即其他框架中的Entity),刪掉Component,刪掉Find Of Type。這些都是無(wú)用的VM中的一部分,破壞了OOD的規(guī)則,使得游戲變得非常慢。
4.無(wú)框架組合(即使用編程語(yǔ)言的功能實(shí)現(xiàn)組合)如果刪掉整個(gè)組合框架,并且沒(méi)有Component基類,我們?cè)鯓硬拍苁褂媒M合來(lái)管理GameObject呢?
我們不需要寫VM再在我們自己的奇怪的語(yǔ)言之上實(shí)現(xiàn)GameObject,我們可以使用C++自身的功能來(lái)實(shí)現(xiàn),因?yàn)檫@就是我們游戲程序員的工作。
下面的提交中刪除了整個(gè)實(shí)體/組件框架:
https://github.com/hodgman/dod-playground/commit/f42290d0217d700dea2ed002f2f3b1dc45e8c27c下面是原始版本的代碼:
https://github.com/hodgman/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cpp下面是改進(jìn)后的代碼:
https://github.com/hodgman/dod-playground/blob/f42290d0217d700dea2ed002f2f3b1dc45e8c27c/source/game.cpp這段改動(dòng)包括:
從每個(gè)組件類型中刪掉了“: public Component”。給每個(gè)組件類型添加了構(gòu)造函數(shù)。OOD的主旨是封裝類的狀態(tài),但這些類非常小、非常簡(jiǎn)單,所以沒(méi)有太多東西需要隱藏,它的接口只是數(shù)據(jù)描述而已。但是,封裝成為面向?qū)ο笾е闹饕蛑皇牵梢宰岊惒蛔兞浚╟lass invariant,https://en.wikipedia.org/wiki/Class_invariant)永遠(yuǎn)為真……或者說(shuō),在違反某個(gè)不變量時(shí),你只需要檢查封裝的實(shí)現(xiàn)代碼就能找到Bug。在這段示例代碼中,我們值得添加一個(gè)構(gòu)造函數(shù)來(lái)確保一個(gè)簡(jiǎn)單的不變量,即所有值必須被初始化。我將過(guò)于通用的“Update”方法改名,使之能夠反映出實(shí)際功能,比如MoveComponent的叫做Update Position,Avoid Component的叫做Resolve Collisions。我刪掉了三段有關(guān)模板和預(yù)制組件(Prefab)硬編碼的代碼,即創(chuàng)建包含特定Component類型的GameObject代碼,并用三個(gè)C++類來(lái)代替。修正了“virtual void Update”反面模式。不再讓組件通過(guò)服務(wù)定位器模式互相查找,而是讓GameObject在構(gòu)造過(guò)程中直接鏈接組件。5.對(duì)象這樣,我們不再使用下面的“VM”代碼:
for (auto i = 0; i < kObjectCount; ++i) { GameObject* go = new GameObject("object"); PositionComponent* pos = new PositionComponent(); pos->x = RandomFloat(bounds->xMin, bounds->xMax); pos->y = RandomFloat(bounds->yMin, bounds->yMax); go->AddComponent(pos); SpriteComponent* sprite = new SpriteComponent(); sprite->colorR = 1.0f; sprite->colorG = 1.0f; sprite->colorB = 1.0f; sprite->spriteIndex = rand() % 5; sprite->scale = 1.0f; go->AddComponent(sprite); MoveComponent* move = new MoveComponent(0.5f, 0.7f); go->AddComponent(move); AvoidComponent* avoid = new AvoidComponent(); go->AddComponent(avoid); s_Objects.emplace_back(go); }
而是使用正常的C++實(shí)現(xiàn):
struct RegularObject{ PositionComponent pos; SpriteComponent sprite; MoveComponent move; AvoidComponent avoid; RegularObject(const WorldBoundsComponent& bounds) : move(0.5f, 0.7f) , pos(RandomFloat(bounds.xMin, bounds.xMax), RandomFloat(bounds.yMin, bounds.yMax)) , sprite(1.0f, 1.0f, 1.0f, rand() % 5, 1.0f) { }};... regularObject.reserve(kObjectCount);for (auto i = 0; i < kObjectCount; ++i) regularObject.emplace_back(bounds);6.算法
現(xiàn)在另一個(gè)難題是算法。還記得開(kāi)始時(shí)我說(shuō)過(guò),接口和算法是共生(Symbotic)的,兩者應(yīng)該互相影響對(duì)方的設(shè)計(jì)嗎?“virtual void Update”反面模式也不適合這種情況。原始的代碼有個(gè)主循環(huán)算法,它的結(jié)構(gòu)如下:
for (auto go : s_Objects) { go->Update(time, deltaTime);
你可能會(huì)認(rèn)為這段代碼很簡(jiǎn)潔,但我認(rèn)為這段代碼很糟糕。它完全混淆了游戲中的控制流和數(shù)據(jù)流。
如果我們想理解軟件,維護(hù)軟件,給軟件添加新功能,優(yōu)化軟件,甚至想讓它能在多個(gè)CPU核心上運(yùn)行得更快,那么我們必須理解控制流和數(shù)據(jù)流。所以,“virtual void Update”不應(yīng)該出現(xiàn)。
相反,我們應(yīng)該使用更明確的主循環(huán),才能讓論證控制流更容易(這里數(shù)據(jù)流依然被混淆了,我們會(huì)在稍后的提交中解決)。
for (auto& go : s_game->regularObject) { UpdatePosition(deltaTime, go, s_game->bounds.wb); } for (auto& go : s_game->avoidThis) { UpdatePosition(deltaTime, go, s_game->bounds.wb); } for (auto& go : s_game->regularObject) { ResolveCollisions(deltaTime, go, s_game->avoidThis); }
這種風(fēng)格的缺點(diǎn)是,每加入一個(gè)新類型的對(duì)象,就要在主循環(huán)中添加幾行。我會(huì)在以后的文章中解決這個(gè)問(wèn)題。
7.性能現(xiàn)在代碼中仍然有違反OOD的地方,有一些不好的設(shè)計(jì)抉擇,還有許多可以優(yōu)化的地方,但這些問(wèn)題我會(huì)在以后的文章中解決。
至少在目前來(lái)看,這個(gè)“改正后的OOD”版本的性能不弱于Aras演講中最后的ECS版本,甚至可能超過(guò)它……
而我們所做的只是將偽OOP代碼刪除,并使用真正遵守OOP規(guī)則的代碼而已(并且刪除了100多行代碼!)。
9.下一步我還想談更多的問(wèn)題,包括解決殘余的OOD問(wèn)題、不可更改的對(duì)象(函數(shù)式風(fēng)格編程,https://en.wikipedia.org/wiki/Functional_programming),以及對(duì)數(shù)據(jù)流、消息傳遞的論證能帶來(lái)的好處。
并給我們的OOD代碼添加一些DOD論證,給OOD代碼添加一些關(guān)系型技巧,刪掉那些“實(shí)體”類并得到純粹由組件組成的、以不同風(fēng)格互相鏈接的組件(指針 VS 事件處理),真實(shí)世界的組件容器,加入更多優(yōu)化以跟上ECS版本,以及更多Aras的演講中都沒(méi)有提到的優(yōu)化(如線程和SIMD)。所以,敬請(qǐng)期待我后續(xù)的文章……
原文:https://www.gamedev.net/blogs/entry/2265481-oop-is-dead-long-live-oop/作者:Brooke Hodgman,獨(dú)立游戲、圖形和引擎程序員,現(xiàn)居墨爾本,在GOATi Enterainment的22series.com工作譯者:彎月,責(zé)編:胡巍巍以上就是關(guān)于pos機(jī)代理找對(duì)象難,面向?qū)ο缶幊桃阉赖闹R(shí),后面我們會(huì)繼續(xù)為大家整理關(guān)于pos機(jī)代理找對(duì)象難的知識(shí),希望能夠幫助到大家!
