Chapter 14 組件模式 允許一個單一的實體跨越多個不同域而不會導致耦合。 為實現兩個類之間的代碼共用,應該讓他們擁有同一個類的實例,而不是繼承同一個類。 使用情境: 分割不同的域: 1 class InputComponent 2 { 3 4 public: 5 void update(Bj ...
Chapter 14 組件模式
允許一個單一的實體跨越多個不同域而不會導致耦合。
為實現兩個類之間的代碼共用,應該讓他們擁有同一個類的實例,而不是繼承同一個類。
使用情境:
- 有一個涉及多個域的類。但希望這些域保持解耦;
- 這個類很龐大;
- 希望定義許多共用不同能力的對象。
分割不同的域:
1 class InputComponent 2 { 3 4 public: 5 void update(Bjorn& bjorn) 6 { 7 switch (Controller::getJoystickDirection()) 8 { 9 case DIR_LEFT: 10 bjorn.velocity -= WALK_ACCELERATION; 11 break; 12 13 case DIR_RIGHT: 14 bjorn.velocity += WALK_ACCELERATION; 15 break; 16 } 17 } 18 19 private: 20 static const int WALK_ACCELERATION = 1; 21 22 }; 23 24 class PhysicsComponent 25 { 26 27 public: 28 void update(Bjorn& bjorn, World& world) 29 { 30 bjorn.x += bjorn.velocity; 31 world.resolveCollision(volume_, 32 bjorn.x, bjorn.y, bjorn.velocity); 33 } 34 35 private: 36 Volume volume_; 37 38 }; 39 40 class GraphicsComponent 41 { 42 43 public: 44 void update(Bjorn& bjorn, Graphics& graphics) 45 { 46 Sprite* sprite = &spriteStand_; 47 if (bjorn.velocity < 0) 48 { 49 sprite = &spriteWalkLeft_; 50 } 51 else if (bjorn.velocity > 0) 52 { 53 sprite = &spriteWalkRight_; 54 } 55 56 graphics.draw(*sprite, bjorn.x, bjorn.y); 57 } 58 59 private: 60 Sprite spriteStand_; 61 Sprite spriteWalkLeft_; 62 Sprite spriteWalkRight_; 63 64 };
最後的GameObject類:
1 class GameObject 2 { 3 4 public: 5 int velocity; 6 int x, y; 7 8 void update(World& world, Graphics& graphics) 9 { 10 input_.update(*this); 11 physics_.update(*this, world); 12 graphics_.update(*this, graphics); 13 } 14 15 private: 16 InputComponent input_; 17 PhysicsComponent physics_; 18 GraphicsComponent graphics_; 19 20 };
更進一步,抽象出InputComponent為基類,使得GameObject可以連接不同的Input組件:
1 class InputComponent 2 { 3 public: 4 virtual ~InputComponent() {} 5 virtual void update(Bjorn& bjorn) = 0; 6 }; 7 8 class PlayerInputComponent : public InputComponent 9 { 10 11 public: 12 virtual void update(Bjorn& bjorn) 13 { 14 switch (Controller::getJoystickDirection()) 15 { 16 case DIR_LEFT: 17 bjorn.velocity -= WALK_ACCELERATION; 18 break; 19 20 case DIR_RIGHT: 21 bjorn.velocity += WALK_ACCELERATION; 22 break; 23 } 24 } 25 26 private: 27 static const int WALK_ACCELERATION = 1; 28 29 }; 30 31 class DemoInputComponent : public InputComponent 32 { 33 34 public: 35 virtual void update(Bjorn& bjorn) 36 { 37 // AI to automatically control Bjorn... 38 } 39 };
其他組件也是如此:
1 class PhysicsComponent 2 { 3 public: 4 virtual ~PhysicsComponent() {} 5 virtual void update(GameObject& obj, World& world) = 0; 6 }; 7 8 class GraphicsComponent 9 { 10 public: 11 virtual ~GraphicsComponent() {} 12 virtual void update(GameObject& obj, Graphics& graphics) = 0; 13 }; 14 15 class BjornPhysicsComponent : public PhysicsComponent 16 { 17 public: 18 virtual void update(GameObject& obj, World& world) 19 { 20 // Physics code... 21 } 22 }; 23 24 class BjornGraphicsComponent : public GraphicsComponent 25 { 26 public: 27 virtual void update(GameObject& obj, Graphics& graphics) 28 { 29 // Graphics code... 30 } 31 };
然後是更靈活的GameObject類:
1 class GameObject 2 { 3 4 public: 5 int velocity; 6 int x, y; 7 8 GameObject(InputComponent* input, 9 PhysicsComponent* physics, 10 GraphicsComponent* graphics) 11 : input_(input), 12 physics_(physics), 13 graphics_(graphics) 14 {} 15 16 void update(World& world, Graphics& graphics) 17 { 18 input_->update(*this); 19 physics_->update(*this, world); 20 graphics_->update(*this, graphics); 21 } 22 23 private: 24 InputComponent* input_; 25 PhysicsComponent* physics_; 26 GraphicsComponent* graphics_; 27 28 }; 29 30 GameObject* createBjorn() 31 { 32 return new GameObject(new PlayerInputComponent(), 33 new BjornPhysicsComponent(), 34 new BjornGraphicsComponent()); 35 }
Chapter 15 事件隊列
對消息或事件的發送與受理進行時間上的解耦。
事件隊列給接受端的控制權:延遲處理、聚合請求或完全拋棄。發送端所能做的就是往隊列里投遞消息,無法有實時反饋的預期。
避免A到B到A的事件迴圈:不要在處理事件端的代碼里再發送事件。
Audio類:
1 class Audio 2 { 3 4 public: 5 static void init() 6 { 7 head_ = 0; 8 tail_ = 0; 9 } 10 11 // Methods... 12 13 private: 14 static int head_; 15 static int tail_; 16 17 static const int MAX_PENDING = 16; 18 19 static PlayMessage pending_[MAX_PENDING]; 20 }; 21 22 void Audio::playSound(SoundId id, int volume) 23 { 24 // Walk the pending requests. 25 for (int i = head_; i != tail_; 26 i = (i + 1) % MAX_PENDING) 27 { 28 if (pending_[i].id == id) 29 { 30 // Use the larger of the two volumes. 31 pending_[i].volume = max(volume, pending_[i].volume); 32 33 // Don't need to enqueue. 34 return; 35 } 36 } 37 38 assert((tail_ + 1) % MAX_PENDING != head_); 39 40 // Add to the end of the list. 41 pending_[tail_].id = id; 42 pending_[tail_].volume = volume; 43 tail_ = (tail_ + 1) % MAX_PENDING; 44 } 45 46 void Audio::update() 47 { 48 // If there are no pending requests, do nothing. 49 if (head_ == tail_) return; 50 51 ResourceId resource = loadSound(pending_[head_].id); 52 int channel = findOpenChannel(); 53 if (channel == -1) return; 54 startSound(resource, channel, pending_[head_].volume); 55 56 head_ = (head_ + 1) % MAX_PENDING; 57 }
- 環狀緩衝區;
- PlaySound只做元素入列,首先要判斷是否有重覆的,如果有就標記音量高的那個,tail增長時取餘;
- Update做元素取出,首先判斷是否有待取元素,head增長時取餘。
Chapter 16 服務定位器
為某服務提供一個全局訪問入口來避免使用者與該服務具體實現類之間產生耦合。
Audio服務:
1 class Audio 2 { 3 4 public: 5 virtual ~Audio() {} 6 7 virtual void playSound(int soundID) = 0; 8 virtual void stopSound(int soundID) = 0; 9 virtual void stopAllSounds() = 0; 10 11 };
Audio服務提供器:
1 class ConsoleAudio : public Audio 2 { 3 4 public: 5 virtual void playSound(int soundID) 6 { 7 // Play sound using console audio api... 8 } 9 10 virtual void stopSound(int soundID) 11 { 12 // Stop sound using console audio api... 13 } 14 15 virtual void stopAllSounds() 16 { 17 // Stop all sounds using console audio api... 18 } 19 };
包含空服務的定位器:
1 class Locator 2 { 3 4 public: 5 static void initialize() { service_ = &nullService_; } 6 7 static Audio& getAudio() { return *service_; } 8 9 static void provide(Audio* service) 10 { 11 if (service == NULL) 12 { 13 // Revert to null service. 14 service_ = &nullService_; 15 } 16 else 17 { 18 service_ = service; 19 } 20 } 21 22 private: 23 static Audio* service_; 24 static NullAudio nullService_; 25 26 };
註冊一個服務提供器:
1 ConsoleAudio *audio = new ConsoleAudio(); 2 Locator::provide(audio);
使用:
1 Audio *audio = Locator::getAudio(); 2 audio->playSound(VERY_LOUD_BANG);
調用playSound()的代碼對ConsoleAudio一無所知,它只知道Audio的抽象介面。Locator與ConsoleAudio也沒有耦合,代碼里唯一知道具體實現類的地方,是提供這個服務的代碼。
空服務:
1 class NullAudio: public Audio 2 { 3 public: 4 virtual void playSound(int soundID) { /* Do nothing. */ } 5 virtual void stopSound(int soundID) { /* Do nothing. */ } 6 virtual void stopAllSounds() { /* Do nothing. */ } 7 };
getAudio返回一個引用而不是指針,因為在C++里理論上一個引用不會為NULL,返回一個引用提示了使用者任何時候都能期望得到一個有效的對象。
空服務可以保證在Locator初始化之前使用它的安全性,也可以用來實現暫時禁用服務:
1 Locator::provide(NULL);
日誌裝飾器:
1 class LoggedAudio : public Audio 2 { 3 4 public: 5 LoggedAudio(Audio &wrapped) 6 : wrapped_(wrapped) 7 {} 8 9 virtual void playSound(int soundID) 10 { 11 log("play sound"); 12 wrapped_.playSound(soundID); 13 } 14 15 virtual void stopSound(int soundID) 16 { 17 log("stop sound"); 18 wrapped_.stopSound(soundID); 19 } 20 21 virtual void stopAllSounds() 22 { 23 log("stop all sounds"); 24 wrapped_.stopAllSounds(); 25 } 26 27 private: 28 void log(const char* message) 29 { 30 // Code to log message... 31 } 32 33 Audio &wrapped_; 34 35 };
1. 如果服務被限制在游戲的一個單獨域中,那就把服務的作用域限制到類中
eg.獲取網路訪問的服務就可能應該被限制在聯網的類中
2.廣泛使用的服務,作用域為全局域
eg.日誌服務應該是全局的