在线阅读地址:
参考文章:
行为模式
类型对象定义行为的类别而无需完成真正的类。 子类沙盒定义各种行为的安全原语。 最先进的是字节码,将行为从代码中分离,放入数据文件中。
- 字节码
- 享元模式、原型模式中,不同的属性被存储在数据库中,而字节码是将行为存在数据库中
- 可用于实现可视化脚本编辑工具
- 子类沙箱
- 子类使用基类方法,或在基类方法上扩展
- 类型对象
- 其实是享元模式、原型模式的一种应用,以不同数据(而不是不同类)区分对象类型
字节码
将行为编码为虚拟机器上的指令,赋予其数据的灵活性
如何能在分离的数据文件中定义行为,游戏引擎还能加载并执行它们?
解释器模式: 太慢了
玩家电脑在运行游戏时并不会遍历一堆C++语法结构树。 我们提前将其编译成了机器码,CPU基于机器码运行
虚拟的机器码:
-
密集。 它是一块坚实连续的二进制数据块,没有一位被浪费。
-
线性。 指令被打成包,一条接一条地执行。不会在内存里到处乱跳(除非你的控制流代码真真这么干了)。
-
底层。 每条指令都做一件小事,有趣的行为从组合中诞生。
-
速度快。 综合所有这些条件(当然,也包括它直接由硬件实现这一事实),机器码跑得跟风一样快。
模式:
指令集 定义了可执行的底层操作。 一系列的指令被编码为字节序列。 虚拟机 使用 中间值栈 依次执行这些指令。 通过组合指令,可以定义复杂的高层行为。
何时使用:
这个模式应当用在你有许多行为需要定义,而游戏实现语言因为如下原因不适用时:
- 过于底层,繁琐易错。
- 编译慢或者其他工具因素导致迭代缓慢。
- 安全性依赖编程者。如果想保证行为不会破坏游戏,你需要将其与代码的其他部分隔开。
当然,该列表描述了一堆特性。谁不希望有更快的迭代循环和更多的安全性? 然而,世上没有免费的午餐。字节码比本地代码慢,所以不适合引擎的性能攸关的部分。
示例代码:
为了将法术编码进数据,储存了一数组enum值
enum Instruction
{
INST_SET_HEALTH = 0x00,
INST_SET_WISDOM = 0x01,
INST_SET_AGILITY = 0x02,
INST_PLAY_SOUND = 0x03,
INST_SPAWN_PARTICLES = 0x04
};
switch (instruction)
{
case INST_SET_HEALTH:
setHealth(0, 100);
break;
case INST_SET_WISDOM:
setWisdom(0, 100);
break;
case INST_SET_AGILITY:
setAgility(0, 100);
break;
case INST_PLAY_SOUND:
playSound(SOUND_BANG);
break;
case INST_SPAWN_PARTICLES:
spawnParticles(PARTICLE_FLAME);
break;
}
用这种方式,解释器建立了沟通代码世界和数据世界的桥梁。我们可以像这样将其放进执行法术的虚拟机:
class VM
{
public:
void interpret(char bytecode[], int size)
{
for (int i = 0; i < size; i++)
{
char instruction = bytecode[i];
switch (instruction)
{
// 每条指令的跳转分支……
}
}
}
};
这样使用并不灵活,我们需要引入参数
栈式机器
要执行复杂的嵌套表达式,得先从最里面的子表达式开始。 计算完里面的,将结果作为参数向外流向包含它们的表达式, 直到得出最终结果,整个表达式就算完了。
switch (instruction)
{
case INST_SET_HEALTH:
{
int amount = pop();
int wizard = pop();
setHealth(wizard, amount);
break;
}
case INST_SET_WISDOM:
case INST_SET_AGILITY:
// 像上面一样……
case INST_PLAY_SOUND:
playSound(pop());
break;
case INST_SPAWN_PARTICLES:
spawnParticles(pop());
break;
}
设计师希望法术能表达规则,而不仅仅是数值
行为==组合
字节码 · Behavioral Patterns · 游戏设计模式 (tkchu.me)
还挺简单的,但是不好做笔记,就直接看原文吧
子类沙箱
用一系列由基类提供的操作定义子类中的行为。
模式
基类定义抽象的沙箱方法和几个提供的操作。 将操作标为protected,表明它们只为子类所使用。 每个推导出的沙箱子类用提供的操作实现了沙箱函数。
何时使用
子类沙箱模式是潜伏在代码库中简单常用的模式,哪怕是在游戏之外的地方亦有应用。 如果你有一个非虚的protected方法,你可能已经在用类似的东西了。 沙箱方法在以下情况适用:
-
你有一个能推导很多子类的基类。
-
基类可以提供子类需要的所有操作。
-
在子类中有行为重复,你想要更容易地在它们间分享代码。
-
你想要最小化子类和程序的其他部分的耦合。
在基类中实现子类可能会有的操作函数,然后通过继承传递给子类,将子类与外界的耦合转移到基类上面。但是基类可能会变得很庞大,这时候可以将操作分流出去,作为一个类,然后将这个类暴露给基类
如果Superpower
已经很庞杂了,我们也许想要避免这样。 取而代之的是创建SoundPlayer
类暴露该函数:
class SoundPlayer
{
void playSound(SoundId sound, double volume)
{
// 实现代码……
}
void stopSound(SoundId sound)
{
// 实现代码……
}
void setVolume(SoundId sound)
{
// 实现代码……
}
};
Superpower
提供了对其的接触:
class Superpower
{
protected:
SoundPlayer& getSoundPlayer()
{
return soundPlayer_;
}
// 沙箱方法和其他操作……
private:
SoundPlayer soundPlayer_;
};
将提供的操作分流到辅助类可以为你做一些事情:
-
减少了基类中的方法。 在这里的例子中,将三个方法变成了一个简单的获取函数。
-
在辅助类中的代码通常更好管理。 像
Superpower
的核心基类,不管意图如何好,它被太多的类依赖而很难改变。 通过将函数移到耦合较少的次要类,代码变得更容易被使用而不破坏任何东西。 -
减少了基类和其他系统的耦合度。 当
playSound()
方法直接在Superpower
时,基类与SoundId
以及其他涉及的音频代码直接绑定。 将它移动到SoundPlayer
中,减少了Superpower
与SoundPlayer
类的耦合,这就封装了它其他的依赖。
参见
-
当你使用更新模式时,你的更新函数通常也是沙箱方法。
-
这个模式与模板方法正相反。 两种模式中,都使用一系列受限操作实现方法。 使用子类沙箱时,方法在推导类中,受限操作在基类中。 使用模板方法时,基类 有方法,而受限操作在推导类中。
-
你也可以认为这个模式是外观模式的变形。 外观模式将一系列不同系统藏在简化的API后。使用子类沙箱,基类起到了在子类前隐藏整个游戏引擎的作用。
类型对象
创建一个类A来允许灵活地创造新“类型”,类A的每个实例都代表了不同的对象类型
模式
定义类型对象类和有类型的对象类。每个类型对象实例代表一种不同的逻辑类型。 每种有类型的对象保存对描述它类型的类型对象的引用。
实例相关的数据被存储在有类型对象的实例中,被同种类分享的数据或者行为存储在类型对象中。 引用同一类型对象的对象将会像同一类型一样运作。 这让我们在一组相同的对象间分享行为和数据,就像子类让我们做的那样,但没有固定的硬编码子类集合。