如果人生也能存檔——C#中的備忘錄模式
大家好,老胡又和大家見面了。首先承認今天的博客有點標題黨了,人生是沒有存檔,也沒有後悔葯的。有存檔和後悔葯的,那是遊戲,不知道這是不是遊戲讓人格外放鬆的原因之一。
今天恰逢端午放假,就讓我們來試着做一個小遊戲吧,順帶看看備忘錄模式是如何在這種情況下面工作的。
遊戲背景
這是一個簡單的打怪遊戲,有玩家,有怪獸,玩家作為主角光環,有如下三個特殊能力
- 攻擊怪獸有暴擊幾率
- 有幾率迴避怪獸攻擊
- 可以自己治療一定生命值
遊戲實現
角色類
角色基類
首先是角色類,角色類提供玩家和怪獸最基本的抽象,比如血量、攻擊力、攻擊和治療。(對於怪獸來說,治療是沒有提供實現的,壞人肯定不能再治療了)
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是新添加的存檔相關的操作。試着運行一下。
看起來一切正常,這樣我們就使用備忘錄模式,完成了存檔讀檔的功能。
結語
這就是備忘錄模式的使用,如果大家以後遇到這種場景
- 想要保存狀態,又不想破壞封裝
- 需要把狀態保存到其他地方
那麼就可以考慮使用這個模式。
遊戲有存檔,人生沒存檔,願我們把握當下,天天努力。
祝大家端午安康,下次見。