Top.Mail.Ru

С# и концепция ООП. Продолжение

На прошлом уроке   я рассказал основы концепции ОПП и мы начали писать игру «Мафия» на C#.  Продолжаем.

Напомню краткое содержание: мы познакомились с различными парадигмами программирования (императивное, декларативное, функциональное ОПП и др.).  Было раскрыто преимущество ООП. Мы решили что-нибудь запрограммировать на C#, например, компьютерную игру, в качестве игры выбрали «Мафию». Но еще решили писать ее так, чтобы потом на основе этой игры можно было создать игру «Вторжение рептилоидов». Была предложена структура классов игры, и мы начали программировать, создали проект и заготовки нескольких классов.

Давайте продолжим разработку ядра игры:

    /// <summary>
    /// Шаги игры
    /// </summary>
    public enum GameStep
    {
        /// <summary>
        /// Мафия выбирает жертву
        /// </summary>
        MafiaChoosesVictim,

        /// <summary>
        /// Шериф ищет мафию
        /// </summary>
        SheriffSearchMafia,

        /// <summary>
        /// Маньяк выбирает жертву
        /// </summary>
        ManiacChoosesVictim,

        /// <summary>
        /// Доктор лечит
        /// </summary>
        DoctorChoosesPatient,

        //Путана берет на ночь человека
        PutanaTakePerson
    }

    /// <summary>
    /// Ядро игры
    /// </summary>
    public class Core
    {
        /// <summary>
        /// Список персонажей
        /// </summary>
        protected List<AbstractUnit> units;

        /// <summary>
        /// Есть маньяк
        /// </summary>
        public bool maniac_exits;

        /// <summary>
        /// Есть доктор
        /// </summary>
        public bool doctor_exists;

        /// <summary>
        /// Есть путана
        /// </summary>
        public bool putana_exists;

        public Core()
        {
            units = new List<AbstractUnit>();
        }

        /// <summary>
        /// Шаг игры 
        /// </summary>
        /// <returns>true - можно делать следующий шаг, false – нет больше шагов</returns>
        public bool step()
        {

        }
    } 

Итак, что мы сделали? Объявили перечисление (enum),  в которое забили всевозможные шаги. Добавили булевы переменные, которые будут у нас связаны с флажками настройками (чтобы мы могли опционально выбрать, будет ли у нас маньяк, доктор и путана). Теперь осталось в методе step реализовать нечто вроде конечного автомата. Но стоп! Вам нравиться то, что мы сейчас сделали? Подумайте хорошенько. С учетом того, что мы хотим сделать универсальное ядро игры и на его основе создать потом игру «Вторжение рептилоидов». Лично мне это решение уже не нравиться. Почему? Ну, например, решили мы добавить в игру еще одного персонажа, например «Продажный судья» или «Коррумпированный чиновник». Тогда в игре надо предусмотреть шаг «Судья сажает невиновного» или «Судья вставляет палки в колеса комиссару». Стоит заметить, что у персонажа может быть и два шага. В частности, добавив «Продажного судью» мы добавляем мафии еще один шаг «Мафия дает взятку судье».  И что, тогда добавлять новое поле «Есть продажный судья» и переписывать ядро игры?

Но как сделать алгоритм шагов более универсальным? Концепция ООП позволяет это. Например, можно предусмотреть в ядре список абстрактных объектов «Шаг», как мы сделали это для персонажей. Перебрать эти шаги в списке и вызвать у них, например, метод execute(), который у каждого шага будет реализован по-своему. Ну а сами шаги можно жестко задать где-нибудь в методе init(), который просто переписать, если мы решили изменить шаги игры. Тут надо будет переписать всего лишь один метод. Или даже не переписать, а просто вставить в нужное место код добавления объекта в список. И все. Проиллюстрирую это на примере одной моей программы, которую я писал для своей магистерской диссертации:

            steps = new List<Step>();
            steps.Add(new KFunctionStep());
            steps.Add(new SpecialPointsStep());
            steps.Add(new TrianglesStep());
            steps.Add(new HistogramStep());
            steps.Add(new MatchingStep());
            steps.Add(new DistanceStep());
            steps.Add(new ShiftStep());

Тут я использовал обычный список List, типизированный абстрактным классом. Все добавляемые в данном коде объекты являются потомками этого абстрактного класса. Для перехода к следующему шагу я просто увеличивал на единицу номер текущего шага и вызывал у соответствующего объекта метод выполнения. И вот как выглядит код выполнения очередного шага:

        /// <summary>
        /// Выполнить шаг
        /// </summary>
        /// <param name="picture_box">Картинка для визуализации</param>
        /// <returns>true - шаг выполнен, false - нет следующего шага</returns>
        public bool next_step(PictureBox picture_box)
        {
            if (data.current_step >= data.steps.Count) return false;
            data.steps[data.current_step].get_input_data((data.current_step==0 ? null : data.steps[data.current_step - 1]), this);
            data.steps[data.current_step].exec();
            if (picture_box != null) data.steps[data.current_step].visualize(picture_box,0);
            data.current_step++;
            return true;
        }

Все просто и лаконично, никаких запутанных конечных автоматов и «спагетти кода».  Нечто аналогичное можно сделать и в нашей игре. Но давайте еще раз немного подумаем. Каждый персонаж может участвовать в более чем одном шаге. Это раз. Добавляя нового персонажа, нам нужно добавлять для него хотя бы один объект наследник от некого абстрактного шага. А что если сделать как-то так по-хитрому, чтобы добавляя нового персонажа, мы автоматом создавали для него шаги? Например, у персонажа можно предусмотреть поле, которое показывает, чей следующий ход. Например, если у нас был ход мафии, а по правилам после того, как мафия засыпает, просыпается комиссар, мы просто указываем в поле «Следующий»  персонажа «Мафия» значение «Комиссар».  Но стоп. Мы забыли, что у нас может быть несколько персонажей одного типа. В таком случае, мы в поле «Следующий» будем указывать не конкретный объект, а тип. И следующий шаг «будит» все объекты заданного типа. А еще лучше предусмотреть некий абстрактный объект или даже интерфейс «Фильтр», который после окончания шага активирует объекты по определенному правилу (задел на будущее).

    /// <summary>
    /// Абстрактный класс персонажа
    /// </summary>
    public abstract class AbstractUnit
    {
        /// <summary>
        /// Фильтр персонажей следующего шага
        /// </summary>
        public IUnitFilter next_unit_filter;

        /// <summary>
        /// Конструктор по фильтру следующего шага
        /// </summary>
        /// <param name="a_next_unit_filter">Фильтр следюущего шага</param>
        public AbstractUnit(IUnitFilter a_next_unit_filter)
        {
            next_unit_filter = a_next_unit_filter;
        }

        /// <summary>
        /// Ход игрока
        /// </summary>
        public abstract void step();
}

И создадим интерфейс «Фильтр»:

    /// <summary>
    /// Фильтр следующего шага
    /// </summary>
    public interface IUnitFilter
    {
        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        List<AbstractUnit> get_next_unix(AbstractUnit unit);
     }

Итак, давайте сразу внесем изменения в наш абстрактный класс персонажа:

    /// <summary>
    /// Ядро игры
    /// </summary>
    public class Core
    {
        /// <summary>
        /// Список персонажей
        /// </summary>
        protected List<AbstractUnit> units;

        /// <summary>
        /// Игроки, которые делают ход
        /// </summary>
        protected List<AbstractUnit> next_units;

        public Core()
        {
            units = new List<AbstractUnit>();
        }

        /// <summary>
        /// Шаг игры 
        /// </summary>
        /// <returns>true - можно делать следующий шаг, false – нет больше шагов</returns>
        public bool step()
        {
            for(int i=0; i<next_units.Count; i++)
            {
                next_units[i].step();                
            }
            AbstractUnit unit = next_units[next_units.Count - 1];
            next_units=unit.next_unit_filter.get_next_unix(unit);
            if (next_units.Count == 0) return false;
            return true;
        }
    }

Итак, шаблон готов. Сейчас мы будем наполнять его программой. Какие там в мафии есть персонажи? Мафия, комиссар, город… Давайте начнем с мафии. Так и назовем его MafiaUnit:

    /// <summary>
    /// Персонаж "Мафия"
    /// </summary>
    public class MafiaUnit : AbstractUnit
    {
        /// <summary>
        /// Конструктор по фильтру
        /// </summary>
        /// <param name="a_next_unit_filter">Фильтр</param>
        public MafiaUnit(IUnitFilter a_next_unit_filter) : base(a_next_unit_filter)
        {
        }

        /// <summary>
        /// Ход игрока
        /// </summary>
        public override void step()
        {
            
        }
    }

Непонятно, а как мафия выбирает жертву? Из кого? У данного класса нет никакой ссылки на список персонажей, из которого выбирать. У нас полный список персонажей есть в классе Core. Можно, конечно, предусмотреть в AbstractUnit ссылку на Core, но стоит ли так делать? Тем более, что выбор нужно производить не из полного списка, из него нужно исключить других мафиозиев, а также тех, кто уже убит. В принципе, мы вполне можем получить список живых персонажей из Core, предусмотрев у него соответствующий метод. Сам список у нас имеет модификатор protected, и, поэтому, ничего страшного, если мы объявим в AbstractUnit ссылку на Core, так как к полному списку персонаж все равно не сможет получить доступ. Итак, давайте сначала у класса Core предусмотрим метод get_alive_units:

/// <summary>
/// Список живых пользователей
/// </summary>
/// <returns>Список</returns>
public List<AbstractUnit> get_alive_units()
{

}

Так, опять заглушка. Потому что непонятно, как определить, что юнит живой. Можно предусмотреть у него поле is_alive булевого типа, если мы убиваем персонажа, то просто присваиваем этому полю false. Но это будет не совсем верно. Не стоит давать возможность классу персонажа напрямую изменять поля других персонажей. Почему? Во-первых, может так случиться, что при изменении какого-то поля необходимо произвести какие-либо действия. Например, у нас персонаж показывается на экране в виде картинки, при изменении значения поля надо перерисовать эту картинку. Как тут быть? Можно вместо поля сделать свойство. Тогда при присваивании автоматически будет вызвал программный код, «повешенный» на это свойство. Он и выполнит нужные действия. Во-вторых, вдруг мы заходим сделать игру  без пользователей, только с ботами, и понаблюдать, как боты будут играть между собой. А потом захотим устроить соревнования между программистами, кто напишет лучшего бота. Только вот где гарантия, что программисты не воспользуются «читом» — кто им мешает, в нарушении правил игры, просто присвоить все противникам свойство is_alive значение false? Никто. Только если мы не придумаем какой-то промежуточный класс и не запретим такое прямое присваивание. В виду большой трудоемкости реализовать данную идею, мы это не будем делать, наша задача просто написать учебный вариант компьютерной игры. Сделаем проще, пусть будет свойство is_alive. Код на него вешать не будет, просто объявим is_alive как свойство, а не как поле, а потом, в случае необходимости, навешаем на него код. Объявим это поле мы в классе AbstractUnit:

 

/// <summary>
/// Живой ли персонаж
/// </summary>
public bool is_alive { get; set; }

get и set в фигурных скобках как раз и обозначает, что это свойство. После них должен быть программный код на чтение (после get) и на запись (после set). Если мы просто укажем get и set – то это зарезервированное свойство, то есть, это как бы поле, которое легким мановением руки преварщается… превращается поле …. В свойство! 

Все, теперь можно дописать метод get_alive_units:

        /// <summary>
        /// Список живых пользователей
        /// </summary>
        /// <returns>Список</returns>
        public List<AbstractUnit> get_alive_units()
        {
            return units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item.is_alive;
                });
         }

Немного пояснений к коду. Что такое FindAll? Это метод списка (List) который возвращает все найденные по условию элементы списка (в виде нового списка). Условие поиска задается в делегате (delegate). Что такое делегат? Это программный код, оформленный как делегат. По сути анонимная функция. Здесь та анонимная функция (делегат) передается в качестве параметра метода FindAll. Когда мы передаем куда-то делегат, он, как правило, должен возвращаться значение, в нашем случае он должен возвращать значение булевого типа, что он и делает, флаг is_alive у нас как раз имеет булевый тип.

Теперь возвращаемся к методу step() класса MafiaUnit. Стоп, у нас же нет ссылки на Core. Давайте объявим ее в классе AbstractUnit:

/// <summary>
/// Ссылка на ядро
/// </summary>
public Core core;

А чтобы мы не забывали передать ссылку, изменим конструктор:

        /// Конструктор по фильтру следующего шага и ссылке на ядро
        /// </summary>
        /// <param name="a_core">Ссылка на ядро</param>
        /// <param name="a_next_unit_filter">Фильтр следующего шага</param>
        public AbstractUnit(Core a_core, IUnitFilter a_next_unit_filter)
        {
            core = a_core;
            next_unit_filter = a_next_unit_filter;
        }

Теперь-то не забудем, иначе программа у нас просто не скомпилируется, напомнив нам вот таким вот сообщением об ошибке:

 

Исправляем конструктор класса MafiaUnit:

        /// <summary>
        /// Конструктор по фильтру
        /// </summary>
        /// <param name="a_core">Ссылка на ядро</param>
        /// <param name="a_next_unit_filter">Фильтр</param>
        public MafiaUnit(Core a_core, IUnitFilter a_next_unit_filter) : base(a_core, a_next_unit_filter)
        {
        }

Теперь самое время вернутся к методу step класса MafiaUnit:

 

/// <summary>
        /// Ход игрока
        /// </summary>
        public override void step()
        {
            List<AbstractUnit> units = core.get_alive_units();
            List<AbstractUnit> victims = units.FindAll(
                delegate (AbstractUnit item)
                {
                    return !(item is MafiaUnit);
                });
        }

 

Мы получили список живых персонажей, а потом отфильтровали его, исключив мафию. Надо выбрать жертву из полученного списка. Как это сделать? Давай на начальном этапе поступим максимально просто: выбреем случайного человека. Потом, если что, усложним алгоритм. Нам понадобиться ГСЧ. Это класс Random, нам надо объявить экземпляр этого класса. Желательно, только один. А вот тут проблема. Где его объявить и как передать во все объекты, которые нуждаются в ГСЧ? Создать в каждом отдельном случае новый экземпляр класса не вариант, это расточительство какое-то. Как быть? К счастью, в C# есть статические поля и методы.  Что это такое? Это такие поля и методы, которые общие для класса, и не привязаны к его конкретному экземпляру. По сути, аналог переменой или функции императивного языка программирования. Вот как объявляется статическое поле (всего лишь добавляем к нему static):

 

/// <summary>
/// Генератор случайных чисел
/// </summary>
public static Random rnd = new Random();

Мы его объявили и сразу же инициализировали. Разумеется, мы объявить его моем не где попало, а в каком-то классе. Например, в классе Core. Вот так теперь будет выглядеть сам класс:

 

    /// <summary>
    /// Ядро игры
    /// </summary>
    public class Core
    {
        /// <summary>
        /// Список персонажей
        /// </summary>
        protected List<AbstractUnit> units;

        /// <summary>
        /// Игроки, которые делают ход
        /// </summary>
        protected List<AbstractUnit> next_units;

        /// <summary>
        /// Генератор случайных чисел
        /// </summary>
        public static Random rnd = new Random();

        public Core()
        {
            units = new List<AbstractUnit>();
        }

        /// <summary>
        /// Шаг игры 
        /// </summary>
        /// <returns>true - можно делать следующий шаг, false – нет больше шагов</returns>
        public bool step()
        {
            for(int i=0; i<next_units.Count; i++)
            {
                next_units[i].step();                
            }
            AbstractUnit unit = next_units[next_units.Count - 1];
            next_units=unit.next_unit_filter.get_next_unix(unit);
            if (next_units.Count == 0) return false;
            return true;
        }

        /// <summary>
        /// Список живых пользователей
        /// </summary>
        /// <returns>Список</returns>
        public List<AbstractUnit> get_alive_units()
        {
            return units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item.is_alive;
                });
        }
    }

Как использовать? Через имя класса и точку, например, вот так:

int victim_index = Core.rnd.Next(victims.Count);

Метод Next класса Random генерирует целое случайное число от нуля(включительно) до указанного параметра(не включительно). То есть, если мы укажем параметр 10, то ГСЧ будет генерировать числа от 0 до 9. Если в качестве параметра мы укажем размер списка, то рандомизатор сгенерирует случайный индекс элемента из этого списка. В списке или массиве счет элементов идет с нуля. Последний элемент имеет индекс количество элементов минус 1. Вот полный код метода step класса MafiaUnit:

        /// <summary>
        /// Ход игрока
        /// </summary>
        public override void step()
        {
            List<AbstractUnit> units = core.get_alive_units();
            List<AbstractUnit> victims = units.FindAll(
                delegate (AbstractUnit item)
                {
                    return !(item is MafiaUnit);
                });
            int victim_index = Core.rnd.Next(victims.Count);
            core.kill(victims[victim_index]);
        }

Метода kill у класса Core еще нет, давайте его напишем:

        /// <summary>
        /// Убить персонажа
        /// </summary>
        /// <param name="unit">Персонаж</param>
        public void kill(AbstractUnit unit)
        {
            unit.is_alive = false;
        }

И все? — Удивитесь вы. Нет, не все, тут еще надо бы какие-то действия сделать, пока непонятно какие. Ну, там сообщить, что кто-то убит. Кстати, насчет сообщить. Давайте сразу изучим такую возможность C#, как ивенты – обработчики событий. По сути, это такие подпрограммы, которые вызываются при наступлении определенного события. Например, когда убивают персонажа.

Для того, чтобы можно было создать такой обработчик, нужнее делегат:

    /// <summary>
    /// Этот делегат используется как событие персонажа
    /// </summary>
    public delegate void UnitEvent(AbstractUnit unit);

Создадим этот делегат в том же файле, где у нас Core:

Теперь мы можем задать у класса Core поле, в котором будет храниться ссылка на обработчик событий:

/// <summary>
/// Событие: "Персонаж убит"
/// </summary>
public UnitEvent OnUnitKilled;

Чтобы при наступлении события был вызван этот обработчик, в методе kill обеспечим ему вызов:

        /// <summary>
        /// Убить персонажа
        /// </summary>
        /// <param name="unit">Персонаж</param>
        public void kill(AbstractUnit unit)
        {
            unit.is_alive = false;
            OnUnitKilled?.Invoke(unit);
        }

Синтаксис связывания ивента с конкретной подпрограммой:

<переменная типа Core>.OnUnitKilled+=<Имя подпрограммы — обработчика>

Но более подробно об этом в следующей части, когда мы начнем писать интерфейсную часть.

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar