架构,性能和游戏
"YAGNI" You aren't gonna need it (你不需要那个)
Blaise Pascal有向著名的信件结尾,“我没时间写的更短。”
另一向名言来自Antoine de Saint Exupery:“臻于完美之时,不是加无可加,而是减无可减。
重访设计模式
命令模式
命令是具现化的方法调用。
称命令模式为“具现化方法调用”,意思是方法调用被存储在对象中。
class Command { public: virtual ~Command() {} virtual void execute(GameActor& actor) = 0; virtual void undo() = 0; };
class Jumpcommand : public Command { public: virtual void execute(GameActor& actor) { actor.jump(); } virtual void undo() { } };
class InputHandler { public: Command* handleInput();
private: Command* buttonX; Command* buttonY; Command* buttonA; Command* buttonB; };
Command* InputHandler ::handleInput() { if (isPressed(Button_X)) return buttonX; else if (isPressed(Button_Y)) return buttonY; else if (isPressed(Button_A)) return buttonA; else if (isPressed(Button_B)) return buttonB; return NULL; }
int main() { Command* command = inputHandler.handleInput(); if (command) { command->execute(actor); } }
|
享元模式
将共有数据拿出来分离到一个类中
class TreeModel { private: Mesh mesh; Texture bark; Texture leaves; };
class Tree
{ private: TreeModel* model;
Vector position; double height; double thickness; Color barkTint; Color leafrint; };
|
观察者模式
成就系统?
观察者模式与事件系统的区别 观察者模式: 观察做了有趣事情的事物 事件系统: 观察的对象代表了发生的有趣事情
class Observer {
public: virtual ~Observer() {} virtual void onNotify(const Entity& entity, Event event) = 0; };
class Achievements : public Observer { public: virtual void onNotify(const Entity& entity, Event event) { switch (event) { case EVENT_ENTITY_FELL: if (entity.isHero() && heroIsOnBridge_) { unlock(ACHIEVEMENT_FELL_OFF_BRIDGE); } break; } }
private: void unlock(Achievements achievement) {}
bool heroIsOnBridge_; };
class Subject { private: Observer* observers[MAX_OBSERVERS]; int numObservers_;
public: void addobserver(Observer* observer) {}
void removeObserver(Observer* observer) {}
protected: void notify(const Entity& entity, Event event) { for (int i = 0; i < numObservers_; i++) { observers[i]->onNotify(entity, event); } } };
class Physics : public Subject { public: void updateEntity(Entity& entity); }; physics.entityFell().addObserver(this);
|
原型模式
思路: 一个对象可以产生出与他自己相近的对象
class Monster { public: virtual ~Monster() {} virtual Monster* clone() = 0; };
class Ghost : public Monster { public: Ghost(int health, int speed) : health_(health), speed_(speed) {}
virtual Monster* clone() { return new Ghost(health_, speed_); }
private: int health_; int speed_; };
typedef Monster* (*SpawnCallback)();
class Spawner { public: Spawner(SpawnCallback spawn) : spawn_(spawn) {}
Monster* spawnMonster() { return spawn_(); }
private: SpawnCallback spawn_; }; Spawner* ghostSpawner = new Spawner(spawnGhost);
class Spawner_template { public: virtual ~Spawner_template() {} virtual Monster* spawnMonster() = 0; };
template <class T> class SpawnerFor : public Spawner_template { public: virtual Monster* spawnMonster() { return new T(); } };
Spawner_template* ghostSpawner2 = new SpawnerFor<Ghost>();
|
单例模式
保证一个类只有一个实例, 并且提供了访问该实例的全局访问点
劣势: 有全局变量, 理解函数更加困难, 促进了耦合的发生, 不利于并行
class FileSystem { public: static FileSystem& instance() { static FileSystem* instance = new FileSystem(); return *instance; }
private: FileSystem() {} };
|
我们希望变量尽可能局部, 对象影响的范围越小越好
传进来(将带向作为参数传进函数), 但是例如"日志"这种横切面关注点不适合
从基类获得
从现有的全局中获取 class Game { public: static Game& instance() { return instance_; }
Log& getLog() { return *log_; } FileSystem& getFileSystem() { return *fileSystem_; } AudioPlayer& getAudioPlayer() { return *audioPlayer_; }
private: static Game instance_;
Log* log_; FileSystem* fileSystem_; AudioPlayer* audioPlayer_; };
|
服务定位器(见后)
状态模式
有限状态机
先处理状态, 再处理输入
enum State { STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING };
void Heroine::handleInput(Input input) { switch (state_) { case STATE_STANDING: if (input == PRESS_B) { state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE JUMP); } else if (input == PRESS_DOWN) { state = STATE_DUCKING; setGraphics(IMAGE_DUCK); } break;
case STATE_JUMPING: if (input == PRESS_DOWN) { state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); } break;
case STATE_DUCKING: if (input == RELEASE_DOWN) { state_ = STATE_STANDING; setGraphics(IMAGE_STAND); } break; } }
|
OO 状态模式
class Heroine { public: virtual void handleInput(Input input) { HeroineState* state = state_->handleInput(*this, input); if (state != NULL) { delete state_; state_ = state; state_->enter(*this); } }
virtual void update() { state_->update(*this); } private: HeroineState* state_; };
class HeroineState {
public: virtual ~HeroineState() {} virtual void handleInput(Heroine& heroine, Input input) {} virtual void update(Heroine& heroine) {} };
class DuckingState : public HeroineState { public: DuckingState() : chargeTime_(0) {}
virtual void handleInput(Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { return new StandingState(); } }
virtual void update(Heroine& heroine) { chargeTime_++; if (chargeTime_ > MAX_CHARGE) { heroine.superBomb(); } }
private: int chargeTime_; };
class StandingState : public HeroineState { public: virtual void enter(Heroines heroine) { heroine.setGraphics(IMAGE_STAND); } };
|
此外还有
- 并发状态机(多个并列状态机)
- 分层状态机(状态可以有父状态, 如果子状态没有处理, 就交给父状态)
- 下推自动机(将之前的状态存储在栈中, 例如开完火就弹出开火状态, 返回到之前的状态)
序列模式
双缓冲模式
使用两个帧缓冲
使用条件
- 需要维护一些增量修改的状态
- 当修改到一半时, 状态可能被外部请求
- 想要防止请求状态的外部代码知道内部的工作方式
- 想要读取状态并且不想等修改完成
class Framebuffer { public: Framebuffer() { clear(); }
void clear() { for (int i = 0; i < WIDTH * HEIGHT; i++) { pixels_[i] = WHITE; } }
void draw(int x, int y) { pixels_[(WIDTH * y) + x] = BLACK; }
const char* getPixels() { return pixels_; }
private: static const int WIDTH = 160; static const int HEIGHT = 120;
char pixels_[WIDTH * HEIGHT]; };
class Scene { public: Scene() : current_(&buffers_[0]), next_(&buffers_[1]) {}
void draw() { next_->clear(); next_->draw(1, 1); swap(); }
Framebuffer& getBuffer() { return *current_; }
private: void swap() { Framebuffer* temp = current_; current_ = next_; next_ = temp; }
Framebuffer buffers_[2]; Framebuffer* current_; Framebuffer* next_; };
|
游戏循环
游戏循环: 事件循环, 处理用户输入但是不等待他
一个循环就是一个 tick
while(true){ processInput(); update(); render(); }
|
但是如何以固定的速度运行游戏?
方案 1
while (true) { double start = getCurrentTime(); processInput(); update(); render();
sleep(start + MS_PER_FRAME - getCurrentTime()); }
|
方案 2
double lastTime = getCurrentTime(); while (true) { double current = getCurrentTime(); double elapsed = current - lastTime; processInput(); update(elapsed); render(); lastTime = current; }
|
方案 3
double previous = getCurrentTime(); double lag = 0.0; while (true) { double current = getCurrentTime(); double elapsed = current - previous; previous = current; lag += elapsed;
processInput();
while (lag >= MS_ PER_UPDATE) { update(); lag -= MS_ PER_ UPDATE; }
render(); }
|
更新方法
对象独立, 跟着时间进行模拟update()
每一帧, 游戏循环遍历集合, 在每个对象上调用 update()方法
(可能需要用到双缓冲, 可能需要传入消逝的时间)
行为模式
字节码(解释器模式)
将行为编码为虚拟机上的指令
指令集定义可执行的底层操作 一系列指令被编码为字节序列 (可以想象成, 每个字节码代表一个指令, 高层只需要使用字节码就能编写高层行为) 虚拟机(解释器, 对字节码进行解析, 输入字节码, 执行对应的指令, 可以使用栈来存储字节码和参数)使用中间值堆栈执行这些指令 通过组合指令定义复杂高层行为
子类沙箱
用一系列由基类提供的操作定义子类中的行为
比如: 在基类中提供 protected 方法, 这些方法是可供子类使用的基本单元 子类只需要继承基类, 然后在用基类的基本方法实现自己的activate()
方法
但是这会导致 脆弱的基类问题: 基类变得难以改变
class Superpower { public: virtual ~Superpower() {}
protected: virtual void activate() = 0; void move(double x, double y, double z) {}
void playSound(SoundId sound, double volume) { }
void spawnParticles(ParticleType type, int count) {} };
|
类型对象
让类 A 灵活的创造新类型, 类 A 的每个实例都代表了不同的对象类型
eg: 要创建多种 monster
本质上是将部分的类型系统从硬编码的继承结构拉出, 放到可以运行时定义的数据中, 但是这样定义类型相关的行为就会变得困难
典型的例子就是 虚函数表
class Breed { public: Breed(int health, const char* attack) : health_(health), attack_(attack) {}
Monster* newMonster() { return new Monster(*this); }
int getHealth() { if (health_ != 0 || parent_ == NULL) return health_;
return parent_->getHealth(); } const char* getAttack() { return attack_; }
private: int health_; const char* attack_; Breed* parent_; };
class Monster { public: const char* getAttack() { return breed_.getAttack(); }
private: Monster(Breed& breed) : health_(breed.getHealth()), breed_(breed) {} int health_; Breed& breed_; };
Monster* monster = someBreed.newMonster();
|
解耦模式
组件模式
允许单一实体 跨越多个领域 而不导致这些领域彼此耦合
但是这样会导致: 对象需要实例化初始化, 正确的连接, 不同组件间沟通会有困难, 控制他们如何使用内存会更复杂
class Bjorn { public: int velocity; int x, Y;
void update(World& world, Graphics& graphics) { input_.update(*this); physics_.update(*this, world); graphics_.update(*this, graphics); }
private: InputComponent* input_; PhysicsComponent physics_; GraphicsComponent graphics_; }
|
事件队列
解耦发出消息的时间 和 处理他的时间
while (running) { Event event = getNextEvent(); }
|
环形队列
等等(?)
服务定位器
提供服务的全局接入点, 避免使用者和服务类耦合
服务类定义了操作的抽象接口, 分离的服务定位器提供查询获取服务的方法, 同时隐藏服务提供者的具体细节和定位它的过程
class ConsoleAudio : public Audio { public: virtual void playSound(int soundID) {} virtual void stopSound(int soundID) {} virtual void stopAllSounds() {} };
class Locator { public: static void initialize() { service_ = &nullService_; } static Audio& getAudio() { return *service_; } static void provide(Audio* service) { if (service == NULL) service = &nullService_; else service_ = service; }
private: static Audio* service_; static NullAudio nullService_; };
|
日志系统的实现
class LoggedAudio : public Audio { public: LoggedAudio(Audio& wrapped) : wrapped_(wrapped) {}
virtual void playSound(int soundID) { log("play sound"); wrapped_.playSound(soundID); }
virtual void stopSound(int soundID) { log("stop sound"); wrapped_.stopSound(soundID); }
virtual void stopAllSounds() { log("stop all sounds"); wrapped_.stopAllSounds(); }
private: void log(const char* message) {} Audio& wrapped_; };
void enableAudioLogging() { Audio* service = new LoggedAudio(Locator::getAudio()); Locator::provide(service); }
|
反射?: 编程语言在运行时与类型系统打交道的能力
优化模式
数据局部性
cpu 缓存
c++ 中的指针访问, 虚函数都会导致内存访问不连续
冷/热分割
脏标识模式
将工作延期至需要其结果才去执行, 避免不必要的工作
对象池模式
放弃单独的分配和释放对象, 从固定的池中重用对象
空间分区
将对象根据位置存储在数据结构中, 来高效的定位对象