大家好,老胡又和大家见面了。首先承认今天的博客有点标题党了,人生是没有存档,也没有后悔药的。有存档和后悔药的,那是游戏,不知道这是不是游戏让人格外放松的原因之一。

今天恰逢端午放假,就让我们来试着做一个小游戏吧,顺带看看备忘录模式是如何在这种情况下面工作的。

游戏背景

这是一个简单的打怪游戏,有玩家,有怪兽,玩家作为主角光环,有如下三个特殊能力

  • 攻击怪兽有暴击几率
  • 有几率回避怪兽攻击
  • 可以自己治疗一定生命值

游戏实现

角色类
角色基类

首先是角色类,角色类提供玩家和怪兽最基本的抽象,比如血量、攻击力、攻击和治疗。(对于怪兽来说,治疗是没有提供实现的,坏人肯定不能再治疗了)

class Character
{
public int HealthPoint { get; set; }
public int AttackPoint { get; set; } public virtual void AttackChracter(Character opponent)
{
opponent.HealthPoint -= this.AttackPoint;
if (opponent.HealthPoint < 0)
{
opponent.HealthPoint = 0;
}
} public virtual void Cure()
{
//故意留空给子类实现
}
}
玩家类

玩家实现了治疗功能并且有暴击几率。

class Player : Character
{
private float playerCriticalPossible;
public Player(float critical)
{
playerCriticalPossible = critical;
} public override void AttackChracter(Character opponent)
{
base.AttackChracter(opponent);
Console.WriteLine("Player Attacked Monster"); Random r = new Random();
bool critical = r.Next(0, 100) < playerCriticalPossible * 100;
if (critical)
{
base.AttackChracter(opponent);
Console.WriteLine("Player Attacked Monster again");
}
} public override void Cure()
{
Random r = new Random();
HealthPoint += r.Next(5, 10);
Console.WriteLine("Player cured himself");
}
}
怪兽类

怪兽没有治疗能力但是有一定的几率丢失攻击目标。

class Monster : Character
{
private float monsterMissingPossible;
public Monster(float missing)
{
monsterMissingPossible = missing;
} public override void AttackChracter(Character opponent)
{
Random r = new Random();
bool missing = r.Next(0, 100) < monsterMissingPossible * 100;
if (missing)
{
Console.WriteLine("Monster missed it");
}
else
{
base.AttackChracter(opponent);
Console.WriteLine("Monster Attacked player");
}
}
}
游戏类

游戏类负责实例化玩家和怪兽、记录回合数、判断游戏是否结束,暴露可调用的公共方法给游戏操作类。

class Game
{
private Character m_player;
private Character m_monster;
private int m_round;
private float playerCriticalPossible = 0.6f;
private float monsterMissingPossible = 0.2f; public Game()
{
m_player = new Player(playerCriticalPossible)
{
HealthPoint = 15,
AttackPoint = 2
};
m_monster = new Monster(monsterMissingPossible)
{
HealthPoint = 20,
AttackPoint = 6
};
} public bool IsGameOver => m_monster.HealthPoint == 0 || m_player.HealthPoint == 0; public void AttackMonster()
{
m_player.AttackChracter(m_monster);
} public void AttackPlayer()
{
m_monster.AttackChracter(m_player);
} public void CurePlayer()
{
m_player.Cure();
} public void BeginNewRound()
{
m_round++;
} public void ShowGameState()
{
Console.WriteLine("".PadLeft(20, '-'));
Console.WriteLine("Round:{0}", m_round);
Console.WriteLine("player health:{0}", "".PadLeft(m_player.HealthPoint, '*'));
Console.WriteLine("monster health:{0}", "".PadLeft(m_monster.HealthPoint, '*'));
}
}
游戏操作类

在我们这个简易游戏中,没有UI代码,游戏操作类负责在用户输入和游戏中搭建一个桥梁,解释用户的输入。

class GameRunner
{
private Game m_game;
public GameRunner(Game game)
{
m_game = game;
} public void Run()
{
while (!m_game.IsGameOver)
{
m_game.BeginNewRound();
bool validSelection = false;
while (!validSelection)
{
m_game.ShowGameState();
Console.WriteLine("Make your choice: 1. attack 2. Cure");
var str = Console.ReadLine();
if (str.Length != 1)
{
continue;
}
switch (str[0])
{
case '1':
{
validSelection = true;
m_game.AttackMonster();
break;
}
case '2':
{
validSelection = true;
m_game.CurePlayer();
break;
}
default:
break;
}
}
if(!m_game.IsGameOver)
{
m_game.AttackPlayer();
}
}
}
}
客户端

客户端的代码就非常简单了,只需要实例化一个游戏操作类,然后让其运行就可以了。

class Program
{
static void Main(string[] args)
{
Game game = new Game();
GameRunner runner = new GameRunner(game);
runner.Run();
}
}

试着运行一下,

看起来一切都好。

 

加上存档

虽然游戏可以正常运行,但是总感觉还是少了点什么。嗯,存档功能,一个游戏没有存档是不健全的,毕竟,人生虽然没有存档,但是游戏可是有的!让我们加上存档功能吧,首先想想怎么设计。

 

需要存档的数据

首先我们要明确,有哪些数据是需要存档的,在这个游戏中,玩家的生命值、攻击力、暴击率;怪兽的生命值、攻击力和丢失率,游戏的回合数,都是需要存储的对象。

 

存档定义

这是一个需要仔细思考的地方,一般来说,需要考虑以下几个地方:

  • 存档需要访问一些游戏中的私有字段,比如暴击率,需要在不破坏游戏封装的情况下实现这个功能
  • 存档自身需要实现信息隐藏,即除了游戏,其他类不应该访问存档的详细信息
  • 存档不应该和游戏存放在一起,以防不经意间游戏破坏了存档数据,应该有专门的类存放存档

 

备忘录模式出场

这个时候应该是主角出场的时候了。看看备忘录模式的定义

在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态

再看看UML,

(图片来自网络,如果侵权请联系我删除)

看起来完全符合我们的需求啊,Originator就是游戏类,知道如何创造存档和从存档中恢复状态,Memento类就是存档类,Caretaker是一个新类,负责保存存档。

经过思考,我们决定采取备忘录模式,同时加入以下措施:

  • 将存档定义为游戏中的私有嵌套类,这样存档可以毫无压力的访问游戏中的私有字段,同时外界永远没有办法去实例化或者尝试通过转型来获得这个类,完美的保护了存档类
  • 存档类是一个简单的数据集合,不包含任何其他逻辑
  • 添加一个存档管理器,可以放在游戏操作类中,可以通过它看到我们当前有没有存档
  • 存档放在存档管理器中
  • 存档实现一个空接口,在存档管理器中以空接口形式出现,这样外部类在访问存档的时候,仅能看到这个空接口。而在游戏类内部,我们在使用存档之前先通过向下转型实现类型转换(是的,向下转型不怎么好,但是偶尔可以用一下)
代码实现
空接口
interface IGameSave
{ }
私有嵌套存档类

该类存放在game里面,无压力地在不破坏封装的情况下访问game私有字段

private class GameSave : IGameSave
{
public int PlayerHealth { get; set; }
public int PlayerAttack { get; set; }
public float PlayerCritialAttackPossible { get; set; }
public int MonsterHealth { get; set; }
public int MonsterAttack { get; set; }
public float MonsterMissingPossible { get; set; }
public int GameRound { get; set; }
}
创建存档和从存档恢复

game中添加创建存档和从存档恢复的代码,在从存档恢复的时候,使用了向下转型,因为从存档管理器读出来的只是空接口而已

public IGameSave CreateSave()
{
var save = new GameSave()
{
PlayerHealth = m_player.HealthPoint,
PlayerAttack = m_player.AttackPoint,
PlayerCritialAttackPossible = playerCriticalPossible,
MonsterAttack = m_monster.AttackPoint,
MonsterHealth = m_monster.HealthPoint,
MonsterMissingPossible = monsterMissingPossible,
GameRound = m_round
};
Console.WriteLine("game saved");
return save;
} public void RestoreFromGameSave(IGameSave gamesave)
{
GameSave save = gamesave as GameSave;
if(save != null)
{
m_player = new Player(save.PlayerCritialAttackPossible) { HealthPoint = save.PlayerHealth, AttackPoint = save.PlayerAttack };
m_monster = new Player(save.MonsterMissingPossible) { HealthPoint = save.MonsterHealth, AttackPoint = save.MonsterAttack };
m_round = save.GameRound;
}
Console.WriteLine("game restored");
}
存档管理器类

添加一个类专门管理存档,此类非常简单,只有一个存档,要支持多存档可以考虑使用List

    class GameSaveStore
{
public IGameSave GameSave { get; set; }
}
在游戏操作类添加玩家选项

首先在游戏操作类中添加一个存档管理器

private GameSaveStore m_gameSaveStore = new GameSaveStore();

接着修改Run方法添加用户操作

public void Run()
{
while (!m_game.IsGameOver)
{
m_game.BeginNewRound();
bool validSelection = false;
while (!validSelection)
{
m_game.ShowGameState();
Console.WriteLine("Make your choice: 1. attack 2. Cure 3. Save 4. Load");
var str = Console.ReadLine();
if (str.Length != 1)
{
continue;
}
switch (str[0])
{
case '1':
{
validSelection = true;
m_game.AttackMonster();
break;
}
case '2':
{
validSelection = true;
m_game.CurePlayer();
break;
}
case '3':
{
validSelection = false;
m_gameSaveStore.GameSave = m_game.CreateSave();
break;
}
case '4':
{
validSelection = false;
if(m_gameSaveStore.GameSave == null)
{
Console.WriteLine("no save to load");
}
else
{
m_game.RestoreFromGameSave(m_gameSaveStore.GameSave);
}
break;
}
default:
break;
}
}
if(!m_game.IsGameOver)
{
m_game.AttackPlayer();
}
}
}

注意,上面的3和4是新添加的存档相关的操作。试着运行一下。

看起来一切正常,这样我们就使用备忘录模式,完成了存档读档的功能。

 

结语

这就是备忘录模式的使用,如果大家以后遇到这种场景

  • 想要保存状态,又不想破坏封装
  • 需要把状态保存到其他地方

那么就可以考虑使用这个模式。

游戏有存档,人生没存档,愿我们把握当下,天天努力。

祝大家端午安康,下次见。

如果人生也能存档——C#中的备忘录模式的更多相关文章

  1. [工作中的设计模式]备忘录模式memento

    一.模式解析 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象.备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把 ...

  2. 重学 Java 设计模式:实战备忘录模式「模拟互联网系统上线过程中,配置文件回滚场景」

    作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 实现不了是研发的借口? 实现不了,有时候是功能复杂度较高难以实 ...

  3. 制作类似ThinkPHP框架中的PATHINFO模式功能

    一.PATHINFO功能简述 搞PHP的都知道ThinkPHP是一个免费开源的轻量级PHP框架,虽说轻量但它的功能却很强大.这也是我接触学习的第一个框架.TP框架中的URL默认模式即是PathInfo ...

  4. 设计模式(一):“穿越火线”中的“策略模式”(Strategy Pattern)

    在前段时间呢陆陆续续的更新了一系列关于重构的文章.在重构我们既有的代码时,往往会用到设计模式.在之前重构系列的博客中,我们在重构时用到了“工厂模式”.“策略模式”.“状态模式”等.当然在重构时,有的地 ...

  5. 4.在MVC中使用仓储模式进行增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-using-the-repository-pattern-in-mvc/ 系列目录: ...

  6. Qt 中使用Singleton模式需小心

    在qt中,使用Singleton模式时一定要小心.因为Singleton模式中使用的是静态对象,静态对象是直到程序结束才被释放的,然而,一旦把该静态对象纳入了Qt的父子对象体系,就会导致不明确的行为. ...

  7. 安卓中的Model-View-Presenter模式介绍

    转载自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0425/2782.html 英文原文:Introduction to M ...

  8. Objective-C中的委托(代理)模式

    我个人更喜欢把委托(Delegate)模式称为代理(Proxy)模式.还是那句话,第一次接触代理模式是在Java中接触的,在Java中实现代理模式和接口是少不了的.当时学习Spring的时候用到了接口 ...

  9. Android -- 思考 -- 为什么要在项目中使用MVP模式

    1,其实有时候一直在找借口不去思考这个问题,总是以赶项目为由,没有很认真的思考这个问题,为什么我们要在项目中使用MVP模式,自己也用MVP也已经做了两个项目,而且在网上也看了不少的文章,但是感觉在高层 ...

随机推荐

  1. 如何管理win系列服务器,win10 pro如何 使用远程桌面

    远程桌面,大家都理解,专业的运维人员都是连接上百台服务器进行操作管理工作. 先介绍一款专业的远程桌面管理工具:iis7远程桌面批量管理  win10 pro如何 使用远程桌面? 一. 首先在win10 ...

  2. .NET编程5月小结 - Blazor, Unity, Dependency Injection

    本文是我在5月份看到的一些有趣的内容的集合.在这里你可以找到许多有关Blazor.ASPNET Core的学习资源和示例项目,有关在Unity中使用Zenject进行单元测试的博客,有关Unity项目 ...

  3. JavaScript (三) js的基本语法 - - 数组 与函数 、关键字

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.关键字 1.break 关键字使用 break关键字:如果在循环中使用,遇到了break,则立刻跳出 ...

  4. Java实现 第十一届 蓝桥杯 (高职专科组)省内模拟赛

    有错误的或者有问题的欢迎评论 十六进制数1949对应的十进制数 19000互质的数的个数 70044与113148的最大公约数 第十层的二叉树 洁净数 递增序列 最大的元素距离 元音字母辅音字母的数量 ...

  5. Java实现 蓝桥杯VIP 算法训练 确定元音字母位置

    算法训练 确定元音字母位置 时间限制:1.0s 内存限制:512.0MB 输入一个字符串,编写程序输出该字符串中元音字母的首次出现位置,如果没有元音字母输出0.英语元音字母只有'a'.'e'.'i'. ...

  6. Arrays.binarySearch和Collections.binarySearch的详细用法

    概述 binarysearch为在指定数组中查找指定值得索引值,该值在范围内找得到则返回该值的索引值,找不到则返回该值的插入位置,如果该值大于指定范围最大值则返回-(maxlength+1),而: i ...

  7. Java实现中值问题

    中值问题是求一个n个数列表中某一数组下标k,它要求该下标元素比列表中的一半元素大,又比另一半元素小,这个中间的值被称为中值. 使用Lomuto划分算法思想,此处引用<算法设计与分析基础>第 ...

  8. PAT D进制的A+B

    输入两个非负 10 进制整数 A 和 B ( <=230-1 ) ,输出 A+B 的 D (1<D<=10) 进制数. 输入格式: 输入在一行中依次给出 3 个整数 A.B 和 D. ...

  9. MD760按键说明书

  10. iOS-AutoLayout中动画使用的细节 和 iOS layout机制

    在Main.storyboard拖入一个UIView,随便设置一个背景色, 使用autolayout  为紫色的view添加约束 :(0,0,100,100) , 为该view添加动画代码如下: #i ...