本篇博客是在 AI 工具 Claude 的辅助下完成。
常见的设计模式有以下几类:
创建型模式
:这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,比如工厂方法模式
、抽象工厂模式
、单例模式
、原型模式
等。结构型模式
:这些设计模式关注类和对象的组合,使他们之间的相互关系更加清晰,比如适配器模式
、装饰器模式
、代理模式
、外观模式
、桥接模式
、组合模式
等。行为型模式
:这些设计模式专注点关注对象之间的通信,比如观察者模式
、迭代器模式
、模板方法模式
、策略模式
、命令模式
、职责链模式
、访问者模式
、中介者模式
等。
总体来说,常用设计模式有20多个,在游戏开发中比较常用的设计模式有以下几种:
- 单例模式
- 工厂方法模式
- 适配器模式
- 装饰器模式
- 观察者模式
- 策略模式
- 模板方法模式
- 代理模式
- 状态模式
- 命令模式
灵活使用设计模式可以使游戏开发更加模块化、扩展性更好并易于维护。
1. 单例模式
单例模式(Singleton Pattern),确保一个类只有一个实例,并提供一个全局访问点。
其主要目的是控制资源的数量,实现共享资源的控制。
1.1 基本结构
单例模式通常包含以下角色:
- 单例类:负责创建和管理单例对象,控制客户端对单例对象的访问。
- 单例对象:单例类创建的唯一对象。
单例模式的实现方式主要有三种:
1.1.1 饿汉式
线程安全,调用效率高,但始终占用内存。
class Singleton {
private static instance: Singleton = new Singleton();
private constructor() { }
public static getInstance(): Singleton {
return Singleton.instance;
}
}
let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2); // true
1.1.2 懒汉式
线程不安全,调用效率高,起初不占用内存。
class Singleton {
private static instance: Singleton;
private constructor() { }
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2); // true
1.1.3 双重校验锁
线程安全,调用效率高,起初不占用内存。
// singleton.ts
let instance: Singleton;
export class Singleton {
private constructor() { }
public static getInstance(): Singleton {
if (!instance) {
instance = new Singleton();
}
return instance;
}
}
这里利用了 TypeScript
的模块化特性来实现线程安全。因为在编译期,这个模块会被编译为一个单独的作用域,所以即使有多个线程同时调用 getInstance
方法,也不会产生竞争条件,从而创建多个实例。
// main.ts
import { Singleton } from './singleton';
let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2); // true
1.2 优缺点
单例模式的主要优点是:
- 控制资源的数量。
- 节省系统资源。
- 实现线程安全。
单例模式的主要缺点是:
- 扩展性差,一个类只能有一个实例。
- 测试难度大,无法 mock。
- 与单一职责原则冲突,一个类既充当了工厂角色又充当了产品角色。
所以,单例模式在需要 严格控制资源数量
和 实现共享数据集
的场景中比较适用。但由于扩展性差,在可能频繁变化的场景中要慎用。
1.3 适用场景
在游戏开发中,单例模式常用于 资源管理器
、对象池
、游戏逻辑
等只需要一个实例的系统。
1.3.1 资源管理器
class ResourceManager {
private static instance: ResourceManager;
private resources: Map<string, any> = new Map();
private constructor() { }
public static getInstance(): ResourceManager {
if (!ResourceManager.instance) {
ResourceManager.instance = new ResourceManager();
}
return ResourceManager.instance;
}
public loadResource(name: string, resource: any) {
this.resources.set(name, resource);
}
public getResource(name: string) {
return this.resources.get(name);
}
}
// 使用
let manager = ResourceManager.getInstance();
manager.loadResource("玩家资源", playerResource);
let resource = manager.getResource("玩家资源");
1.3.2 对象池
class ObjectPool {
private static instance: ObjectPool;
private pool: Obj[] = [];
private constructor() { }
public static getInstance(): ObjectPool {
if (!ObjectPool.instance) {
ObjectPool.instance = new ObjectPool();
}
return ObjectPool.instance;
}
public allocObject() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return new Obj();
}
public releaseObject(obj: any) {
this.pool.push(obj);
}
}
// 使用
let pool = ObjectPool.getInstance();
let obj1 = pool.allocObject();
pool.releaseObject(obj1);
let obj2 = pool.allocObject();
1.3.3 游戏逻辑管理器
class GameLogic {
private static instance: GameLogic;
private constructor() { }
public static getInstance(): GameLogic {
if (!GameLogic.instance) {
GameLogic.instance = new GameLogic();
}
return GameLogic.instance;
}
public update() {
// 单例更新游戏逻辑
}
}
// 调用
let logic = GameLogic.getInstance();
logic.update();
所以,单例模式在游戏开发中可以保证一些系统/管理器 全局唯一
,并为整个游戏提供接口。它的应用场景比较广泛,使得游戏的逻辑和资源得以良好地管理和共享。
2. 工厂方法模式
工厂方法模式(Factory Method Pattern),是一种创建型设计模式,它提供一个创建对象的接口,但由子类决定要实例化的类是哪一个。
工厂方法模式让类的实例化推迟到子类中进行。
2.1 基本结构
工厂方法模式通常包含以下主要角色:
Product
:抽象产品类,定义了产品的接口。ConcreteProduct
:具体产品类,实现了抽象产品类。Factory
:抽象工厂类,定义了创建产品的接口,由子类实现。ConcreteFactory
:具体工厂类,重写了抽象工厂类的方法,创建具体产品类的实例。
其基本结构如下:
AbstractProduct
+ ProductA
+ ProductB
AbstractFactory
+ ConcreteFactory1
+ ConcreteFactory2
ConcreteFactory1.createProduct() => ProductA
ConcreteFactory2.createProduct() => ProductB
客户端通过 ConcreteFactory
来创建不同的 ConcreteProduct
对象,而无需知道具体产品的类名。
2.2 优缺点
工厂方法模式的主要优势有:
- 它可以将客户端与具体产品类解耦,客户端不再需要知道具体产品类的类名。
- 它可以将产品创建的过程交给子类来实现,以便支持新的产品类型。
- 它增加了系统的灵活性,可以根据条件创建不同的产品对象。
缺点有:
- 类的个数较多,结构复杂。
- 增加了系统的抽象性和理解难度。
2.3 适用场景
工厂方法模式适用场景:
- 系统中有多个产品族,而系统只消费其中某一族产品。
- 要提供给客户端的具体产品类在编译期不确定,而是在运行期确定。
- 客户端不需要知道创建产品过程的细节。
在游戏开发中,工厂方法模式常用于创建不同类型的游戏对象和资源。
不同的工厂创建不同类型的对象。例如:
2.3.1 创建不同类型的敌人
interface Enemy {
attack(): void;
move(): void;
}
class Goblin implements Enemy {
attack() {
console.log('弓箭攻击!');
}
move() {
console.log('移动到玩家身边!');
}
}
class Orc implements Enemy {
attack() {
console.log('利斧攻击!');
}
move() {
console.log('向玩家冲锋!');
}
}
abstract class EnemyFactory {
public abstract createEnemy(): Enemy;
}
class GoblinFactory extends EnemyFactory {
public createEnemy(): Enemy {
return new Goblin();
}
}
class OrcFactory extends EnemyFactory {
public createEnemy(): Enemy {
return new Orc();
}
}
let goblinFactory = new GoblinFactory();
let goblin = goblinFactory.createEnemy();
goblin.attack(); // 弓箭攻击!
let orcFactory = new OrcFactory();
let orc = orcFactory.createEnemy();
orc.move(); // 向玩家冲锋!
2.3.2 创建不同类型的道具
interface Item {
use(): void;
}
class HealthPotion implements Item {
use() {
console.log('用药回血!');
}
}
class ManaPotion implements Item {
use() {
console.log('用药回魔!');
}
}
abstract class ItemFactory {
public abstract createItem(): Item;
}
class HealthPotionFactory extends ItemFactory {
public createItem(): Item {
return new HealthPotion();
}
}
class ManaPotionFactory extends ItemFactory {
public createItem(): Item {
return new ManaPotion();
}
}
let healthPotionFactory = new HealthPotionFactory();
let healthPotion = healthPotionFactory.createItem();
healthPotion.use(); // 用药回血!
let manaPotionFactory = new ManaPotionFactory();
let manaPotion = manaPotionFactory.createItem();
manaPotion.use(); // 用药回魔!
工厂方法模式用于创建不同类型的游戏对象,通过不同的工厂类实例化不同的具体产品类,进而获取不同类型的对象实例。
它使得对象的创建和使用分离,降低了系统的耦合度,增强了可扩展性。
2.4 补充讲解
这里以游戏开发中创建不同类型的敌人为例,分别使用 工厂方法模式
和 非工厂方法模式
两种方法实现,通过对比二者使用上的区别,来更好的感受工厂方法模式的优势。
不使用工厂方法模式实现:
interface Enemy {
name: string;
attack(): void;
}
class Skeleton implements Enemy {
name = 'Skeleton';
attack() {
console.log('Skeleton attacks!');
}
}
class Zombie implements Enemy {
name = 'Zombie';
attack() {
console.log('Zombie attacks!');
}
}
function createEnemy(type: string): Enemy {
if (type === 'skeleton') {
return new Skeleton();
} else if (type === 'zombie') {
return new Zombie();
}
}
let enemy1 = createEnemy('skeleton');
let enemy2 = createEnemy('zombie');
使用工厂方法模式实现:
interface Enemy {
name: string;
attack(): void;
}
interface EnemyFactory {
createEnemy(type: string): Enemy;
}
class Skeleton implements Enemy {
name = 'Skeleton';
attack() {
console.log('Skeleton attacks!');
}
}
class Zombie implements Enemy {
name = 'Zombie';
attack() {
console.log('Zombie attacks!');
}
}
class EnemyFactoryImpl implements EnemyFactory {
createEnemy(type: string): Enemy {
if (type === 'skeleton') {
return new Skeleton();
} else if (type === 'zombie') {
return new Zombie();
}
}
}
let factory: EnemyFactory = new EnemyFactoryImpl();
let enemy1 = factory.createEnemy('skeleton');
let enemy2 = factory.createEnemy('zombie');
两种方式都可以实现所需功能,而且似乎不使用工厂方法模式的代码更加简洁。
那么工厂方法模式的优势如何体现呢?
这里我们以添加一个新的 Boss
敌人类型为例,演示在使用和不使用工厂方法模式的情况下,如何修改代码:
不使用工厂方法模式,当添加一个新的 Boss
敌人时,我们需要:
- 定义
Boss
类:
class Boss implements Enemy {
name = 'Boss';
attack() {
console.log('Boss attacks!');
}
}
- 修改
createEnemy
方法来创建Boss
实例:
function createEnemy(type: string): Enemy {
if (type === 'skeleton') {
return new Skeleton();
} else if (type === 'zombie') {
return new Zombie();
} else if (type === 'boss') {
return new Boss();
}
}
- 客户端调用
createEnemy('boss')
来创建Boss
实例:
let enemy1 = createEnemy('skeleton');
let enemy2 = createEnemy('zombie');
let boss = createEnemy('boss');
所以在这个模式下,每添加新的敌人类型都需要修改 createEnemy
方法,违反开闭原则。
而使用工厂方法模式,当添加一个新的 Boss
敌人时,我们需要:
- 定义
Boss
类:
class Boss implements Enemy {
name = 'Boss';
attack() {
console.log('Boss attacks!');
}
}
- 创建
BossEnemyFactory
继承自EnemyFactory
来产生Boss
实例:
class BossEnemyFactory implements EnemyFactory {
createEnemy(type: string): Enemy {
if (type === 'boss') {
return new Boss();
}
}
}
- 客户端通过
BossEnemyFactory
来创建Boss
实例,原有代码不变:
let factory: EnemyFactory = new EnemyFactoryImpl();
let enemy1 = factory.createEnemy('skeleton');
let enemy2 = factory.createEnemy('zombie');
let bossFactory = new BossEnemyFactory();
let boss = bossFactory.createEnemy('boss');
在这个模式下,每添加新的敌人类型,只需要创建一个新的对象工厂来生产新的敌人对象即可,无需改动原有代码。
通过上面例子的对比,我们可以感受到,工厂方法模式的优势主要体现在:
- 扩展性:使用工厂方法模式的系统更易扩展,可以在不修改原有代码的基础上添加新的产品(例如添加新的敌人类型)。而不使用工厂方法模式的系统则需要修改
createEnemy
方法来支持新的产品类型,违反了开闭原则。 - 解耦性:使用工厂方法模式的客户端与具体产品类解耦,如果有新的产品加入,客户端的代码不需要任何修改。而不使用工厂方法模式的客户端则依赖于具体产品类,当有新的产品加入时可能需要修改客户端代码。
- 复用性:可以重用抽象工厂类,并通过子类来创建不同系列的产品对象。非工厂方法模式不具备这种灵活性。
所以,使用工厂方法模式通过封装变化点,产生可重用的设计和体系结构,可以带来更高的 扩展性
、解耦性
和 复用性
,这也是设计模式的目标。
3. 适配器模式
适配器模式(Adapter Pattern)是一种结构型模式,它能使接口不兼容的类可以一起工作。它通常通过包装一个接口来实现这一功能,而不是直接修改已有的接口。
适配器模式主要用来吸收两个已经存在的接口之间的差异,解决因接口不匹配所造成的类的兼容问题。它通过对现有接口的适配,使得原本因接口不同而无法协同工作的类可以协同工作,最大限度地重用已有的类,而不必修改原有代码。
3.1 基本结构
适配器模式包含以下主要要素:
Target
:目标接口,需要适配的接口。Adaptee
:需要适配的现有接口。Adapter
:适配器类,通过包装Adaptee
对象,把源接口转换成目标接口。
其类图结构如下:
Target
+ ConcreteTarget
Adaptee
+ ConcreteAdaptee
Adapter
- adaptee: Adaptee
- target: Target
工作原理是:客户端调用 Adapter
的方法,Adapter
再调用 Adaptee
转换成目标接口要求的方法,并把结果返回给客户端。
3.2 优缺点
适配器模式的主要优点有:
- 可以实现两个已经存在但接口不兼容的类的兼容工作。
- 增加了类的复用性。
- 实现开闭原则。
缺点有:
- 过多使用适配器会让系统变得复杂。
- 由于要包装现有类,所以会产生额外的开销。
3.3 适用场景
适配器模式的适用场景:
- 系统需要使用现有的类,但其接口不符合系统的需求。
- 想要创建一个可以复用的类,可以适配与不同的接口。
- 组合产品线中兼容性差异问题。
比如我们有一个武器接口和两个具体的武器类:
interface Weapon {
attack(): void;
}
class Sword implements Weapon {
attack() { console.log('剑攻击!'); }
}
class Axe implements Weapon {
attack() { console.log('斧攻击!'); }
}
此时,有一个游戏角色可以装备武器并进行攻击:
class Warrior {
weapon?: Weapon;
public equipWeapon(weapon: Weapon) {
this.weapon = weapon;
}
public attack() {
this.weapon?.attack();
}
}
然后,就可以使用了:
let sword = new Sword();
let axe = new Axe();
let warrior = new Warrior();
warrior.equipWeapon(sword);
warrior.attack(); // 剑攻击!
warrior.equipWeapon(axe);
warrior.attack(); // 斧攻击!
此时,加入一个新的武器类型 Bow(弓)
,其方法名为 shoot
而不是 attack
:
class Bow {
shoot() { console.log('射箭!') }
}
此时 Bow
类并不匹配 Weapon
接口,无法直接使用,Warrior
也无法直接装备 Bow
。
如果不适用适配器模式,我们可能需要对 Warrior
类进行修改,增加新的接口,如:
class Warrior {
weapon?: Weapon;
bow?: Bow;
public equipWeapon(weapon: Weapon) {
this.weapon = weapon;
}
public attack() {
this.weapon?.attack();
}
public equipBow(bow: Bow) {
this.bow = bow;
}
public shoot() {
this.bow?.shoot();
}
}
这样便破坏了开闭原则,不利于代码维护。
这时候我们可以使用适配器模式,定义一个 BowAdapter
来适配 Bow
到 Weapon
接口:
class BowAdapter implements Weapon {
bow?: Bow;
constructor(bow: Bow) {
this.bow = bow;
}
attack() {
this.bow?.shoot();
}
}
现在就可以这样使用了:
let bow = new Bow();
let bowAdapter = new BowAdapter(bow);
let warrior = new Warrior();
warrior.equipWeapon(bowAdapter);
warrior.attack(); // 射箭!
可以看到,BowAdapter
适配器将 Bow
适配到 Weapon
接口,使 Warrior
可以像使用其他常规武器一样使用 Bow
,而不需要修改 Warrior
的代码。
这就是适配器模式的典型应用场景,它通过构建一个适配器类作为两个已有接口之间的桥梁,使原本不匹配的类能够协同工作。最大限度地重用已有代码,无需修改原有系统。
4. 装饰器模式
装饰器模式(Decorator Pattern),是一种结构型模式。它是通过包装对象来实现扩展其功能,可以动态地给对象添加一些额外的职责,而不改变其原有的结构。
4.1 基本结构
装饰器模式包含以下主要元素:
Component
:抽象构件,定义对象的接口。ConcreteComponent
:具体构件,实现抽象构件。Decorator
:抽象装饰类,包含一个构件对象,并定义一个与构件接口相同的接口。ConcreteDecorator
:具体装饰类,继承抽象装饰类,并在调用构件方法的前后添加额外的方法。
其类图结构如下:
Component
+ ConcreteComponent
Decorator
+ ConcreteDecorator
- component: Component
客户端通过装饰器对构件对象进行包装,,从而增强构件对象的行为,但客户端看来却仍然与原有的构件对象有相同的接口。
4.2 优缺点
装饰器模式的主要优点有:
- 继承的替代方案。通过包装可以扩展对象的行为,而无需继承。
- 可以动态添加对象功能。通过嵌套不同装饰器可以添加多个扩展。
- 符合开闭原则。扩展系统功能不修改现有代码。
缺点有:
- 会产生许多装饰类,系统变得复杂。
- 动态装饰时,难以判断一个对象具有哪些扩展。
4.3 适用场景
装饰器模式的适用场景:
- 需要扩展对象的功能,但继承不合适。
- 动态地为对象增加职责,这些职责可以改变。
- 当不能采用继承的方式进行扩展时。
装饰器模式通过 动态包装对象
实现其功能扩展,这种灵活的方式可以避免继承导致的类爆炸问题,并满足开闭原则的要求。
在游戏开发中,装饰器模式常用于给游戏对象添加附加功能,而不改变其原有结构。
例如:给游戏角色添加 buff
。
// 定义角色接口
interface ICharacter {
attack(): void;
move(): void;
description(): string;
}
// 具体构件: 实现角色
class Character implements ICharacter {
attack() { console.log('攻击!'); }
move() { console.log('移动!'); }
description() { return '普通角色'; }
}
// 抽象装饰: 定义buff接口
abstract class CharacterDecorator implements ICharacter {
protected character: ICharacter;
constructor(character: ICharacter) {
this.character = character;
}
attack() {
this.character.attack();
}
move() {
this.character.move();
}
description() {
return this.character.description();
}
}
// 具体装饰: 速度提高buff
class SpeedBuff extends CharacterDecorator {
attack() {
console.log('速度提高,攻击!'); // 添加额外功能
super.attack();
}
move() {
console.log('速度提高,移动!'); // 添加额外功能
super.move();
}
description() {
return `${this.character.description()},速度提高`;
}
}
let character = new Character();
character = new SpeedBuff(character);
character.attack(); // 速度提高,攻击! 攻击!
character.move(); // 速度提高,移动! 移动!
console.log(character.description()); // 普通角色,速度提高
可以看到,SpeedBuff
装饰器给 Character
添加了 速度buff
,在不改变原 Character
类的基础上增加了额外功能,并通过组合的方式弥补了 Character
类的不足。
所以,装饰器模式就是动态地给一个对象添加额外的职责,而不用生成新的子类。通过使用对象组合,可以动态地扩展对象的功能。
再举一个例子。比如我们有一个游戏角色类:
// 定义角色接口
interface ICharacter {
name: string;
hp: number;
power: number;
attack(): void;
}
// 具体构件: 实现角色
class Character implements ICharacter {
name: string;
hp: number;
power: number;
constructor(name: string, hp: number, power: number) {
this.name = name;
this.hp = hp;
this.power = power;
}
attack() {
console.log(`${this.name}攻击,造成${this.power}点伤害!`);
}
}
let character = new Character('骑士', 100, 10);
character.attack(); // 骑士攻击,造成10点伤害!
现在我们想给这个角色添加 防御buff
和 攻击buff
,可以这样使用装饰器模式:
// 抽象装饰类 实现Character接口
abstract class CharacterDecorator implements ICharacter {
protected character: Character;
constructor(character: Character) {
this.character = character;
}
//实现接口方法 getter 和 setter
get name() { return this.character.name }
get hp() { return this.character.hp }
get power() { return this.character.power }
set name(value: string) { this.character.name = value; }
set hp(value: number) { this.character.hp = value; }
set power(value: number) { this.character.power = value; }
attack() {
this.character.attack();
}
}
// 具体装饰类 防御 buff
class DefenseDecorator extends CharacterDecorator {
attack() {
console.log('防御buff生效,受到的伤害减半!');
this.character.attack();
}
}
// 具体装饰类 攻击 buff
class AttackDecorator extends CharacterDecorator {
attack() {
console.log('攻击buff生效,伤害加倍!');
this.character.power *= 2;
this.character.attack();
this.character.power /= 2;
}
}
// 调用
let character = new Character('骑士', 100, 10);
character = new DefenseDecorator(character);
character = new AttackDecorator(character);
character.attack();
// 攻击buff生效,伤害加倍!
// 防御buff生效,受到的伤害减半!
// 骑士攻击,造成20点伤害!
可以看到,DefenseDecorator
给 Character
添加了 防御buff
,AttackDecorator
给 Character
添加了 攻击buff
。这两个装饰器动态地扩展了Character的功能,而不需要修改Character本身的代码。
Character
可以被多个装饰器包装,实现更加丰富的功能,而不改变其原有的结构和原有的功能,这就是装饰器模式的强大之处。
5. 观察者模式
观察者模式(Observer Pattern),定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。
5.1 基本结构
观察者模式的典型结构包含:
Subject
:抽象主题类,它维护了一个观察者集合,提供了增加、移除观察者的接口。ConcreteSubject
:具体主题类,它实现抽象主题类,在状态发生变化时通知所有的观察者。Observer
:抽象观察者类,它定义了一个更新接口,使得在主题状态发生变化时更新自己。ConcreteObserver
:具体观察者类,它实现抽象观察者类,并在主题状态发生变化时更新自己的状态。
5.2 优缺点
观察者模式的优点:
- 观察者和主题之间是抽象耦合的。
- 主题对象可以很容易地增加和删除观察者对象。
- 观察者模式建立了一套触发机制。
观察者模式的缺点:
- 如果一个主题有很多直接和间接的观察者,将所有的观察者通知到会花费很长时间。
- 如果在观察者和主题之间有循环依赖,观察者会被重复调用。
- 主题和观察者之间的依赖关系并不很清晰。
观察者模式在软件系统中广泛使用,比如事件处理系统、新闻出版系统等等。它提供了一种在软件系统之间低耦合的结构。
5.3 适用场景
观察者模式在游戏开发中常用于游戏对象之间的 消息传递
和 事件处理
,其中一个对象的状态变化会通知其他对象,使它们作出相应的反应。
例如:玩家升级时,通知跟随者也升级。
// 主题接口
interface ISubject {
// 增加观察者
attach(observer: IObserver): void;
// 删除观察者
detach(observer: IObserver): void;
// 通知观察者
notify(): void;
}
// 观察者接口
interface IObserver {
// 更新接口
update(): void;
}
// 主题类:玩家类
class Player implements ISubject {
// 观察者列表
observers: IObserver[] = [];
// 玩家等级
level = 1;
// 添加观察者
attach(observer: IObserver): void {
this.observers.push(observer);
}
// 删除观察者
detach(observer: IObserver): void {
this.observers = this.observers.filter(o => o != observer);
}
// 通知观察者
notify(): void {
// 通知所有观察者更新
this.observers.forEach(o => o.update());
}
// 升级
levelUp() {
this.level++;
this.notify();
}
}
// 观察者类:跟随者
class Follower implements IObserver {
name: string;
constructor(name: string) {
this.name = name;
}
// 更新状态
update(): void {
console.log(`${this.name}升至第${player.level}级!`);
}
}
// 客户端
let player = new Player();
let follower1 = new Follower('跟随者1');
let follower2 = new Follower('跟随者2');
// 玩家添加两个跟随者
player.attach(follower1);
player.attach(follower2);
// 玩家升级时,跟随者更新状态
player.levelUp();
// 跟随者1升至第2级!
// 跟随者2升至第2级!
// 删除跟随者2
player.detach(follower2);
// 玩家升级时,跟随者1更新状态
player.levelUp();
// 跟随者1升至第3级!
可以看到,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。 Player
作为 主题(Subject)
,Follower
作为 观察者(Observer)
,多个 Follower
可以同时监听一个 Player
。
主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。当 Player
升级时,会通知跟随者也作出升级的反应。
6. 策略模式
策略模式(Strategy Pattern),是一种行为型设计模式,它定义了一系列的算法,并分别封装起来,让它们之间可以互相替换,策略模式可以让算法的变化独立于使用算法的客户。
6.1 基本结构
策略模式的典型结构包含:
Strategy
:抽象策略类,定义了一个公共接口,各种不同的算法以不同的方式实现这个接口。ConcreteStrategy
:具体策略类,实现了抽象策略类,封装了具体的算法或行为。Context
:环境类,维护一个策略对象,最终给客户端调用。
6.2 优缺点
策略模式的优点:
- 可以很好地管理和组织多个相关的算法。每种算法类之间相互独立,可以相互替换。
- 扩展方便。新增策略时,只需增加一个新的策略类,不影响现有的环境类和策略类。
- 提供继承的代替方案。若使用继承来扩展算法,将产生许多衍生类,不易维护。
- 避免使用条件语句。
策略模式的缺点:
- 客户端复杂度增大。因为客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 可能产生许多策略类,增加程序的大小。
6.3 适用场景
策略模式的适用场景:
- 一个系统有许多类,它们的行为随着外部条件改变而改变。
- 一个系统需要动态地在几种算法中选择一种。
在游戏开发中,策略模式常用于实现不同的 算法
、行为
方式。
例如:定义不同的移动算法,玩家可以在运行时选择使用
// 定义移动策略接口类
interface IMovementStrategy {
move(): void;
}
// 步行移动策略类
class WalkingStrategy implements IMovementStrategy {
move() {
console.log('步行移动');
}
}
// 跑步移动策略类
class RunningStrategy implements IMovementStrategy {
move() {
console.log('跑步移动');
}
}
// 跳跃移动策略类
class JumpingStrategy implements IMovementStrategy {
move() {
console.log('跳跃移动');
}
}
// 玩家类
class Player {
movementStrategy?: IMovementStrategy;
// 选择移动策略
setMovementStrategy(strategy: IMovementStrategy) {
this.movementStrategy = strategy;
}
// 移动
move() {
this.movementStrategy?.move();
}
}
// 客户端调用
let player = new Player();
player.setMovementStrategy(new WalkingStrategy());
player.move(); // 步行移动
player.setMovementStrategy(new RunningStrategy());
player.move(); // 跑步移动
player.setMovementStrategy(new JumpingStrategy());
player.move(); // 跳跃移动
可以看到,Player
通过 setMovementStrategy()
方法在运行时选择不同的移动策略( WalkingStrategy
、RunningStrategy
、JumpingStrategy
),并在 move()
方法中调用当前策略的算法来执行移动。
再举一个例子来深入解释策略模式。
比如在游戏中,我们有不同的敌人AI策略(普通敌人、精英敌人、BOSS敌人),玩家在不同区域会遇到不同的敌人。
我们可以这样定义:
// 敌人AI策略接口类
interface IEnemyStrategy {
move(): void;
attack(): void;
}
// 普通敌人AI策略类
class NormalEnemyStrategy implements IEnemyStrategy {
move() {
console.log('普通敌人移动');
}
attack() {
console.log('普通敌人攻击');
}
}
// 精英敌人AI策略类
class EliteEnemyStrategy implements IEnemyStrategy {
move() {
console.log('精英敌人移动');
}
attack() {
console.log('精英敌人攻击');
}
}
// BOSS敌人AI策略类
class BossEnemyStrategy implements IEnemyStrategy {
move() {
console.log('BOSS敌人移动');
}
attack() {
console.log('BOSS敌人攻击');
}
}
然后我们有一个敌人类,在不同的区域会选择不同的 AI 策略:
// 敌人类
class Enemy {
strategy?: IEnemyStrategy;
// 选择AI策略
setStrategy(strategy: IEnemyStrategy) {
this.strategy = strategy;
}
// 移动
move() {
this.strategy?.move();
}
// 攻击
attack() {
this.strategy?.attack();
}
}
// 客户端调用
let enemy1 = new Enemy();
enemy1.setStrategy(new NormalEnemyStrategy());
enemy1.move(); // 普通敌人移动
enemy1.attack(); // 普通敌人攻击
let enemy2 = new Enemy();
enemy2.setStrategy(new EliteEnemyStrategy());
enemy2.move(); // 精英敌人移动
enemy2.attack(); // 精英敌人攻击
let enemy3 = new Enemy();
enemy3.setStrategy(new BossEnemyStrategy());
enemy3.move(); // BOSS敌人移动
enemy3.attack(); // BOSS敌人攻击
在这个例子中,策略模式的优势是非常明显的:
- 策略类和敌人类是松耦合的,将算法的变化封装在策略类里面,策略可以独立于敌人进行扩展和改变。耦合性低,扩展性强。
- 可以根据不同的情况动态地选择不同的策略,有很强的灵活性。
得益于这些优势,策略模式是实现 游戏对象智能
和 扩展性
的重要手段之一。
6.4 补充讲解
为了更好的理解策略模式的优势,我们举个例子。
假设我们有一个计算器,它可以执行加法运算、减法运算、乘法运算、除法运算等等。
不用策略模式的话,我们可以这样实现:
// 计算器类
class Calculator {
// 计算方法
calc(operator: string, a: number, b: number): number {
if (operator === "+") {
return a + b;
} else if (operator === "-") {
return a - b;
} else if (operator === "*") {
return a * b;
} else if (operator === "/") {
return a / b;
}
// ...
return 0;
}
}
// 客户端调用
let calculator = new Calculator()
console.log(calculator.calc("+", 2, 3)); // 5
console.log(calculator.calc("-", 7, 2)); // 5
这种实现方式使用了条件语句选择不同的算法,如果以后算法增加,需要不断修改这个方法,这显然是不符合开闭原则的。
而使用策略模式的话,我们可以这样实现:
首先定义一个抽象策略接口类 IStrategy
,并实现加法、减法等具体策略类。
// 抽象策略类
interface IStrategy {
calc(a: number, b: number): number;
}
// 具体策略类 加法
class AddStrategy implements IStrategy {
calc(a: number, b: number): number {
return a + b;
}
}
// 具体策略类 减法
class SubStrategy implements IStrategy {
calc(a: number, b: number): number {
return a - b;
}
}
然后实现计算器类 Calculator
。
// 环境类 计算器类
class Calculator {
private strategy: IStrategy;
// 设置算法策略
public setStrategy(strategy: IStrategy) {
this.strategy = strategy;
}
// 计算
public calc(a: number, b: number): number {
return this.strategy.calc(a, b);
}
}
客户端调用时,可以选择不同的算法策略来进行运算。
// 客户端调用
let calculator = new Calculator();
// 加法
calculator.setStrategy(new AddStrategy());
console.log(calculator.calc(1, 2)); // 3
// 减法
calculator.setStrategy(new SubStrategy());
console.log(calculator.calc(1, 2)); // -1
当算法变化时,我们只需要增加新的策略类,而环境类和已有策略类不需要任何修改,这保证了策略模式具有良好的可扩展性和灵活性。
例如,在上述代码基础上,增加乘法运算策略时。
// 新增策略类 乘法
class MultiStrategy implements IStrategy {
calc(a: number, b: number): number {
return a * b;
}
}
// 客户端调用
let calculator = new Calculator();
// 乘法
calculator.setStrategy(new MultiStrategy());
console.log(calculator.calc(2, 3)); // 6
无需修改已有的代码,具有良好的可扩展性和灵活性。
7. 模板方法模式
模板方法模式(Template Method Pattern),是一种行为型设计模式。在抽象类中定义模板方法,该模板方法中包含了一些步骤,而每个步骤又调用了抽象类中的抽象方法。子类可以重写抽象方法,但是必须按照模板方法中的步骤顺序执行。
举个简单的例子:
把大象装冰箱需要几步,第一,打开冰箱门,第二步,把大象放进去,第三步,关上冰箱门。
这便是典型的模板方法模式。不论把大象放冰箱,还是把鸡蛋放冰箱,都需要这三个步骤,这个便是模板方法;放不同的东西进冰箱,第二步实现的具体细节不同,这些具体步骤由子类重写实现。
7.1 基本结构
模板方法模式的基本结构包含以下几个要素:
抽象类(Abstract Class):定义抽象方法和模板方法。
- 模板方法:定义算法骨架,包含一些步骤,这些步骤会调用抽象方法。
- 抽象方法:抽象步骤,由子类实现。
- 钩子方法:在模板方法中已经实现,但允许子类加以重写的方法。
- 具体子类(Concrete Class):继承抽象类,实现抽象方法。
- 具体实例(Objects):子类的具体对象,执行模板方法。
模板方法模式可以看作是一种 稳定骨架,可变细节
的设计思想。它通过抽象定义算法模板,将算法的不变部分的实现封装在父类中,而变化的部分的实现则由子类来完成,从而封装变化、避免重复、控制继承树。这就是模板方法模式的核心思想。
7.2 优缺点
模板方法模式的优点:
- 提高代码复用性,子类只需要实现父类约定的方法。
- 扩展方便,可以通过继承扩展算法步骤,不影响其他子类。
- 调用简单,只需要调用父类的模板方法,具体子类的实现被隐藏
模板方法模式的缺点:
- 类的数量增加。需要定义抽象类和具体子类,导致类的数量增加。
- 初始学习成本较大。模板方法模式的结构和实现都较为复杂,初始学习难度较大。
7.3 适用场景
模板方法模式的适用场景:
- 有多个子类共有的方法,且逻辑相同。
- 重要的、复杂的算法,可以抽象出公共的模板方法,而将可变的部分延迟到子类中。
- 控制子类的扩展。模板方法定义了算法的完整流程,子类只能在指定的位置执行指定的步骤。
在游戏开发中,模板方法模式有着广泛的应用,如:角色行为、生命周期、地图生成以及资源加载等方面。它实现了算法流程的统一,避免了代码重复,同时也为各种变化留出了扩展点。这使得游戏开发可以得到很高的复用性和灵活性。
例如:游戏对象行为控制。
abstract class Movement {
// 模板方法,定义算法骨架
public move() {
this.start();
this.walk();
this.run();
this.otherAction();
this.end();
}
protected start() {
console.log('开始移动');
}
protected end() {
console.log('移动结束');
}
protected otherAction() {
// 钩子方法,方便扩展
}
// 定义抽象方法,由子类实现
protected abstract walk(): void;
protected abstract run(): void;
}
class PlayerMovement extends Movement {
// 重写抽象方法,定义玩家移动行为
protected walk() {
console.log('玩家步行');
}
protected run() {
console.log('玩家跑步');
}
protected jump() {
console.log('玩家跳跃');
}
protected otherAction() {
this.jump();
}
}
class MonsterMovement extends Movement {
// 重写抽象方法,定义怪物移动行为
protected walk() {
console.log('怪物走路');
}
protected run() {
console.log('怪物奔跑');
}
}
let player = new PlayerMovement();
player.move();
// 开始移动
// 玩家步行
// 玩家跑步
// 玩家跳跃
// 移动结束
let monster = new MonsterMovement();
monster.move();
// 开始移动
// 怪物走路
// 怪物奔跑
// 移动结束
可以看到,Movement
定义了角色移动算法的框架,而将 步行
、跑步
等具体实现推迟到子类。
PlayerMovement
和 MonsterMovement
继承 Movement
并重定义了某些方法,实现了不同游戏对象的移动方式。此外,PlayerMovement
通过重写钩子方法 otherAction()
为玩家类添加了 跳跃
的行为。
所以,模板方法模式在基类中定义对象的行为框架,而将部分算法的细节延迟到子类实现,这可以大大提高代码 复用性
,同时允许子类灵活变化,非常适用于游戏对象的行为封装和扩展。
8. 代理模式
代理模式(Proxy Pattern),是一种结构型设计模式。给某一个对象提供一个代理对象,并由代理对象控制对实际对象的访问,客户端通过代理对象间接地访问目标对象。代理对象负责请求的预处理和过滤,可以起到保护目标对象的作用。
8.1 基本结构
代理模式的典型结构是:
- 抽象主题(Subject)角色:定义实际主题和代理的公共接口。
- 实际主题(Real Subject)角色:定义代理所代表的实际对象。
- 代理(Proxy)角色:内部包含对实际主题的引用,并提供与实际主题相同的接口。
抽象主题(Subject)
│
│ 代理(Proxy)
│
└─ 真实主题(RealSubject)
代理模式的主要特点是:
- 代理对象与目标对象具有相同的接口,所以代理对象可以替代目标对象。
- 代理对象在访问目标对象之前和之后可以执行一些相关操作,如延迟加载、访问控制、缓存等。
- 代理对象可以隐藏目标对象的细节信息。
8.2 优缺点
代理模式的优点:
- 可以在访问真实对象之前进行权限控制或其他功能处理。
- 可以在访问真实对象之后进行缓存处理,避免重复操作。
- 可以对真实对象进行延迟加载,在需要时才创建。
- 可以隐藏真实对象的复杂度,提供简单接口。
主要缺点有:
- 额外开销:由于代理对象需要干预所有对目标对象的访问,所以会带来一定的性能开销。
- 引入复杂度:代理对象本身也比较复杂,需要维护对目标对象的引用,预处理请求等逻辑。这增加了系统的复杂性。
8.3 适用场景
代理模式通过引入代理对象来控制真实对象的访问,它经常用于 延迟加载
、访问控制
、缓存
等场景,有效地提高系统性能,是软件设计中比较常用和重要的模式之一。
在游戏开发中,代理模式常用于控制游戏对象的访问,实现延迟加载和缓存等功能。
例如:使用代理实现游戏对象的延迟加载。
// 定义游戏角色接口
interface IGameCharacter {
fight(): void;
}
// 实现游戏角色的实际对象
class Character implements IGameCharacter {
constructor(public name: string) { }
fight() {
console.log(`${this.name} 正在战斗!`);
}
}
// 实现游戏角色的代理对象
class ProxyCharacter implements IGameCharacter {
// 定义对实际对象的引用,初始为空
private character?: Character;
constructor(public name: string) { }
fight() {
// 首次请求时创建对象,实现延迟加载
if (!this.character) {
this.character = new Character(this.name);
}
this.character.fight();
}
}
//客户端调用,创建代理角色对象
let proxy = new ProxyCharacter('小明');
// 第一次调用战斗方法
proxy.fight(); // 小明 正在战斗!
// 第二次调用战斗方法
proxy.fight(); // 小明 正在战斗!
可以看到,ProxyCharacter
是一个代理类,代理 Character
真实对象。在第一次访问时,它才会创建真实的 Character
对象,而不是在一开始就创建所有的对象,这可以节省系统资源,提高性能。
9. 状态模式
状态模式(State Pattern),是一种行为型设计模式。它允许一个对象在其内部状态改变时改变其行为,对象看起来似乎改变了其类。
9.1 基本结构
状态模式的基本思想是:当对象的内部状态改变时,对象的行为也随之改变。这个状态的改变是动态的,根据环境的改变来切换状态。
状态模式通常包括以下主要角色:
- 环境(Context):也称为上下文,它定义了客户端使用的接口,并且负责具体状态的切换。
- 抽象状态(State):定义一个接口以封装与环境有关的行为。
- 具体状态(Concrete State):实现抽象状态接口并实现与环境有关的具体行为。
状态模式的结构如下:
环境(Context)
│
│ 抽象状态(State)
│ │
│ ├─ 具体状态 A(ConcreteStateA)
│ └─ 具体状态 B(ConcreteStateB)
│ ...
9.2 优缺点
状态模式的主要优点:
- 简洁:与条件语句相比,可读性更高、结构更清晰。
- 扩展性:易于增加新状态和转换。
主要缺点有:
- 复杂度:状态类数量过多时会过于复杂。
- 耦合:环境与状态类耦合度高。
- 开闭原则:增加新状态时,环境类需要修改。
9.3 适用场景
使用状态模式时需要考虑以下几点:
- 状态变化是否复杂?如果状态转换比较简单,使用状态模式得不偿失。
- 是否需要频繁添加新的状态和转换?如果状态较稳定,状态模式优点不明显。
- 是否可以减少环境与状态的耦合?我们可以在环境中持有状态的抽象,而非具体状态。
- 是否可以同时满足扩展性和开闭原则?可以考虑使用状态模式与策略模式组合。
- 采取措施减轻状态模式带来的影响,如控制状态类数量,提高状态类的可重用性等。
在游戏开发中,状态模式经常用来表示游戏对象的 状态机
,当对象的状态改变时,对象的行为也会跟着改变。这使得游戏对象可以有更加真实和复杂的表现。
例如,可以实现游戏对象的状态机:
// 定义角色状态枚举
enum State {
Standing, // 站立
Running, // 跑动
Jumping // 跳跃
}
class Character {
state: State = State.Standing;
// 设置状态
setState(state: State) {
this.state = state;
}
// 移动
move() {
switch (this.state) {
case State.Standing:
console.log('站立状态移动');
break;
case State.Running:
console.log('跑动状态移动');
break;
case State.Jumping:
console.log('跳跃状态移动');
break;
}
}
}
// 客户端调用
let character = new Character();
character.setState(State.Running);
character.move(); // 跑动状态移动
character.setState(State.Jumping);
character.move(); // 跳跃状态移动
可以看到,Character
的行为 (move()
方法) 会随着状态 ( standing
、running
、jumping
) 的改变而改变。这使得对象看起来就像改变了类一样。
9.4 补充讲解
9.4.1 如何添加新状态
还用前面的例子,如何给角色添加新的状态 Attack 攻击
。
// 定义角色状态枚举
enum State {
Standing,
Running,
Jumping,
Attack // 新增状态,攻击
}
class Character {
...
// 移动
move() {
switch (this.state) {
...
case State.Attack:
console.log('攻击状态');
break;
}
}
}
// 客户端调用
let character = new Character();
character.setState(State.Attack);
character.move(); // 攻击状态
9.4.2 状态模式和策略模式的区别
状态模式和策略模式都是行为模式。
状态模式:
- 封装状态转换逻辑,实现行为随状态而变化。
- 根据状态来决定执行哪个行为。
- 状态变化影响执行的行为。
策略模式:
- 封装算法家族,可以在运行时选择不同算法实现。
- 固定的上下文根据策略来决定使用哪个算法。
- 策略的改变影响所使用的算法。
两者区别的关键在于状态和策略本身的区别:
- 状态模式注重状态之间的切换以及行为的改变,而策略模式更关注在一个固定环境下选择不同的算法来实现行为。
- 状态模式中同时只能处于一种状态。而策略模式可以同时使用多个策略。
- 状态模式中环境类通常会有多个状态类的实例,并且环境类的行为会随着状态的改变而改变。而在策略模式中,环境类具有固定的行为,只是具体的算法实现会改变。
- 增加新状态时,环境类要做相应的调整,增加新策略时,不需要改变环境类。
从这几个方面可以看出,。两者都是用于实现系统的动态行为,但是侧重点不同。
状态模式——状态变化 → 行为变化
策略模式——算法选择 → 行为实现
9.4.3 状态模式和策略模式结合使用
可以结合使用状态模式和策略模式,最大限度地发挥每种模式的优势。
例如在前面的例子中,使用策略模式的思想,使其符合开闭原则。
可以这样设计:
定义状态抽象类。
abstract class State {
// 进入该状态
enter() { }
// 退出该状态
exit() { }
// 执行状态行为
execute() { }
}
定义三种具体状态类:
class StandState extends State {
enter() {
console.log('进入站立状态');
}
exit() {
console.log('退出站立状态');
}
execute() {
console.log('站立状态移动');
}
}
class RunState extends State {
enter() {
console.log('进入跑动状态');
}
exit() {
console.log('退出跑动状态');
}
execute() {
console.log('跑动状态移动');
}
}
class JumpState extends State {
enter() {
console.log('进入跳跃状态');
}
exit() {
console.log('退出跳跃状态');
}
execute() {
console.log('跳跃状态移动');
}
}
环境(游戏角色)类:
class Character {
state?: State;
// 设置状态
setState(state: State) {
this.state?.exit();
this.state = state;
this.state.enter();
}
// 移动
move() {
this.state?.execute();
}
}
客户端调用
// 客户端调用
let character = new Character();
character.setState(new StandState()); // 进入站立状态
character.move(); // 跑动状态移动
character.setState(new JumpState()); // 退出站立状态 进入跳跃状态
character.move(); // 跳跃状态移动
10. 命令模式
命令模式(Command Pattern),是一种行为设计模式,它将一个请求封装为一个对象,以便支持事件、交易、撤消等操作。
10.1 基本结构
命令模式中的主要角色包括:
- 抽象命令(Command):声明执行操作的接口。
- 具体命令(Concrete Command):定义一个接收者和行为之间的具体绑定。实现抽象命令接口。
- 客户端(Client):创建具体命令对象并设置其接收者。
- 调用者/请求者(Invoker):向命令对象进行请求,并不知道具体的命令行为。
- 接收者(Receiver):知道如何实施与之相关的命令操作,任何类都可能作为命令的接收者。
命令模式的结构图如下:
抽象命令(Command)
│
├─ 具体命令A(ConcreteCommandA)
│ │
│ └─ 接收者A(ReceiverA)
└─ 具体命令B(ConcreteCommandB)
│
└─ 接收者B(ReceiverB)
调用者(Invoker)
│
└─ 命令(Command)
10.2 优缺点
命令模式的主要优点:
- 解耦调用者和接收者。调用者只需要知道具有命令接口的命令对象,而不必知道具体的命令实现与接收者,两者可以独立变化。
- 容易设计命令队列。通过命令对象,可以将多个命令装入队列,并控制执行顺序,实现宏命令等功能。
- 可以实现命令的延迟执行。通过调用者来决定何时执行命令。
- 容易实现可撤销操作。只需要在命令队列中存储命令对象,即可根据需要撤销上一步或上 N 步操作。
- 容易实现事件模型。命令对象可以作为事件触发的参数被传递,接收者根据事件执行相应命令。
命令模式的主要缺点:
- 可能会导致某些系统有过多的具体命令类。如果都设计成具体命令类,会增加系统的复杂度。
- 一个系统通常只需要实现命令接口的一部分命令,但需要全部实现,这会增加系统的开发难度与测试难度。
- 某些系统的命令比较简单,使用命令模式可能造成复杂度上升,带来的好处并不明显。
- 由于需要引入命令对象,可能会影响系统的性能与资源占用。
10.3 适用场景
命令模式适用的主要场景有:
- 需要将请求调用者和接收者解耦,使得调用者不需要知道接收者的详细信息。
- 需要支持命令的撤销、重做、事务等功能。
- 需要参数化某些请求,将参数化的请求对象作为参数传递。
- 需要支持宏命令,将多个命令组合成一个命令对象。
在游戏开发中,命令模式主要应用在玩家 输入处理
, NPC 行为控制
, 操作撤销/重做
等方面。
例如,我们可以设计一个 RPG 游戏
的 战斗系统
,通过命令模式实现不同的技能和行动。
首先定义技能和行动的基类以及角色类:
// 动作基类
abstract class Action {
actor: Actor;
target?: Actor;
constructor(actor: Actor, target?: Actor) {
this.actor = actor;
this.target = target;
}
public abstract execute(): void;
}
// 技能基类
abstract class Skill extends Action {
name: string;
constructor(name: string, actor: Actor, target?: Actor) {
super(actor, target);
this.name = name;
}
}
// 角色类
class Actor {
name: string;
health: number;
constructor(name: string, health = 100) {
this.name = name;
this.health = health;
}
}
然后定义一些具体的技能和行动:
// 攻击技能类
class AttackSkill extends Skill {
execute() {
console.log(`${this.actor.name} 使用 ${this.name} 攻击 ${this.target!.name}`);
this.target!.health -= 10; // 损失生命值
}
}
// 治疗技能类
class HealSkill extends Skill {
execute() {
console.log(`${this.actor.name} 使用 ${this.name} 治疗了 ${this.target!.name}`);
this.target!.health += 10; // 恢复生命值
}
}
// 逃跑类
class RunAway extends Action {
execute() {
console.log(`${this.actor.name} 逃跑了!`);
}
}
actor
是施法者,target
是受术者,这些技能和行动通过 execute
方法来执行效果。
然后我们可以定义 命令队列
,控制不同角色的行动:
// 命令队列
class CommandQueue {
private commands: Action[] = [];
addCommand(command: Action) {
this.commands.push(command);
}
execute() {
const command = this.commands.shift();
if (command) {
command.execute();
}
}
}
客户端调用时。
// 定义两个战斗的角色
const actor = new Actor('小明');
const target = new Actor('小红');
// 定义命令列表
let queue = new CommandQueue();
// 向列表中添加命令
queue.addCommand(new AttackSkill('火球术', actor, target));
queue.addCommand(new HealSkill('治疗术', actor, target));
queue.addCommand(new RunAway(target));
// 执行列表中的命令
console.log(target.health); // 100(小红血量)
queue.execute(); // 小明 使用 火球术 攻击 小红
console.log(target.health); // 90(小红血量)
queue.execute(); // 小明 使用 治疗术 治疗了 小王
console.log(target.health); // 100(小红血量)
queue.execute(); // 小红 逃跑了!
可以看到,我们将所有技能封装为命令对象,通过命令队列来控制执行,并且可以任意组合不同的技能和行动,这使得战斗系统拥有更大的灵活性。
10.4 补充讲解
下面补充讲解一些示例,以便更好的理解命令模式的特点。
10.4.1 使用命令模式和不使用的区别
如果没有命令模式,我们的代码可能如下:
class Actor {
name: string;
constructor(name: string) {
this.name = name;
}
attack(target: Actor) {
console.log(`${this.name} 使用 ${this.name} 攻击 ${target.name}`);
}
heal(target: Actor) {
console.log(`${this.name} 使用 ${this.name} 治疗了 ${target.name}`);
}
runAway() {
console.log(`${this.name} 逃跑了!`);
}
}
let actor = new Actor("小明");
let target = new Actor('小红');
actor.attack(target); // 小明 使用 小明 攻击 小红
actor.heal(target); // 小明 使用 小明 治疗了 小红
actor.runAway(); // 小明 逃跑了!
可以看到,行为的调用者 ( actor
) 与具体实现 ( attack
、heal
、runAway
方法 ) 是紧密耦合的。如果我们要改变调用顺序、实现撤销操作,或基于事件驱动执行行为,这种紧耦合的代码会很难维护和扩展。
而借助命令模式,我们可以这样调用:
// 实例化命令对象
let attackCmd = new AttackCommand(actor, target);
let healCmd = new HealCommand(actor, target);
let runAwayCmd = new RunAwayCommand(actor);
let queue = new CommandQueue();
// 将命令对象放入命令队列
queue.add(attackCmd);
queue.add(healCmd);
queue.add(runAwayCmd);
// 执行命令
queue.execute(); // 执行攻击命令
queue.execute(); // 执行治疗命令
queue.execute(); // 执行逃跑命令
调用者 ( CommandQueue
) 与接收者 ( actor
) 解耦,行为通过 命令对象
来抽象,调用者只需要知道和持有这些命令对象,而无需关心具体细节。
命令模式的主要优势不在于任何单一操作,而在于它可以让调用者与接收者解耦,灵活控制和组合不同的行为,从而实现更加强大的功能。
10.4.2 命令模式如何实现撤销操作
例如,我们可以这样实现一个简单的可撤销系统。
首先,定义命令接口类,和移动命令类。
interface ICommand {
execute(): void;
undo(): void;
}
class MoveCommand implements ICommand {
constructor(private gameObject: GameObject) { }
execute() {
this.gameObject.move();
}
undo() {
this.gameObject.undoMove();
}
}
定义游戏对象类。
class GameObject {
move() {
console.log("移动:前进 10 米");
}
undoMove() {
console.log("撤销:后退 10 米");
}
}
定义命令队列类。
// 命令队列
class CommandQueue {
private commands: ICommand[] = [];
private history: ICommand[] = [];
addCommand(command: ICommand) {
this.commands.push(command);
}
execute() {
const command = this.commands.shift();
if (command) {
command.execute();
this.history.push(command);
}
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
客户端调用时。
let actor = new GameObject();
let queue = new CommandQueue();
queue.addCommand(new MoveCommand(actor));
// 移动
queue.execute(); // 移动:前进 10 米
// 撤销上一步移动
queue.undo(); // 撤销:后退 10 米
可以看到,我们将移动命令定义为一个实现了 ICommand
的 MoveCommand
类。CommandQueue
负责执行这些命令并将其保存到历史记录中。通过调用 undo 方法,我们可以撤销上一步操作。
所以,命令模式通过将 请求/操作
封装为 对象
,实现命令的 队列化
、记录
与 撤销
,这为游戏开发带来了极大便利,是实现游戏的 可撤销系统
、事件系统
以及 AI 系统
的重要手段。
以上便是游戏开发中常用的 10 种设计模式。
此处评论已关闭