首頁 > 軟體

C#備忘錄人生存檔的設計模式範例

2022-06-01 22:01:04

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是新新增的存檔相關的操作。試著執行一下。

看起來一切正常,這樣我們就使用備忘錄模式,完成了存檔讀檔的功能。

結語

這就是備忘錄模式的使用,如果大家以後遇到這種場景

  • 想要儲存狀態,又不想破壞封裝
  • 需要把狀態儲存到其他地方

那麼就可以考慮使用這個模式。

遊戲有存檔,人生沒存檔,願我們把握當下,天天努力。

祝大家端午安康,更多關於C#備忘錄設計模式的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com