不知道大家小时候有没有玩过经典单机游戏,特别是一些小而美的 RPG 游戏,如金庸群侠传、口袋妖怪等等,由于通关游戏需要很长的时间,所以这些 RPG 游戏都会提供存档的功能,让玩家能够休息一会,明日再战。而下次进入游戏读取存档即可接着上次的画面继续游玩。

游戏中的这种存档、读档功能就可以使用备忘录模式(Memento Pattern)来实现。

第19篇·基于备忘录模式实现游戏存档与读取 - 图1

1. 定义

备忘录模式是一种行为型设计模式,它的定义是:“在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

简而言之就是允许客户端将某个场景恢复到原来保存过的时间点,这个功能跟前段时间的热播电视剧《开端》的不断循环非常像,包括很多年前周星驰的电影《大话西游》中利用月光宝盒不断穿越到紫霞仙子自杀前的时间段,也是同样的道理。

2. 类图

在备忘录模式中,有这样几种角色:

  • 发起人(Originator)角色:记录当前对象状态信息,提供创建和恢复备忘录数据的功能
  • 备忘录(Memento)角色:负责存储发起人的状态信息,通过备忘录管理者对外暴露
  • 备忘录管理者(Caretaker)角色:提供管理备忘录功能(如创建、查询),但不能对备忘录数据进行修改

备忘录模式的类图如下:

第19篇·基于备忘录模式实现游戏存档与读取 - 图2

3. 示例

本来想用金庸群侠传来作为备忘录模式的场景介绍的,由于金庸群侠传规定输三次就相当于游戏失败,所以每次在挑战高手前我都会存个档,以防直接“退出江湖”。但是 Mac Chorme 浏览器已不提供 flash 能力,而且 4399 小游戏竟然需要实名注册才能玩!

所以今天就用口袋妖怪用精灵球抓精灵的场景来演示备忘录模式,先来说说场景:在口袋妖怪游戏中,最让玩家有成就感的事情莫过于抓捕野生的稀有精灵,但是在某些特殊的场景中,只有一次机会投掷出精灵球,没有成功抓捕到那么精灵就会逃走或者把你的精灵打败,例如:凯西、三神兽等。

第19篇·基于备忘录模式实现游戏存档与读取 - 图3

假设现在对面的就是凯西(原谅我没有其他素材了,随便抓了只波波,凯西还要打到第二座道馆才能遇到),玩家只有一次机会抓它,此时玩家肯定要先存档,如果这次没成功那么读档重来。

第19篇·基于备忘录模式实现游戏存档与读取 - 图4

直到成功抓到凯西,这波才能算功成身退。

下面就用备忘录模式来模拟上面的场景,首先创建备忘录角色,这里未被收服的凯西,以及玩家自己的小火龙都是备忘录角色,代码如下:

  1. public class Memento {
  2. // 精灵名称
  3. private String name;
  4. // 精灵生命值
  5. private Integer hp;
  6. // 精灵等级
  7. private String level;
  8. // 精灵归属人
  9. private String belonger;
  10. public Memento(String name, Integer hp, String level, String belonger) {
  11. this.name = name;
  12. this.hp = hp;
  13. this.level = level;
  14. this.belonger = belonger;
  15. }
  16. // 备忘录类内的属性没有 setter 方法,其属性只能在创建备忘录时填入,不应该被外界修改
  17. public String getName() {
  18. return name;
  19. }
  20. public Integer getHp() {
  21. return hp;
  22. }
  23. public String getLevel() {
  24. return level;
  25. }
  26. public String getBelonger() {
  27. return belonger;
  28. }
  29. @Override
  30. public String toString() {
  31. return "Memento{" +
  32. "name='" + name + '\'' +
  33. ", hp=" + hp +
  34. ", level='" + level + '\'' +
  35. ", belonger='" + belonger + '\'' +
  36. '}';
  37. }
  38. }

然后编写备忘录管理者角色,这里与类图有点不一样的是在当前的场景中,存档中会保存多个精灵的状态,所以一个存档会使用一个 List 来存储,同时它会使用一个唯一 key 来保证不重复,其代码如下:

  1. public class Caretaker {
  2. // 由于存档需要保存多个精灵的状态,所以每个存档用一个 list 来存储
  3. private Map<String, List<Memento>> archiveMap = new HashMap<>();
  4. public List<Memento> getArchive(String key) {
  5. return archiveMap.get(key);
  6. }
  7. public void setArchiveMap(String key, List<Memento> archive) {
  8. archiveMap.put(key, archive);
  9. }
  10. }

然后编写发起人角色:

  1. public class Player {
  2. private List<Memento> mementos;
  3. public Player(List<Memento> mementos) {
  4. this.mementos = mementos;
  5. }
  6. public List<Memento> createMemento() {
  7. List<Memento> target = new ArrayList<>();
  8. for (Memento memento : mementos) {
  9. target.add(memento);
  10. }
  11. return target;
  12. }
  13. public void restoreMemento(List<Memento> mementos) {
  14. this.mementos = mementos;
  15. }
  16. @Override
  17. public String toString() {
  18. return "Player{" +
  19. "mementos=" + mementos +
  20. '}';
  21. }
  22. }

然后编写测试代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Memento bobo = new Memento("凯西", 10, "Lv3", null);
  4. Memento fireDragon = new Memento("小火龙", 20, "Lv6", "player1");
  5. List<Memento> mementos = new ArrayList<>();
  6. mementos.add(bobo);
  7. mementos.add(fireDragon);
  8. Player player = new Player(mementos);
  9. System.out.println("第一次遇到凯西时的状态: " + player);
  10. System.out.println("存档");
  11. List<Memento> mementoCreated = player.createMemento();
  12. Caretaker caretaker = new Caretaker();
  13. caretaker.setArchiveMap("1", mementoCreated);
  14. System.out.println("抓捕失败,凯西逃走,开始读档");
  15. List<Memento> archive = caretaker.getArchive("1");
  16. player.restoreMemento(archive);
  17. System.out.println("读档后的状态: " + player);
  18. }
  19. }

输出结果为:

  1. 第一次遇到凯西时的状态: Player{mementos=[Memento{name='凯西', hp=10, level='Lv3', belonger='null'}, Memento{name='小火龙', hp=20, level='Lv6', belonger='player1'}]}
  2. 存档
  3. 抓捕失败,凯西逃走,开始读档
  4. 读档后的状态: Player{mementos=[Memento{name='凯西', hp=10, level='Lv3', belonger='null'}, Memento{name='小火龙', hp=20, level='Lv6', belonger='player1'}]}

于是,在不断的尝试过后,终于如愿以偿抓到了凯西。

第19篇·基于备忘录模式实现游戏存档与读取 - 图5

4. 使用场景

备忘录模式的使用场景是:

  • 需要保存及恢复数据或者提供回滚功能的场景

5. 小结

本文讲述了备忘录模式,它的定义是——在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

比如一些游戏存档的场景、MySQL MVCC、文本编辑器使用 Ctrl+Z 实现的撤销功能等,都可以使用备忘录模式来实现。

备忘录模式的优点是:

  • 提供了数据回滚能力,同时也不会破坏封装性

备忘录模式的缺点是:

  • 备忘录模式会存储对象大量的快照版本,如果频繁使用会增加内存消耗

6. 参考资料

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。