源代码就是软件设计。测试和调试是软件设计必不可少的一部分。
评价架构设计的好坏就是评价它应对改动有多么轻松。软件架构的关键目标: 最小化在编写代码前需要了解的信息。尽量减少代码改动所波及的范围。
问题分析:
- 在设计之前先考虑清楚,选出最适合的数据结构和算法
先使用已经写成正确的代码
- 输入/输出数据格式和数量
- 特别事件
- 需求
- 程序最重要的部分
- 错误事件
- 用户接口
- 可移植性
- 扩展/维护
自顶向下细化:
- 在描述问题产生的名词产生类,动词产生函数。
- 主程序将工作分配给各种数据结构和函数
- 类和函数应该隐藏一些东西
- 函数只需要很好的完成1项任务(无需要做太多说明)
- 函数必须精确说明前置条件(输入)和 后置条件(输出)
- 函数开始时保证前置条件成立及其原因
- 当程序超过10%的部分需要修改,则需要重写函数。
- 在类和函数在编码完成后立即进行测试
- 结构化预排:
先解释主函数,再解释其他函数 - 在主程序的关键位置插入打印语句
名称:
- 类型的起始字母大写,其他小写
- 类,函数,常量和全局变量的名字必须能表达它的目的。
- 对暂时使用/局部变量保持名称简单
- 用前缀/后缀关联同一类型的名称
- 避免单独使用I/O
测试:
驱动器:为函数提供输入的辅助程序
测试数据:
- 玻璃盒:结构性测试:图论
对于小模块,设计数据能够到达所有分支 黑盒:由于错误经常发生在接口中,适合在整个程序完成后使用,功能性测试:离散数学(集合和概率论)
- 简易值和典型值
最坏情况测试:包括以下2种
边界值测试:变量之间相互独立。极值(及其附近)
- 健壮性测试:非法值(注意反馈异常的方式)
- 等价类测试:避免重复类型的取值
- 强指的是多缺陷假设;失效是由两个或两个以上缺陷同时作用引起的,要求在选取测试用例时同时让多个变量取极值。
- 弱指的是单缺陷假设:选取测试用例时仅仅使得一个变量取极值,其他变量均取正常值。
- 弱一般等价类:单缺陷假设,不讨论异常区域
- 玻璃盒:结构性测试:图论
强一般等价类:多缺陷假设,不考虑异常区域
弱健壮等价类:单缺陷假设,要考虑异常区域
强健壮等价类:多缺陷假设,要考虑异常区域;
决策表:将对输入的判断条件和输出的类型以表的形式展现。适用于输入变量存在关联。
占位函数:
- 定义内容几乎为空的函数,保证正确的编译主函数
- 后期用真正的类/函数进行替代,或者将其进行细化
临时支架:
在无法定位错误时,由于之前程序已经将工作划分为不同部分,在划分点插入调试语句帮助判断。- 防御式设计:
在函数开始的地方放置if语句检查前置条件,特别是当输入来自I/O,文件等。 - 静态分析器
程序评价:
- 问题说明:
设计是否完全符合要求,如果不是请做出解释 - 正确性:
对程序的最可能出错的部分进行玻璃盒测试 - 用户接口
效率:在确保代码正确之前不要优化代码
敏捷方法:
- 极限编程(XP)
- 结对编程:轮流负责写和检查
- 随时与用户进行沟通
- 计划:将大的要求分解成小的要求,估计实现和合并的时间
- 短时间交付1次
- 测试驱动:编写代码通过之前的单元测试(白盒测试)。
由于模块之间需要协调工作,为了在相互耦合的模块被全部写出来之前能够进行测试,可以通过Mock Objects模式暂时代替别的模块参与测试。
验收测试(黑盒测试):通过验收测试框架测试系统,能够分离测试输入和期望输出。 - 隐喻:使模块的功能变得明显
- 拆入和合并模块
持续进销重构(在不改变代码外观的前提下修改内部结构)
最开始的UML图不一定是最佳设计
软件腐化:
- 模块间的耦合度高,结构设计复杂,难以理解。
- 由于需求的变动,导致改动违背了原来的设计
敏捷式开发:不进行预先设计,通过测试保证程序的正确性。
- 单一职责(SRP)
- 开发(扩展)-封闭(更改)(OCP)
- strategy和template method模式
- Acyclic visitor模式保证输出的顺序
- Liskov替换原则(LSP):派生类不会破坏原有的代码结构。
- 考虑正方形和矩形的继承关系无法完全符合原来的逻辑
- DSB原则(基于契约设计):每个方法的前置和后置条件必须为真。为了保证原有的代码结构,所以派生类必须让原有的方法遵守更为宽松的前置条件和更为苛刻的后置条件。
- 因为正方形和矩形有一些公有的特性,所以可以通过1个公共的抽象接口连接这2个类。P111或者建立抽象基类。
- 依赖倒置原则(DIP):高层和底层模块都依赖于抽象接口。高层通过接口使用底层,底层按照接口进行实现。例如P119
- 接口隔离原则(ISP):
- 例1:定时器发现门超过规定时间没有关时发出警报。为了避免Timer类和Door类之间的耦合,创建1个中间类的对象和Timer类进行沟通,但是会造成额外的内存开销。建立TimerDoor类多重继承Timer类和Door类,通过分离的接口使用同一个对象。图126
- 例2:ATM机需要不同的UI界面,程序需要将结果显示到界面上。按照不同的输出要求创建不同的Transaction派生类。由于不同的UI界面所需要的操作不同,所以创建不同的UI抽象基类对应不同的Transaction派生类,通过UI类实现具体的操作。
如果反过来将UI类作为基类,可能用户会调用不属于该派生UI类的方法。当需要加入1种新的Transaction派生类时,最大程度的减少重新编译带来的影响。图P128
static UI lui;//
AUI& aui=lui;
class ATransaction:public Transaction
{
public:
ATransaction(AUI& aui):itaui(aui){}
…
private:
AUI& itaui;
};
MVC:
- Model:应用对象。当模型发生变化时,通知对应的View刷新。
- View:在屏幕上的显示
- Controller:界面对用户输入的响应
设计模式:
分类原则:
- 应用目的:
- 创建型:延时构造
- 结构型:组合
- 行为型:协同工作
- 应用范围:
- 类
- 对象
- 应用目的:
Abstract Factory(抽象工厂):隔离两个相关联的物体。
- 例如用户希望使用不同风格的动画效果,为了减少代码量,用抽象类(使用抽象基类难以扩展新的功能)WiedgeFactory定义相关接口,具体子类实现不同的风格。所以用户不需要依赖于具体的风格。
- Factory Method(工厂方法):延时初始化(lazy initialization)
- 注意基类为抽象类或者具体类。
- 可以在参数表中添加指示类型的参数
Maze* MazeGame::Create()
{
…
Room* r1=MakeRoom(1);
r1->SetSide(North,MakeWall());
…
return aMaze;
}
class StandardCreator:public Creator{
public:
virtual Product* CreateProduct();
};
使用额外的参数。
class Creator{
public:
Product* GetProduct(ProductId id);
protected:
virtual Product* CreateProduct(ProductId id);
private:
Product* _Product;
};
Product* Creator::GetProduct(ProductId id)
{
if(id==MINE) return new MyProduct;
…
//可以将有些类的初始化交给基类
return Creator::Create(id);
}
为了减少代码数量(即Creator的派生类),使用模板继承类
template <class TheProduct>
class StandardCreator:public Creator{
public:
virtual Product* CreateProduct();
};
template <class TheProduct>
Product* StandardCreator<TheProduct>::GetProduct()
{
return new TheProduct;
}
- Adapter(适配器模式):转换接口
- 使用继承(P)或者模板(容器模板)
- Composite(组合):将单一对象组合成树状结构,使得用户忽略组合对象与单个对象的不同,对2种对象使用同一接口。(图P119)
- Component(抽象构件角色):为组合中的对象声明(也可以实现)接口。
- Leaf(树叶构件角色):不作为基类,实现Component声明的接口。
- Composite(树枝构件角色):实现Component声明的接口。;存储子部件。
class Component{
public:
virtual void dosomthing();
virtual void add(Component*);
virtual void remove(Component*);
};
class Leaf:public Component{
public:
virtual void dosomthing();
};
class Composite:public Component{
public:
virtual void dosomthing();
virtual void add(Component*);
virtual void remove(Component*);
private:
List<Component*> equipment;
};
- 透明式的组合模式:将管理子构件的方法定义在Component接口中,这样Leaf类就需要处理这些对其意义不大的方法(空实现或抛异常),在接口层次上Leaf和Composite没有区别,即透明性。Component 声明的这些管理子构件的方法对于Leaf来说是不适用的,这样也就带来了一些安全性问题。
- 安全式的组合模式:将管理子构件的方法定义在Composite中,由于Leaf和Composite有不同的接口(方法),又失去了透明性。
/**
* composite1
* / \
* leaf1 composite2
* / \
* leaf2 leaf3
*
* */
int main()
{
Component leaf2=new Leaf;
Component leaf3=new Leaf;
Composite composite2=new Composite;
composite2.add(leaf2);
composite2.add(leaf3);
Component leaf1=new Leaf;
Composite composite1=new Composite;
composite1.add(leaf1);
composite1.add(composite2);
composite1.doSomething();
}
Decorator(装饰):动态(即不改变接口的情况下,继承为静态)为对象添加额外的功能。
- 透明式的装饰模式:Decorator的接口和Component一致。
- 半透明的装饰模式:Decorator的接口和Component不一致。由于在增强性能的时候,往往需要建立新的公开的方法。介于装饰模式和适配器模式之间
例如咖啡有不同的口味,例如在咖啡中加入摩卡或牛奶,不同咖啡豆磨成的咖啡。所以咖啡为Component,摩卡和牛奶为ContreteDecorator。如果要通过继承来表示咖啡成品,所产生的派生类更多。
被装饰的类
class Component{
public:
virtual void dosomthing();
};
Decorator通过Component对象解耦Component和其组件
class Decorator:public Component{
public:
virtual void dosomthing();
private:
Component* equipment;
};
void Component::dosomthing()
{
equipment->dosomthing();
}
ContreteDecorator为额外的功能。
class ContreteDecorator:public Decorator{
public:
virtual void dosomthing();
private:
virtual void dosomthing1();
};
void ContreteDecorator::dosomthing()
{
Decorator::dosomthing();
dosomthing1();
}
- Observer(观察者模式):定义对象之间以对多的依赖关系。当被依赖的对象发生改变,所以依赖它的对象都会得到通知并更新。
- 在降低相互关联类的耦合度时保持一致性。
- subject可以有任意数量的observer
Observer:被依赖对象的更新接口。可以观察多个对象。
class Observer
{
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
};
Subject:添加或删除Observer的接口
注意及时销毁不需要的观察者
class Subject{
public:
virtual void Notify();//通知
virtual void add(Observer*);
virtual void detach(Observer*);
protecetd:
Observer();
private:
List<Observer*> _observer;
};
void Subject::Notify()
{
for(auto r:_observer)
r->onNotify(entity, event);
}
任何实现了onNotify的具体类就成为了观察者。成就系统存储了玩家可以完成的各种各样的成就,当成就在游戏的不同层面被触发时,解耦成就系统和其他部分。
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;
// 处理其他事件,更新heroIsOnBridge_变量……
}
}
private:
void unlock(Achievement achievement)
{
// 如果还没有解锁,那就解锁成就……
}
bool heroIsOnBridge_;
};
现在设计的观察者模式,让它基于函数而不是基于类。在C++中,倾向于让你注册一个成员函数指针作为观察者,而不是Observer接口的实例。
- Strategy(策略):泛型算法
context:定义算法的接口
Strategy:由其子类ConcreteStrategy实现具体的算法
context和Strategy的接口需要保证ConcreteStrategy能够访问context的数据。所以context可以选择将自身或发送部分数据传给Strategy。用C++的模板时必须保证Strategy在编译后不需要改变。
Composition(context)负责维护文章换行。
class Composition{
public:
Composition(Compositor*);
void Rapair();
private:
Compositor* _compositor;
…
};
void Composition::Rapair()
{
…
int breakout=_compositor->Compose(…);
…
}
由Compositor(Strategy)的子类负责实现。
//接收不同类型的数据
class Compositor{
public:
virtual int Compose(…)=0;
//…为Composition传递的参数
};
实例化Composition的对象时设置需要的Compositor类型。
Composition* quick=new Composition(new SimpleCompositor);
- Template Method:重定义算法的某些结构。
ConcreteClass通过AbstractClass实现特定算法的不变步骤。
view类规定视图在成为焦点后进行绘制。
class View{
public:
void Display();//原语操作
protected:
virtual void DoDisplay();//钩子操作
};
void View::Display()
{
SetFocus();
DoDisplay();
ResetFocus();
}
//由View的派生类决定具体的绘画过程
void View::DoDisplay() {}
- Command模式
回调:旅客(起始函数:调用中间函数)要求旅馆(中间函数)在指定时间进行指定的叫醒服务(回调函数)。起始函数可以通过不同的回调函数作为参数,决定中间函数的行为。
为一些没有闭包的语言模拟闭包。
将一个请求封装为一个对象,从而使你可用不同的请求(按键)对客户(游戏对象)进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。
//回调函数
bool bigger(…,…);
//中间函数
void sort(ivec.begin(),ivec.end(),bigger());
示例1:命令对象可以做一件事
例如游戏需要根据对应的按键作出相应的反应。
定义1个基类作为可触发的反应,
class Command
{
public:
//当参数列表为空时假定的耦合(jump(), fireGun()之类的函数可以找到玩家角色)限制了这些命令的用处,
virtual void execute(GameActor& actor) = 0;
};
根据不同的游戏行为定义相应的子类:
class JumpCommand : public Command
{
public:
//virtual void execute() { jump(); }
//使用这个类让游戏中的任何角色进行跳跃。
virtual void execute(GameActor& actor)
{
actor.jump();
}
};
class InputHandler
{
public:
Command* handleInput();
private:
//在代码的输入处理部分,为每个按键存储一个指向命令的指针。
Command* buttonX_;
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
//延时执行按键对应的操作
Command* InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) return buttonX_;
if (isPressed(BUTTON_Y)) return buttonY_;
if (isPressed(BUTTON_A)) return buttonA_;
if (isPressed(BUTTON_B)) return buttonB_;
// 没有按下任何按键,就什么也不做
return NULL;
}
通过在命令和角色间增加了一层重定向。
Command* command = inputHandler.handleInput();
if (command)
{
//可以灵活切换玩家控制的角色
command->execute(actor);
}
一些代码(输入控制器或者AI)产生一系列command放入流中。 另一些代码(调度器或者角色自身)调用并消耗command。 通过在中间加入队列,我们解耦了消费者和生产者。
示例2:命令对象可以撤销一件事
为了让玩家能够进行撤销操作,在基类Command中添加undo()方法,
class MoveUnitCommand : public Command
{
public:
MoveUnitCommand(Unit* unit, int x, int y)
: unit_(unit),
x_(x),
y_(y)
{}
virtual void execute()
{
// 保存移动之前的位置便于复原。
xBefore_ = unit_->x();
yBefore_ = unit_->y();
unit_->moveTo(x_, y_);
}
virtual void undo()
{
unit_->moveTo(xBefore_, yBefore_);
}
private:
Unit* unit_;//操作单元
int xBefore_, yBefore_;
int x_, y_;
};
Command* handleInput()
{
Unit* unit = …;
if (isPressed(BUTTON_UP)) {
// 向上移动单位
int destY = unit->y() - 1;
return new MoveUnitCommand(unit, unit->x(), destY);
}
// 其他的移动……
return NULL;
}
- Flywight(享元模式):运用共享技术支持细粒度的对象
游戏的地图被划分为由微小区块组成的巨大网格。 每个区块都由一种地形覆盖。每种地形类型都有一系列特性会影响游戏玩法。
class Terrain
{
public:
Terrain(int movementCost,
bool isWater,
Texture texture)
: movementCost_(movementCost),
isWater_(isWater),
texture_(texture)
{}
int getMovementCost() const { return movementCost_; }
bool isWater() const { return isWater_; }
const Texture& getTexture() const { return texture_; }
private:
int movementCost_;
bool isWater_;
Texture texture_;
};
每个相同地形的区块会指向相同的地形实例。由于地形是始终存在的,所以直接在游戏世界中存储它们。
class World
{
public:
World()
: grassTerrain_(1, false, GRASS_TEXTURE),
hillTerrain_(3, false, HILL_TEXTURE),
riverTerrain_(2, true, RIVER_TEXTURE)
{}
private:
Terrain grassTerrain_;
Terrain hillTerrain_;
Terrain riverTerrain_;
Terrain* tiles_[WIDTH][HEIGHT];
};
void World::generateTerrain()
{
// 将地面填满草皮.
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
// 加入一些丘陵
if (random(10) == 0)
{
tiles_[x][y] = &hillTerrain_;
}
else
{
tiles_[x][y] = &grassTerrain_;
}
}
}
// 放置河流
int x = random(WIDTH);
for (int y = 0; y < HEIGHT; y++) {
tiles_[x][y] = &riverTerrain_;
}
}
- 原型模式:通过对象进行克隆,而不是通过类产生新的对象。它不但拷贝原型的类,也拷贝它的状态。
例如有不同种类的怪物:Ghost,Demon,Sorcerer等。
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_;
};
- 单例模式:
- 保证一个类只有一个实例:考虑封装文件系统的API类。,如果一个操作创建文件,另一个操作删除同一文件,封装器类需要同时考虑,保证它们没有相互妨碍。单例模式提供的构建类的方式,在编译时保证类只有单一实例。
- 并且提供了访问该实例的全局访问点。
class FileSystem
{
public:
static FileSystem& instance()
{
//C++11标准保证了本地静态变量只会初始化一次
//线程安全
static FileSystem *instance = new FileSystem();
return *instance;
}
private:
FileSystem() {}
};
尽量避免使用:促进了耦合的发生,对并行不友好。在游戏中看到的很多单例类都是“管理器”——照顾其他对象。 如果可以,把所有的行为都移到单例帮助的类中。OOP就是让对象管理好自己。
- state模式:允许一个对象在其内部状态发生变化时改变自己的行为,该对象看起来好像修改了它的类型
英雄需要对玩家的输入做出响应,但是注意响应需要遵循一定的规则。
状态机有3个要素:状态,输入,和转移。
可以代替switch
class Heroine
{
public:
virtual void handleInput(Input input)
{
state_->handleInput(*this, input);
}
virtual void update()
{
state_->update(*this);
}
private:
HeroineState* state_;
};
为状态定义接口。 无需创建多个状态,尽量使用静态状态。当有多个操作对象时,由于chargeTime_所以需要多个DuckingState
class HeroineState
{
public:
//输入
virtual void handleInput(Heroine& heroine, Input input) {}
//转移
virtual void update(Heroine& heroine) {}
static StandingState standing;
static DuckingState ducking;
static JumpingState jumping;
static DivingState diving;
};
对于每个状态,我们定义一个类实现接口。
class DuckingState : public HeroineState
{
public:
DuckingState()
: chargeTime_(0)
{}
virtual void handleInput(Heroine& heroine, Input input) {
if (input == RELEASE_DOWN)
{
// 改回站立状态……
heroine.setGraphics(IMAGE_STAND);
}
}
virtual void update(Heroine& heroine) {
chargeTime_++;
if (chargeTime_ > MAX_CHARGE)
{
heroine.superBomb();
}
}
private:
int chargeTime_;
};
分层状态机: 当一个事件进来,如果子状态没有处理,它就会交给链上的父状态。
英雄可能会有更多相似的状态,例如在站立状态下衍生出行走和奔跑2种状态。所以在class HeroineState和class DuckingState之间添加class OnGroundState
class DuckingState : public OnGroundState
{
public:
virtual void handleInput(Heroine& heroine, Input input)
{
if (input == RELEASE_DOWN){
// 站起……
}
else{
// 没有处理输入,返回上一层
OnGroundState::handleInput(heroine, input);
}
}
};
并发状态机:
当对象加上载具时,为了减少代码冗余,不要将多种状态绑定到多个状态机上。
class Heroine
{
private:
HeroineState* state_;
HeroineState* equipment_;
};
下推状态机:通过栈重回上一状态。
- 解释器模式:
树中的每个对象都是表达式或子表达式。先从最里面的子表达式开始。 计算完里面的,结果向外作为参数流向包含它们的表达式, 直到得出最终结果。但是使用树的速度太慢。
class Expression
{
public:
virtual double evaluate() = 0;
};
数字表达式
class NumberExpression : public Expression
{
public:
virtual double evaluate()
{
return value_;
}
private:
double value_;
};
运算符表达式
class AdditionExpression : public Expression
{
public:
virtual double evaluate()
{
// 计算操作数
double left = left_->evaluate();
double right = right_->evaluate();
// 把它们加起来
return left + right;
}
private:
Expression* left_;
Expression* right_;
};
通过虚拟机运行字节码执行操作。指令被编码为字节序列,虚拟机使用中间值堆栈依次执行这些指令。
用户在更高层编写行为,再用编译器将其翻译为虚拟机能理解的字节码。
void setHealth(int wizard, int amount);
void setWisdom(int wizard, int amount);
void setAgility(int wizard, int amount);
void playSound(int soundId);
void spawnParticles(int particleType);
通过数据调用API。法术的代码就是“字节码”。
enum Instruction
{
INST_SET_HEALTH = 0x00,
INST_SET_WISDOM = 0x01,
INST_SET_AGILITY = 0x02,
INST_PLAY_SOUND = 0x03,
INST_SPAWN_PARTICLES = 0x04
};
class VM
{
public:
void interpret(char bytecode[], int size)
{
for (int i = 0; i < size; i++)
{
char instruction = bytecode[i];
switch (instruction)
{
// 每条指令的跳转分支……
case INST_SET_HEALTH:
{
int amount = pop();
int wizard = pop();
setHealth(wizard, amount);
break;
}
case INST_SET_WISDOM:
setWisdom(0, 100);
break;
case INST_SET_AGILITY:
setAgility(0, 100);
break;
case INST_PLAY_SOUND:
playSound(pop());
break;
case INST_SPAWN_PARTICLES:
spawnParticles(pop());
break;
//组合
case INST_LITERAL:
{
// 从字节码中读取下一个字节
int value = bytecode[++i];
push(value);
break;
}
case INST_GET_HEALTH:
{
int wizard = pop();
push(getHealth(wizard));
break;
}
case INST_ADD:
{
int b = pop();
int a = pop();
push(a + b);
break;
}
}
}
}
private:
static const int MAX_STACK = 128;
int stackSize_;
int stack_[MAX_STACK];
//指令存储
void push(int value)
{
// 检查栈溢出
assert(stackSize_ < MAX_STACK);
stack_[stackSize_++] = value;
}
int pop()
{
// 保证栈不是空的
assert(stackSize_ > 0);
return stack_[--stackSize_];
}
};
使用示例
LITERAL 0 [0] # 巫师索引
LITERAL 0 [0, 0] # 巫师索引
GET_HEALTH [0, 45] # 获取血量()
LITERAL 0 [0, 45, 0] # 巫师索引
GET_AGILITY [0, 45, 7] # 获取敏捷()
LITERAL 0 [0, 45, 7, 0] # 巫师索引
GET_WISDOM [0, 45, 7, 11] # 获取智慧()
ADD [0, 45, 18] # 将敏捷和智慧加起来
LITERAL 2 [0, 45, 18, 2] # 被除数:2
DIVIDE [0, 45, 9] # 计算敏捷和智慧的平均值
ADD [0, 54] # 将平均值加到现有血量上。
SET_HEALTH [] # 将结果设为血量
- 外观模式:用由基类提供的操作定义子类中的行为,为(复杂的)子系统提供统一的高层接口,方便不需要定制子系统的客户操作。
基类定义抽象的沙箱方法和几个提供的操作。 将操作标为protected,表明它们只为子类所使用。 每个沙箱子类用基类提供的操作实现了沙箱函数。
例如超能力类需要和游戏引擎进行交互,为了减少耦合,给每个实现超能力的派生类一系列可使用的基本单元。 playSound()函数播放声音;spawnParticles()函数渲染粒子效果。 保证了这些操作覆盖了你要做的事情,所以你不需要#include随机的头文件,干扰到代码库的其他部分。
class 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)
{
// 实现代码……
}
};
通过创建从Superpower继承的新类,重载沙箱方法activate()。通过调用Superpower提供的protected方法实现主体。
//将超级英雄射向天空,播放合适的声音,扬起尘土。
class SkyLaunch : public Superpower
{
protected:
virtual void activate()
{
// 空中滑行
playSound(SOUND_SPROING, 1.0f);
spawnParticles(PARTICLE_DUST, 10);
move(0, 0, 20);
}
};
- 享元模式:
怪物有生命值,攻击力等多个的属性。怪物有不同品种,不同品种怪物的属性各不相同。如果使用Dragon派生类继承Monster基类,由于怪物的种类过多,会形成代码冗余。所以让每个怪物有品种。
定义类型对象类(Breed)和有类型的对象类(Monster)。每个类型对象实例代表一种不同的逻辑类型。每种有类型的对象保存对描述它类型的类型对象的引用。
构造器:
class Breed
{
public:
Breed(int health, const char* attack)
: health_(health),
attack_(attack)
{}
Monster* newMonster()
{ return new Monster(*this); }
int getHealth() { return health_; }
const char* getAttack() { return attack_; }
private:
int health_; // 初始血值
const char* attack_;
};
通过用不同值实例化Monster来创建成百上千的新品种。
class Monster
{
friend class Breed;
public:
const char* getAttack()
{return breed_.getAttack();}
private:
int health_; // 当前血值
Breed& breed_;
Monster(Breed& breed):
health_(breed.getHealth()),breed_(breed){}
};
在Breed中定义“构造器”函数可以决定Monster类的对象存放内存的位置。
创建一个对象分为两步:内存分配和初始化。 Monster类需要加载图形,初始化怪物AI以及做其他的设置工作。
Monster* monster = someBreed.newMonster();
- 双缓冲模式:状态有可能在被修改的同时被请求。
例如在图形渲染的同时,视频驱动正在读取它。 当它进入了未写的部分,就将没有写的像素绘制到了屏幕上。结果就是撕裂,在屏幕上出现绘制到一半的图像。
例1: - 定义缓冲类封装了缓冲:类保存了两个缓冲的实例:下一缓冲和当前缓冲。当信息从缓冲区中读取,它总是读取当前的缓冲区。 当信息需要写到缓存,它总是在下一缓冲区上操作。
- 这个操作必须是原子的——在交换时,没有代码可以接触到任何一个状态。当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换,
将整个缓冲区封装在Scene类中。
class Scene
{
public:
Scene()
: current_(&buffers_[0]),
next_(&buffers_[1])
{}
void draw()
{
next_->clear();
next_->draw(1, 1);
// ...
next_->draw(4, 3);
swap();
}
Framebuffer& getBuffer() { return *current_; }
private:
void swap()
{
// 只需交换指针
Framebuffer* temp = current_;
current_ = next_;
next_ = temp;
}
Framebuffer buffers_[2];
Framebuffer* current_;
Framebuffer* next_;
};
例2:保证无论程序的响应不会受到排列顺序的影响
class Actor
{
public:
virtual void update() = 0;
void swap()
{
// 交换缓冲区
currentSlapped_ = nextSlapped_;
// 清空新的“下一个”缓冲区。.
nextSlapped_ = false;
}
void slap() { nextSlapped_ = true; }
bool wasSlapped() { return currentSlapped_; }
private:
bool currentSlapped_=false;
bool nextSlapped_;
};
当喜剧演员被扇时,他的反应是扇他面前的人一巴掌。
class Comedian : public Actor
{
public:
void face(Actor* actor) { facing_ = actor; }
virtual void update()
{
if (wasSlapped()) facing_->slap();
}
private:
Actor* facing_;
};
class Stage
{
public:
void add(Actor* actor, int index);
void update();
private:
static const int NUM_ACTORS = 3;
Actor* actors_[NUM_ACTORS];
};
void Stage::update()
{
for (int i = 0; i < NUM_ACTORS; i++)
{
actors_[i]->update();
}
for (int i = 0; i < NUM_ACTORS; i++)
{
actors_[i]->swap();
}
}
Stage stage;
Comedian* harry = new Comedian();
Comedian* baldy = new Comedian();
Comedian* chump = new Comedian();
harry->face(baldy);
baldy->face(chump);
chump->face(harry);
stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);
harry->slap();
stage.update();
- 序列模式
例1:游戏循环:不管潜在的硬件条件,以固定速度运行游戏。
根据现实时间的间隔决定游戏中前进的时间,但是由于不同的设备运行的速度不一样,所以在联机游戏中不同玩家的游戏进度不同。
以固定的时间步长更新游戏,在任意时刻渲染。游戏经常在两次更新之间时显示。
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;
processInput();
//MS_PER_UPDATE:固定步长
//保证无论机器的运行速度快慢都能保持一致
while (lag >= MS_PER_UPDATE)
{
update();
lag -= MS_PER_UPDATE;
}
render();
}
例2:游戏世界管理对象集合。 每个对象实现一个更新方法(注意放在哪个地方)模拟对象在一帧内的行为。每一帧,游戏循环更新集合中的每一个对象。
- 解耦模式