参考材料
1. 【UE4 设计模式】策略模式 Strategy Pattern
2. 备忘录模式
3. 看完,你也能用备忘录模式手写一个游戏的存档功能!
4. 【设计模式】通过游戏存档了解备忘录模式
1. 概述
1.1 描述
$\cdot$ 备忘录模式(Memento Pattern) 保存一个对象的某个状态, 以便在适当的时候恢复对象, 备忘录模式属于行为型模式.
$\\$ 备忘录模式允许在不破坏封装性的前提下, 捕获和恢复对象的内部状态.
1.2 套路
$\cdot$ 实现方式:
$\\$ 1) 创建备忘录类: 用于存储和封装对象的状态.
$\\$ 2) 创建发起人角色: 负责创建备忘录, 并根据需要恢复状态.
$\\$ 3) 创建备忘录管理类(可选): 负责管理所有备忘录对象.
$\cdot$ 关键代码:
$\\$ 1) 备忘录: 存储发起人的状态信息.
$\\$ 2) 发起人: 创建备忘录, 并根据备忘录恢复状态.
$\cdot$ 结构:
$\\$ 备忘录模式包含以下几个主要角色:
$\\$ 1) 备忘录(Memento): 负责存储原发器对象的内部状态. 备忘录可以保持原发器的状态的一部分或全部信息.
$\\$ 2) 原发器(Originator): 创建一个备忘录对象, 并且可以使用备忘录对象恢复自身的内部状态. 原发器通常会在需要保存状态的时候创建备忘录对象, 并在需要恢复状态的时候使用备忘录对象.
$\\$ 3) 负责人(Caretaker): 负责保存备忘录对象, 但是不对备忘录对象进行操作或检查. 负责人只能将备忘录传递给其他对象.
1.3 使用场景
$\cdot$ 当需要提供一种撤销机制, 允许用户回退到之前的状态时.
1.4 优缺点
$\cdot$ 优点:
$\\$ 1) 提供状态恢复机制: 允许用户方便地回到历史状态.
$\\$ 2) 封装状态信息: 用户不需要关心状态的保存细节.
$\cdot$ 缺点:
$\\$ 1) 资源消耗: 如果对象的状态复杂, 保存状态可能会占用较多资源.
2. UE5实践
游戏中的存档功能就是备忘录模式. 我们在玩游戏时, 到一定的进度时, 我们觉得当前的状态就是最有利的, 因此我们会调用存档的功能, 把当前的状态存储到某个地方或者某个文件中. 当我们后续的某个操作出现问题时, 再读取存档就能回到我们刚才保存的进度.
2.1 示例解析
就刚才的例子. 我们把整个游戏看做一个对象: FGame. 在这个游戏中, 我们的进度, 就是描述的当前游戏的状态, 比如我们赚取的金布数量. 因此, 在Game中始终有一个金币数量的状态: Money.
$\\$ 现在, 我们想要把这个金币的数量进行存档. 在游戏中, 是不是经常有一个存档的功能? 这就对应着: FGame中有一个存档的功能方法. 同样, 我们进行读档的时候,对应着的就是FGame 中读档的功能方法.
$\\$ 存档或读档的数据也是游戏的一种状态. 我们不会把这个存档的状态和FGame中现有的状态混在一起. 因为在读档的时候, 存档中的状态会覆盖FGame中原有的状态. 因此, 我们需要一个独立的对象来表示这个存档状态(为什么用对象呢? 因为我们的状态可能不止金币, 可能还有装备, 或者其他的): FMemento.
$\\$ 目前, 我们已经提到了备忘录中的两个角色了: 发起者角色(FGame) 和备忘录角色(FMemento).
$\\$ 注意, 这里还有一个角色: 备忘录管理者角色. 它用来管理备忘录, 仅提供备忘录的存储和获取. 为什么呢? 因为你的备忘录可能不止一个, 就像你的存档不止一个一样.
$\\$ OK, 让我们简单的实现下这个例子.
2.2 示例代码
首先是发起者角色: FGame类, 包含存档和读档的方法: CreateMemento和RestoreMemento.
#pragma once
#include "Memento.h"
class FGame
{
public:
FGame()
{
}
FGame(int32 InMoney) : Money(InMoney)
{
}
FORCEINLINE void SetMoney(int32 InMoney)
{
Money = InMoney;
}
FORCEINLINE int32 GetMoney() const
{
return Money;
}
FMemento* CreateMemento()
{
FMemento* Memento = new FMemento(Money);
return Memento;
}
void RestoreMemento(FMemento* Memento)
{
if (Memento != nullptr)
{
Money = Memento->GetMoney();
return;
}
UE_LOG(LogTemp, Warning, TEXT("读档失败!"));
}
FString ToString() const
{
return FString::Printf(TEXT("Money: %d"), Money);
}
private:
int32 Money;
};
在存取档中, 用到了备忘录对象: FMemento.
#pragma once
#include "CoreMinimal.h"
class FMemento
{
FMemento(int32 InMoney) : Money(InMoney)
{
}
public:
FORCEINLINE int32 GetMoney() const
{
return Money;
}
private:
int32 Money;
};
还有一个类就是备忘录管理者类: FMementoManager.
#pragma once
#include "Memento.h"
class FMementoManager
{
public:
void Add(FMemento* Memento)
{
MementoList.Emplace(Memento);
}
FMemento* Get(int32 Index)
{
if (MementoList.IsValidIndex(Index))
{
return MementoList[Index];
}
return nullptr;
}
void Clear()
{
for (auto& Memento : MementoList)
{
delete Memento;
Memento = nullptr;
}
MementoList.Clear();
}
private:
TArray MementoList;
};
让我们来测试一把.
#include "Game.h"
#include "MementoManager.h"
int main()
{
FGame* Game = new FGame();
FMementoManager* MementoManager = new FMementoManager();
Game->SetMoney(100);
UE_LOG(LogTemp, Log, TEXT("当前的金币是: %d"), Game->GetMoney());
MementoManager->Add(Game->CreateMemento());
// 不小心金币损失到只有10了!
Game->SetMoney(10);
UE_LOG(LogTemp, Log, TEXT("当前的金币是: %d"), Game->GetMoney());
// 从MementoManager取出第一个Memento对象, 并给Game赋值回去.(简称: 读档)
Game->RestoreMemento(MementoManager->Get(0));
UE_LOG(LogTemp, Log, TEXT("执行读档后, 当前的金币是: %d"), Game->GetMoney());
delete Game;
Game = nullptr;
MementoManager->Clear();
delete MementoManager;
MementoManager = nullptr;
return 0;
}
预期结果: 当前的金币是: 100, 当前的金币是: 10. 执行读档后, 当前的金币是: 100.
$\\$ 我们的金币数量, 通过读档后, 又回到了起初的100.
$\\$ 这就是备忘录的使用.