架构,性能和游戏

"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() {
// undo operation
}
};

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) {}
// 记得在析构函数中 removeObserver
// <<失效监听者问题>>

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);


//template
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();
// 或者使用惰性初始化 if null then new
return *instance;
}

private:
FileSystem() {}
};

我们希望变量尽可能局部, 对象影响的范围越小越好

  1. 传进来(将带向作为参数传进函数), 但是例如"日志"这种横切面关注点不适合

  2. 从基类获得

  3. 从现有的全局中获取

    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_;
    };

  4. 服务定位器(见后)

状态模式

有限状态机

先处理状态, 再处理输入

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); }
// other methods
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) {
//这里的实现可以转发给其他组件 engine
//soundEngine_play (sound, volume);

//也可以返回对象, 减少基类复杂度
//return soundPlayer;
}

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++ 中的指针访问, 虚函数都会导致内存访问不连续

冷/热分割

脏标识模式

将工作延期至需要其结果才去执行, 避免不必要的工作

对象池模式

放弃单独的分配和释放对象, 从固定的池中重用对象

空间分区

将对象根据位置存储在数据结构中, 来高效的定位对象