[UE5 设计模式] 备忘录模式Memento Pattern


参考材料
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.
$\\$ 这就是备忘录的使用.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注