Разработка игры на C# (Mini RPG) Часть 3 из 5. Движок игры

Определим дополнительно правила игры.

В случае победы игроком над монстром, игрок получает монеты и у него отнимается здоровье. В случае поражения у игрока отнимается больше здоровья. В магазине оружия можно купить оружие. Купленный предмет добавляется в рюкзак игроку и эффект наступает мгновенно. Оружие добавляет несколько очков силы. Броня добавляет несколько очков максимального здоровья.

Архитектура сборки движка игры представлена на рисунке ниже.

C# движок игры РПГ

Действия

Все действия определены в перечислении

public enum ActionTypes
{
    Attack = 1,
    BuyWeapon = 2,
    BuyArmor = 3,
    Heal = 4
}

Для реализации механизма действий определим интерфейс

public interface IAction
{
    /// <summary>
    /// Action Type
    /// </summary>
    ActionTypes Type { get; }

    /// <summary>
    /// Core process action
    /// </summary>
    /// <param name="state">State to change player info</param>
    /// <param name="config">Game config with settings</param>
    /// <returns>Result of action</returns>
    ActionResultBase Process(GameState state, GameConfiguration config);

    /// <summary>
    /// True if action can be processed by game
    /// </summary>
    bool CanApply(GameState state, GameConfiguration config);
}

И абстрактный класс результата выполнения действия

/// <summary>
/// Game action result
/// </summary>
public abstract class ActionResultBase
{
    protected ActionResultBase()
    {
        IsSuccessful = true;
    }

    public bool IsSuccessful { get; set; }
    public bool IsDead { get; set; }
}

Сами действия реализуются однотипно, поэтому подробно все их рассматрировать не будем, а посмотрим на примере одного (исходный код можно скачать) и особенности остальных. Рассмотрим на примере атаки.

Для результата выполнения действия определим класс

public sealed class AttackActionResult : ActionResultBase
{
    public bool IsWin { get; set; }
    public int Level { get; set; }
}

Основной код обработки действия ниже. Сначала по формуле вычисляем победил ли персонаж монстра или нет. В этом участке можно добавить уровень монстра и другие параметры в формулу. Считаем шанс на победу из 100 возможных. Если игрок победил, то увеличиваем уровень (эквивалент опыта, но за 1 монстра дается 1 уровень). В части определения результата, в случае победы заносится информация из секции настроек / победы, в противном случае для применения эффектов приравниваются настройки из секции поражения.

public ActionResultBase Process(GameState state, GameConfiguration config)
{
    AttackActionResult result = new AttackActionResult();

    int winProbability =
        Math.Min(
            config.Battle.MinWinProbability + state.CurrentPlayer.Power*config.Battle.IncreasePowerProbability,
            config.Battle.MaxWinProbability);

    int random = Generator.Next(1, 100);

    BattleResultConfiguration battleResult = null;

    //Win
    if (random <= winProbability)
    {
        battleResult = config.Battle.WinResult;
        result.IsWin = true;
        state.Attacks++;
    }
    //Loose
    else
    {
        battleResult = config.Battle.LooseResult;
    }

    state.CurrentPlayer.ApplyDamage(-GetDamage(state.CurrentPlayer.Health, battleResult));
    state.CurrentPlayer.AddCoins(battleResult.CoinsChange);
    result.Level = state.Attacks;

    if (state.CurrentPlayer.Health <= 0)
    {
        result.IsDead = true;
    }

    return result;
}

Для всех магазинов характерно определения значения эффекта.

internal abstract class ShopActionBase
{
    protected int GetItemEffect(ShopConfiguration shop)
    {
        int delta = shop.EffectTo - shop.EffectFrom;
        if (delta == 0)
            return shop.EffectTo;

        return shop.EffectFrom + Generator.Next(0, delta);
    }
}

Стоит остановиться подробнее на магазинах. Все магазины могут изменять соответствующие параметры персонажа. Значения эффекта получается случайным образом из промежутка [От, До], определенного в настройках игры. Реализован простейший потокобезопасный генератор псевдослучайных чисел, на основе стандартного Random.

private static readonly Random _generator = new Random();
private static readonly object locker = new object();
/// <summary>
/// Thread safe generate [min,max]
/// </summary>
public static int Next(int min, int maxValue)
{
    lock (locker)
    {
        //https://msdn.microsoft.com/en-us/library/2dx6wyd4(v=vs.110).aspx
        return _generator.Next(min, maxValue + 1);
    }
}

При покупке оружия увеличивается сила. При покупке брони увеличивается максимальное здоровье. А при покупке лечения увеличивается текущее здоровье на точное число (не промежуток), поэтому в настройках укажем в качестве промежутка два одинаковых числа. Результат применения магазина определяется одним числом EffectResult. И обрабатывается в соответствующем действии. Например эффект лечения.

public ActionResultBase Process(GameState state, GameConfiguration config)
{
    HealActionResult result = new HealActionResult();
    if (!CanApply(state, config))
    {
        result.IsSuccessful = false;
        return result;
    }

    state.CurrentPlayer.DecreaseCoins(config.Shops.Heal.Price);
    result.HealAction = state.CurrentPlayer.ApplyHeal(GetItemEffect(config.Shops.Heal));
    return result;
}

Персонаж

Информация о персонаже хранится в классе Player.

public class Player
{
    // Приватные поля для характеристик персонажа
    //  ...
    // Публичные свойства для характеристик

    
    public Player()
    {
        _items = new List<Item>();
    }


    /// <summary>
    /// Player items
    /// </summary>
    public IReadOnlyCollection<Item> Items
    {
        get { return new ReadOnlyCollection<Item>(_items); }
    }
    
    // ... 
    // Инициализация    

    #region Actions

    public void ApplyDamage(int damage)
    {
        _health -= damage;
        if (_health < 0)
            _health = 0;
    }


    public int ApplyHeal(int heal)
    {
        _health += heal;

        if (_health > _maxHealth)
        {
            heal = _health - _maxHealth;
            _health = _maxHealth;
        }

        return heal;
    }

    public void AddCoins(int coins)
    {
        _coins += coins;
    }

    public void DecreaseCoins(int coins)
    {
        if (_coins < coins)
            throw new PlayerDataException("Can't decrease");

        _coins -= coins;
    }

    /// <summary>
    /// Apply item effect and add to pocket
    /// </summary>
    public void ApplyItem(Item item)
    {
        _maxHealth += item.Health;
        _power += item.Damage;

        _items.Add(item);
    }

    #endregion
}

Текущее состояние игры хранится в классе GameState.

public sealed class GameState
{
    public Player CurrentPlayer { get; set; }

    /// <summary>
    /// Win attacks count
    /// </summary>
    public int Attacks { get; set; }

    public void Initialize(InitialPlayerConfiguration config)
    {
        Attacks = 0;

        CurrentPlayer = new Player();
        CurrentPlayer.Initialize(
            config.InitialPlayerHealth,
            config.InitialPlayerMaxHealth,
            config.InitialPlayerPower,
            config.InitialPlayerCoins);
    }

    public GameState DeepCopy()
    {
        return new GameState
        {
            Attacks = Attacks,
            CurrentPlayer = CurrentPlayer.DeepCopy()
        };
    }
}

Эти классы скорее модели чем сама игра. Далее необходимо разработать основной класс игры. Ранее мы определили классы настройки и классы состояния игры. Основной класс игры называется Game. Начало выглядит так

private readonly GameConfiguration _config;
private readonly GameState _gameState;

public Game(IGameConfigReader configReader)
{
    if (configReader == null)
        throw new ArgumentNullException("configReader");

    _config = configReader.ReadConfig();
    _gameState = new GameState();

    _gameState.Initialize(_config.InitialPlayer);
}

В этом месте мы объявляем приватные поля только для чтения (чтобы инициализировать их только 1 раз в конструкторе класса). Конструктор принимает интерфейс чтения настроек. Создается экземпляр состояния игры и инициализируется игрок. Текущая версия не предполагает сохранения, но возможность добавить ее есть. Загрузить ее нужно как раз где-то в этом месте (лучше конечно не в конструкторе объекта).

Далее нужно определить что текущая игра позволяет делать.

#region Actions

public BuyItemActionResult BuyItem(ItemTypes itemType)
{
    IAction butItemAction = new BuyItemAction(itemType);
    return ProcessAction<BuyItemActionResult>(butItemAction);
}

public HealActionResult Heal()
{
    IAction healAction = new HealAction();
    return ProcessAction<HealActionResult>(healAction);
}

public AttackActionResult Attack()
{
    IAction attackAction = new AttackAction();
    return ProcessAction<AttackActionResult>(attackAction);
}

private T ProcessAction<T>(IAction action) where T : ActionResultBase
{
    ActionResultBase result = action.Process(_gameState, _config);

    if (result.IsDead)
    {
        StartFromTheBeginning();
    }

    if (result is T)
        return (T) result;

    throw new InvalidCastException(string.Format("Invalid action processor for {0}", action));
}

#endregion

Конструкция private T ProcessAction<T>(IAction action) where T : ActionResultBase вводит ограничения на общий метод. Конкретно в этом случае, любой объект произвольного типа T на момент компиляции гарантируется наследником от класса ActionResultBase. В случае, если T не является наследником данного класса компилятор сгенерирует ошибку.

Остался еще один класс, который мы не определили. А именно класс пердставляющий предметы в игре.

public class Item
{
    public Item()
    {
    }

    public Item(Item copy)
    {
        ItemType = copy.ItemType;
        Damage = copy.Damage;
        Price = copy.Price;
        Health = copy.Health;
    }

    public ItemTypes ItemType { get; set; }
    public int Damage { get; set; }
    public int Price { get; set; }
    public int Health { get; set; }
}

На этом разработку движка можно закончить. Остается только реализовать интерфейс для игры и играть.

Если у вас после прочтения и изучения исходного кода появились вопросы или комментарии буду очень признателен.

Разработка игры на C# (Mini RPG)

Скачать исходный код игры oxozle.minirpg.zip 46 Кб.

Комментарии

comments powered by Disqus