首页 > 代码库 > 《游戏编程模式》(6)
《游戏编程模式》(6)
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.日志服务应该是全局的
《游戏编程模式》(6)