参考材料
1. 【UE4 设计模式】策略模式 Strategy Pattern
2. 访问者模式
3. 游戏开发:访问者模式实战解析
1. 概述
1.1 描述
$\cdot$ 在访问者模式(Visitor Pattern)中, 我们使用了一个访问者类, 它改变了元素类的执行算法. 通过这种方式, 元素的执行算法可以随着访问者改变而改变. 这种类型的设计模式属于行为型模式. 根据模式, 元素对象已接受访问者对象, 这样访问者对象就可以处理元素对象上的操作.
1.2 套路
$\cdot$ 实现方式:
$\\$ 1) 定义访问者接口: 声明一系列访问方法, 一个访问方法对应数据结构中的一个元素类.
$\\$ 2) 创建具体访问者: 实现访问者接口, 为每个访问方法提供具体实现.
$\\$ 3) 定义元素接口: 声明一个接受访问者的方法.
$\\$ 4) 创建具体元素: 实现元素接口, 每个具体元素类对应数据结构中的一个具体对象.
$\cdot$ 关键代码:
$\\$ 1) 访问者接口: 包含访问不同元素的方法.
$\\$ 2) 具体访问者: 实现了访问者接口, 包含对每个元素类的访问逻辑.
$\\$ 3) 元素接口: 包含一个接受访问者的方法.
$\\$ 4) 具体元素: 实现了元素接口, 提供给访问者访问的入口.
$\cdot$ 结构:
$\\$ 包含的几个主要角色
$\\$ 1) 访问者(Visitor): 定义了访问元素的接口.
$\\$ 2) 具体访问者(Concrete Visitor): 实现访问者接口, 提供对每个具体元素类的访问和相应操作.
$\\$ 3) 元素(Element): 定义了一个接受访问者的方法.
$\\$ 4) 具体元素(Concrete Element): 实现元素接口, 提供一个Accept方法, 允许访问者访问并操作.
$\\$ 5) 对象结构(Object Structure)(可选): 定义了如何组装具体元素, 如一个组合类.
$\\$ 6) 客户端(Client)(可选): 使用访问者模式对对象结构进行操作.
1.3 使用场景
$\cdot$ 当需要对一个对象结构中的对象执行多种不同的且不相关的操作时, 尤其是这些操作需要避免”污染” 对象类本身.
1.4 优缺点
$\cdot$ 优点:
$\\$ 1) 单一职责原则: 访问者模式符合单一职责原则, 每个类只负责一项职责.
$\\$ 2) 扩展性: 容易为数据结构添加新的操作.
$\\$ 3) 灵活性: 访问者可以独立于数据结构变化.
$\cdot$ 缺点:
$\\$ 1) 违反迪米特原则: 元素需要向访问者公开其内部信息.
$\\$ 2) 元素类难以变更: 元素类需要维持与访问者的兼容.
$\\$ 3) 依赖具体类: 访问者模式依赖于具体类而不是接口, 违反了依赖倒置原则.
2. 访问者模式原理—— 生动形象解释
2.1 生活中的例子
$\cdot$ 比喻: 医生巡诊不同病人
$\\$ 想象医院里有很多不同类型的病人: 小孩, 成年人, 老人.
$\\$ 每个病人有自己的身体状况, 但医院会有不同的医生来巡诊, 比如内科医生, 外科医生, 心理医生.
$\\$ 1) 病人类(Element): 小孩, 成年人, 老人.
$\\$ 2) 医生类(Visitor): 内科医生, 外科医生, 心理医生.
$\\$ 每个医生都能”访问” 每个病人, 并根据病人类型做不同的检查和处理.
$\cdot$ 关键点: 你不用在病人类里写所有医生的处理逻辑, 而是让医生自己带着”处理方法” 来访问病人.
$\cdot$ 本质: 访问者模式就是把对一组对象的操作从对象本身分离出来, 新增操作时不用修改对象类, 只需增加访问者类.
2.2 游戏中的实际应用
$\cdot$ Buff/状态系统: 不同Buff(加血, 减速, 眩晕) 访问不同类型的角色(玩家, 怪物, NPC), 实现不同效果.
$\\$ $\cdot$ 数据导出: 导出游戏中各种对象(角色, 道具, 关卡) 到不同格式(JSON, XML, CSV).
$\\$ $\cdot$ 技能系统: 技能访问不同目标, 产生不同效果.
3. UE5实践(以Buff系统为例)
3.1 元素接口和具体元素
#pragma once
#include "CoreMinimal.h"
class IBuffVisitor;
// 元素接口
class ICharacter
{
public:
virtual void Accept(IBuffVisitor* Visitor) = 0;
};
// 具体元素: 玩家
class FPlayer : public ICharacter
{
public:
virtual void Accept(IBuffVisitor* Visitor) override;
public:
int32 HP = 100;
};
// 具体元素: 玩家
class FMonster : public ICharacter
{
public:
virtual void Accept(IBuffVisitor* Visitor) override;
public:
int32 HP = 50;
};
#include "Element.h"
void FPlayer::Accept(IBuffVisitor* Visitor)
{
Visitor.Visit(this);
}
void FMonster::Accept(IBuffVisitor* Visitor)
{
Visitor.Visit(this);
}
3.2 访问者接口和具体访问者
#pragma once
class FPlayer;
class FMonster;
// 访问者接口
class IBuffVisitor
{
virtual void Visit(FPlayer* Player) = 0;
virtual void Visit(FMonster* Monster) = 0;
};
// 具体访问者: 加血Buff
class FHealBuff : public IBuffVisitor
{
public:
virtual void Visit(FPlayer* Player) override;
virtual void Visit(FMonster* Monster) override;
};
// 具体访问者: 中毒Buff
class FPoisonBuff : public IBuffVisitor
{
public:
virtual void Visit(FPlayer* Player) override;
virtual void Visit(FMonster* Monster) override;
};
#include "Visitor.h"
#include "CoreMinimal.h"
void FHealBuff::Visit(FPlayer* Player)
{
Player->HP += 20;
UE_LOG(LogTemp, Log, TEXT("玩家加血,当前HP:%d"), Player->HP);
}
void FHealBuff::Visit(FMonster* Monster)
{
Monster->HP += 10;
UE_LOG(LogTemp, Log, TEXT("怪物加血,当前HP:%d"), Monster->HP);
}
void FPoisonBuff::Visit(FPlayer* Player)
{
Player->HP -= 15;
UE_LOG(LogTemp, Log, TEXT("玩家中毒,当前HP:%d"), Player->HP);
}
void FPoisonBuff::Visit(FMonster* Monster)
{
Monster->HP -= 6;
UE_LOG(LogTemp, Log, TEXT("怪物中毒,当前HP:%d"), Monster->HP);
}
3.3 使用示例
#include "Element.h"
#include "Visitor.h"
int main()
{
ICharacter* Player = new FPlayer();
ICharacter* Monster = new FMonster();
IBuffVisitor* Heal = new FHealBuff();
IBuffVisitor* Poison = new FPoisonBuff();
// 给玩家和怪物加血
Player->Accept(Heal); // 玩家加血, 当前HP: 120
Monster->Accept(Heal); // 怪物加血, 当前HP: 60
// 给玩家和怪物中毒
Player->Accept(Poison); // 玩家中毒,当前HP:105
Monster->Accept(Poison); // 怪物中毒,当前HP:55
delete Player;
Player = nullptr;
delete Monster;
Monster = nullptr;
delete Heal;
Heal = nullptr;
delete Poison;
Poison = nullptr;
return 0;
}
3.4 口诀总结
访问者像医生,
元素是病人;
新增操作加医生,
病人代码不用变!
比如你有一组场景对象(NPC, 怪物, 宝箱), 你想实现”导出数据到JSON” 和”导出到XML” 两种操作.
$\\$ 用访问者模式, 每种导出方式写一个访问者, 后续要加”导出到CSV” 只需加一个访问者类, 原有对象不用动.
4. 深入理解
下面我们将深入理解:
$\\$ 1) 双分派原理(访问者模式的核心).
$\\$ 2) 访问者链(Visitor Chain / Chain of Visitors).
$\\$ 3) 复杂案例(如技能系统, 导出系统等).
4.1 双分派原理(Double Dispatch)
1) 单分派 vs 双分派
$\\$ 单分派: C#, Java等语言默认是”单分派”, 即方法调用只根据接收者对象的实际类型决定调用哪个方法.
$\\$ 双分派: 访问者模式实现了”根据访问者类型和被访问者类型共同决定调用哪个方法”.
2) 形象解释
$\\$ 比喻: 你(访问者) 去医院(元素), 你是内科医生, 病人是小孩.
$\\$ $\cdot$ 医院先看你是哪个医生(访问者类型),
$\\$ $\cdot$ 再看病人是哪个类型(元素类型),
$\\$ $\cdot$ 最终决定调用”内科医生给小孩看病” 的方法.
3) 代码演示
#pragma once
#include "CoreMinimal.h"
// 元素接口
class IElement
{
public:
virtual void Accept(class IVisitor* Visitor) = 0;
};
// 访问者接口
class IVisitor
{
public:
virtual void Visit(class FElementA* A) = 0;
virtual void Visit(class FElementB* B) = 0;
}
// 具体元素
class FElementA : public IElement
{
public:
virtual void Accept(class IVisitor* Visitor) override
{
Visitor->Visit(this); // 这里的this类型是FElementA
}
};
class FElementB : public IElement
{
public:
virtual void Accept(class IVisitor* Visitor) override
{
Visitor->Visit(this); // 这里的this类型是FElementB
}
};
// 具体访问者
class FVisitor1 : pulic IVisitor
{
public:
virtual void Visit(FElementA* A) override
{
UE_LOG(LogTemp, Log, TEXT("Visitor1访问ElementA"));
}
virtual void Visit(FElementB* B) override
{
UE_LOG(LogTemp, Log, TEXT("Visitor1访问ElementB"));
}
};
双分派过程:
$\\$ $\cdot$ Element.Accept(Visitor): 先根据Element的实际类型(ElementA / ElementB) 调用对应的Accept.
$\\$ $\cdot$ Visitor.Visit(this): 再根据Visitor的实际类型和this的类型, 调用正确的Visit重载.
4.2 访问者链(Visitor Chain)
4.2.1 概念
访问者链是指多个访问者依次作用于同一组元素, 类似责任链模式, 但每个访问者都能处理元素.
4.2.2 形象解释
比如你有一组角色, 先让”加血Buff” 访问一遍, 再让”中毒Buff” 访问一遍, 角色会依次受到多种效果.
4.2.3 代码实现
#include "Element.h"
#include "Visitor.h"
int main()
{
TArray Visitors = {new FHealBuff(), new FPoisonBuff()};
TArray Elements = { new FPlayer(), new FMonster() };
for (auto& Visitor : Visitors)
{
for (auto& Element : Elements)
{
Element->Accept(Visitor);
}
}
for (auto& Visitor : Visitors)
{
delete Visitor;
Visitor = nullptr;
}
Visitors.Empty();
for (auto& Element : Elements)
{
delete Element;
Element = nullptr;
}
Elements.Empty();
return 0;
}
这样每个角色会被每个Buff访问一次.
4.3 UE复杂案例
4.3.1 技能系统中的访问者模式
场景: 你有多种技能(火球, 治疗, 冰冻), 每种技能对不同目标(玩家, 怪物, 建筑) 有不同效果.
代码结构
#pragma once
#include "CoreMinimal.h"
class ISkillVisitor;
// 目标接口
class ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) = 0;
};
// 具体目标
class FPlayer : public ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) override
{
Visitor->Visit(this);
}
public:
int32 HP = 100;
};
class FMonster : public ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) override
{
Visitor->Visit(this);
}
public:
int32 HP = 50;
};
class FBuilding : public ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) override
{
Visitor->Visit(this);
}
public:
int32 Durability = 200;
};
// 技能访问者接口
class ISkillVisitor
{
public:
virtual void Visit(FPlayer* Player) = 0;
virtual void Visit(FMonster* Monster) = 0;
virtual void Visit(FBuilding* Building) = 0;
};
// 具体技能
class FFireballSkill : public ISkillVisitor
{
public:
virtual void Visit(FPlayer* Player) override
{
Player->HP -= 10;
UE_LOG(LogTemp, Log, TEXT("玩家被火球击中,HP: %d"), Player->HP);
}
virtual void Visit(FMonster* Monster) override
{
Monster->HP -= 30;
UE_LOG(LogTemp, Log, TEXT("怪物被火球击中,HP: %d"), Monster->HP);
}
virtual void Visit(FBuilding* Building) override
{
Building->Durability -= 50;
UE_LOG(LogTemp, Log, TEXT("建筑被火球击中,耐久: %d"), Building->Durability);
}
};
class FHealSkill : public ISkillVisitor
{
public:
virtual void Visit(FPlayer* Player) override
{
Player->HP += 20;
UE_LOG(LogTemp, Log, TEXT("玩家被治疗,HP: %d"), Player->HP);
}
virtual void Visit(FMonster* Monster) override
{
/*怪物不能被治疗*/
}
virtual void Visit(FBuilding* Building) override
{
/*建筑不能被治疗*/
}
};
使用方式
#include "TargetAndSkillVisitor.h"
int main()
{
TArray Targets = { new FPlayer(), new FMonster(), new FBuilding() };
ISkillVisitor* Fireball = new FFireballSkill();
ISkillVisitor* Heal = new FHealSkill();
for (auto& Target : Targets)
{
Target->Accept(Fireball); // 所有目标都被火球访问
Target->Accept(Heal); // 所有目标都被治疗访问
}
for (auto& Target : Targets)
{
delete Target;
Target = nullptr;
}
Targets.Empty();
delete Fireball;
Fireball = nullptr;
delete Heal;
Heal = nullptr;
return 0;
}
4.3.2 游戏对象导出系统
场景: 你有一堆游戏对象(角色, 道具, 关卡), 需要导出为不同格式(JSON, XML, CSV).
代码结构
#pragma once
#include "CoreMinimal.h"
class IExportVisitor;
class IGameElement
{
public:
virtual void Accept(IExportVisitor* Visitor) = 0;
};
class FCharacter : public ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) override
{
Visitor->Visit(this);
}
public:
FString Name = TEXT("Hero");
};
class FItem : public ITarget
{
public:
virtual void Accept(ISkillVisitor* Visitor) override
{
Visitor->Visit(this);
}
public:
FString ItemName = TEXT("Sword");
};
class IExportVisitor
{
public:
virtual void Visit(FCharacter* Character) = 0;
virtual void Visit(FItem* Item) = 0;
};
class FJsonExportVisitor : public IExportVisitor
{
public:
virtual void Visit(FCharacter* Character) override
{
UE_LOG(LogTemp, Log, TEXT("导出角色为JSON: %s"), *(Character->Name));
}
virtual void Visit(FItem* Item) override
{
UE_LOG(LogTemp, Log, TEXT("导出道具为JSON: %s"), *(Monster->ItemName));
}
};
class FXMLExportVisitor : public IExportVisitor
{
public:
virtual void Visit(FCharacter* Character) override
{
UE_LOG(LogTemp, Log, TEXT("导出角色为XML: %s"), *(Character->Name));
}
virtual void Visit(FItem* Item) override
{
UE_LOG(LogTemp, Log, TEXT("导出道具为XML: %s"), *(Monster->ItemName));
}
};
使用方式
#include "GameElementAndExportVisitor.h"
int main()
{
TArray Elements = { new FCharacter(), new FItem() };
IExportVisitor* JsonVisitor = new FJsonExportVisitor();
IExportVisitor* XMLVisitor = new FXMLExportVisitor();
for (auto& Element : Elements)
{
Element->Accept(JsonVisitor);
Element->Accept(XMLVisitor);
}
for (auto& Element : Elements)
{
delete Element;
Element = nullptr;
}
Elements.Empty();
delete JsonVisitor;
JsonVisitor = nullptr;
delete XMLVisitor;
XMLVisitor = nullptr;
return 0;
}
4.3.3 总结口诀
双分派, 类型双重判,
访问者链, 批量处理忙.
UE技能, 导出皆可用,
新增操作不动原对象!