我所理解的设计模式系列·第19篇·基于备忘录模式实现游戏存档与读取

2022/02/12 设计模式 共 3617 字,约 11 分钟

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

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

design-patttern-19-备忘录模式-封面

1. 定义

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

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

2. 类图

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

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

备忘录模式的类图如下:

design-patttern-19-备忘录模式-1-类图

3. 示例

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

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

design-patttern-19-备忘录模式-2

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

design-patttern-19-备忘录模式-3

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

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

public class Memento {
    // 精灵名称
    private String name;
    // 精灵生命值
    private Integer hp;
    // 精灵等级
    private String level;
    // 精灵归属人
    private String belonger;

    public Memento(String name, Integer hp, String level, String belonger) {
        this.name = name;
        this.hp = hp;
        this.level = level;
        this.belonger = belonger;
    }

    // 备忘录类内的属性没有 setter 方法,其属性只能在创建备忘录时填入,不应该被外界修改
    public String getName() {
        return name;
    }

    public Integer getHp() {
        return hp;
    }

    public String getLevel() {
        return level;
    }

    public String getBelonger() {
        return belonger;
    }

    @Override
    public String toString() {
        return "Memento{" +
                "name='" + name + '\'' +
                ", hp=" + hp +
                ", level='" + level + '\'' +
                ", belonger='" + belonger + '\'' +
                '}';
    }
}

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

public class Caretaker {

    // 由于存档需要保存多个精灵的状态,所以每个存档用一个 list 来存储
    private Map<String, List<Memento>> archiveMap = new HashMap<>();

    public List<Memento> getArchive(String key) {
        return archiveMap.get(key);
    }

    public void setArchiveMap(String key, List<Memento> archive) {
        archiveMap.put(key, archive);
    }
}

然后编写发起人角色:

public class Player {

    private List<Memento> mementos;

    public Player(List<Memento> mementos) {
        this.mementos = mementos;
    }

    public List<Memento> createMemento() {
        List<Memento> target = new ArrayList<>();
        for (Memento memento : mementos) {
            target.add(memento);
        }
        return target;
    }

    public void restoreMemento(List<Memento> mementos) {
        this.mementos = mementos;
    }

    @Override
    public String toString() {
        return "Player{" +
                "mementos=" + mementos +
                '}';
    }
}

然后编写测试代码:

public class Test {
    public static void main(String[] args) {
        Memento bobo = new Memento("凯西", 10, "Lv3", null);
        Memento fireDragon = new Memento("小火龙", 20, "Lv6", "player1");
        List<Memento> mementos = new ArrayList<>();
        mementos.add(bobo);
        mementos.add(fireDragon);

        Player player = new Player(mementos);
        System.out.println("第一次遇到凯西时的状态: " + player);
        System.out.println("存档");
        List<Memento> mementoCreated = player.createMemento();

        Caretaker caretaker = new Caretaker();
        caretaker.setArchiveMap("1", mementoCreated);

        System.out.println("抓捕失败,凯西逃走,开始读档");
        List<Memento> archive = caretaker.getArchive("1");
        player.restoreMemento(archive);
        System.out.println("读档后的状态: " + player);
    }
}

输出结果为:

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

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

design-patttern-19-备忘录模式-4

4. 使用场景

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

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

5. 小结

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

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

备忘录模式的优点是:

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

备忘录模式的缺点是:

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

6. 参考资料

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

文档信息

Search

    Table of Contents