Как я могу расположить несвязанную структуру классов для простой игры? - PullRequest
4 голосов
/ 23 августа 2011

Сейчас у меня шесть классов:

  1. Listener - управляет сокетными соединениями
  2. Мир - коллекция сущностей и задач
  3. Ticker - координаты обновления мира
  4. MessageProcessor - получает команды от игроков
  5. Интеллект - управляет поведением неигровых персонажей
  6. Задачи - отслеживание и выполнение задач

Но они похожи на спагетти со ссылками друг на друга повсюду ... Мир - это модель данных, которую модифицируют классы MessageProcessor, Intelligence и Tasks. Тикер координирует эти три класса, обновляя Мир. Слушатель используется MessageProcessor для входящих сообщений, а другие классы для отправки обновлений.

Как я могу улучшить эту ситуацию?

Ответы [ 2 ]

4 голосов
/ 23 августа 2011

Я недавно дал связанный ответ . Речь шла об улучшении тестируемости кода, для которого общим решением является ослабление связи. Основное внимание в предыдущем ответе было уделено отделению кода, связанного с сетью, от всего мира, и это логично, потому что сетевой код не тестируется на единицу, и его тоже сложно высмеять.

Приведенное решение заключалось в использовании интерфейса для входящих сообщений, например, чтобы отделить MessageProcessor (названный в другом посте «Обработчик») от сетевого кода и аналогичным образом отделить UpdateNotifier от мира.

enter image description here

Пунктирная линия - это просто косвенная ссылка, обрабатываемая интерфейсом или делегатом. В настоящее время не существует прямой связи между Миром и сетевым компонентом, что делает его проверяемым. Это на самом деле просто применение шаблона Model View Adapter .

M-V-A

Это не похоже на дизайн, который вы описали, за исключением того, что вам не хватает нескольких интерфейсов. С этим шаблоном UpdateNotifiers на основе интерфейса, используемым для отправки обновлений, я, по существу, повторно использую ту же архитектуру для обработки NPC, задач или чего-либо еще, что обрабатывается в другом месте. Вы выбираете нужные вам уведомления о событиях для конкретной области и внедряете для них конкретный класс Notifier, чтобы у вас было несколько адаптеров на одну модель.

enter image description here

И это на самом деле выглядит только сложнее, чем есть. Объект World не имеет прямых зависимостей от чего-либо еще, а каждый другой класс имеет не более одной прямой зависимости. Вы также можете изолировать таймер от мира, так как он, вероятно, не нужен там, но, возможно, одним из самых больших препятствий является обработка синхронизации между различными адаптерами.

3 голосов
/ 23 августа 2011

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

Модель

Я бы сказал, из чегоВы описали, главное, что меня поражает, это то, что вы захотите начать реализацию общей функциональности в модели классов, вам понадобятся либо интерфейсы, либо базовые классы, которые вы можете использовать для получения объектов высокого уровня.

Таким образом, вы можете обрабатывать вещи последовательно, без особых дополнительных усилий. Я думаю, что идея «архитектурных слоев» может быть полезна в качестве первого взгляда на то, как об этом думать (например, низкоуровневое оборудование, обработка сокетов и т. Д.)., затем средние слои, такие как то, что происходит в вашей игре, подробности о том, как работает игровая механика и т. д., и вещи высокого уровня, такие как то, что могут делать ПК или NPC, что делает окружающая среда и т. д.а также мысль о том, что вы никогда не хотите «прыгать» слои). Однако, когда дело доходит до этого важная вещьэто просто найти правильные абстракции для вашей игры и сохранить все организованным таким образом, чтобы вы никогда не чувствовали, что часть кода, над которым вы работаете, выполняет две совершенно разные разновидности вещей.

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

Например, вы хотите сосредоточиться на том, что происходит на высоком уровне в объектах более высокого уровня: в ИИ возможно «начать движение к определенному месту», «установить мою скорость», «остановить движение»;а в подсистеме среды «начать дождь», «увеличить скорость ветра», «приглушить свет»;в пользовательском классе «огнестрельное оружие», «сон», «заклинание».Но я бы не хотел, чтобы кто-либо из моих высокоуровневых классов даже знал о таких вещах, как «отправить сообщение миру», «сбросить таймер жажды», «получить данные сокета» или «цикл работоспособности».поставить галочку".(Это всего лишь разъяснения, а не предложения.; D)

События

Например, я думаю, что было бы полезно иметь один объект, отвечающий за отправку событий в Мир таким образом.вам больше не нужно, чтобы все говорили со всеми.Скорее всего, я бы просто создал набор вещей для обработки событий в целом.Итак, возможно EventMain и enumEvents, которые вы используете, чтобы у каждого типа событий был специальный идентификатор.А затем используйте Event в качестве базового класса для определенных событий, которые требуют дополнительной функциональности.(Я имею в виду как идентификатор, так и модель деривации, так что такие вещи, как Dispatcher, которые, вероятно, должны знать только самые базовые сведения о событии, не должны также знать о производных классах. Например,Диспетчер может принять событие и отправить его, даже не зная внутренности производного события. Это может или не может оказаться полезным, но хорошо иметь варианты.) Вы также можете иметь EventDispatcherу которого есть очередь событий для отправки в другие подсистемы.

Вам понадобится что-то общее для получения и отправки событий.Вы можете создать EventSourcer и EventSinker автономные классы, которые можно настроить в любом классе, который генерирует или получает события.Или вы могли бы вместо этого сделать IEventSource и IEventSink, чтобы вы могли реализовать общий интерфейс для нескольких классов, или, возможно, общий класс EventSourceAndSink, который реализует оба, и который является частью вашей модели базового класса, так что всеэто может потребоваться для обработки событий может просто произойти из этого.

Я бы, наверное, сделал ProtocolEncoder и ProtocolDecoder классы.Вы всегда можете объединить их в один объект, но это может оказаться полезным и не должно вызывать каких-либо проблем, если все сделано правильно, чтобы они были двумя отдельными частями кода.Вы также можете иметь класс ProtocolHelper, который выделяет что-то общее между ними.Единственная задача кодировщиков - получать сообщения из сети и превращать их в события для вашей игры, которые затем передаются в EventDispatcher.Класс декодера будет принимать события от диспетчера, которые должны выйти в сеть, и он будет принимать данные от них и отправлять их.

Как получить, куда вы идете

Поскольку у вас есть рабочий код, я бы порекомендовал вам начать делать это там, где это кажется естественным.Это могут быть вещи, которые утомляют вас, или вещи, которые вы заметили, очень похожие в разных местах, которые вы можете сделать регулярными с классом или каким-либо другим типом абстракции, затем вытащить старое и вставить новое.После того, как вы выяснили работоспособный первый вариант классовой модели, он должен дать вам идеи, основанные на том, что у вас уже есть, и по ходу дела постоянно пересматривать свою модель, исправляя вещи, которые являются проблемой, вспенивать, полоскать, повторять.

Это не должно быть много работы, на самом деле, некоторые из самых приятных моментов, когда я работал над кодом, были, когда я смог сделать аккуратный рефакторинг, который оставил некогда отвратительный беспорядокв гораздо лучшей форме, удалив много трудного для понимания кода и заменив его чем-то более простым для понимания, с меньшим количеством строк кода, и это открыло путь к моему следующему добавлению, доставляя удовольствие вместо другого "zomgМне не нужно снова касаться этого кода? "момент.

Желаю удачи, вот номинальное руководство к вещам, о которых я говорил;первый бит более подробный, потому что основной класс событий является одним из наиболее важных понятий, а затем я попытаюсь просто дать краткий обзор классов и их взаимодействия.Я уверен, что мог бы потратить на это еще больше часов, но сейчас я просто скажу: спросите меня, если у вас есть вопросы, и я сделаю все возможное, чтобы дать вам хороший ответ:)

Идеи в коде

О, еще одно замечание: я вообще не имел дело с сложностями, добавленными, если у вас несколько потоков;Есть вещи, от простого до запутанного, чтобы управлять всем этим, если вы делаете, но это еще одно упражнение.:)

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

    // this is internal to the project namespace, say, TimsWorld_o_Hurt
    // I'm now resisting calling everything Xxxx_o_Hurt :)

    // examples o' hurt
using EventHandlingLibrary;

namespace EventHandlingLibrary
{
    // this will provide the base class for all the events, and can
    // also have static methods like factory methods, destination 
    // lookups etc. 

    // I have the enums set to protected with the intent being that
    // specific factory functions should be called by other classes.
    // You should change this if it turns out to be too cumbersome.
    public class EventOfHurt
    {
        #region Event Definitions
            protected enum EEventType
            {
                // System Events
                SystemInitializing,
                SubsystemInitComplete,
                FatalErrorNotification,
                SubsystemPingReponse,
                SubsystemPingRequest,

                // Network Events
                FrameRateError,
                ThroughputData,
                ServerTimeout,
                ServerPingRequest,
                ServerPingResponse,

                // User Events
                WeaponsFire,
                MovementNotification,
                FatigueUpdate

                // and so forth
            }

            protected enum ESubsystem
            {
                System,
                Dispatcher,
                TickerTimer,
                WorldEntity,
                WorldTaskManager,
                UserMessageProcessor,
                NetworkListener,
                NetworkTransmitter,
                ProtocolEncoder,
                ProtocolDecoder,
                PlayerCharacter,
                NonPlayerCharacter,
                EventSink,
                EventSource

                // and such
            }
        #endregion

        #region Event Information
            public Guid EventId { get; protected set; }
            public EEventType EventType { get; protected set; }
            public ESubsystem SourceSubsystem { get; protected set; }
            public ESubsystem DestSubsystem { get; protected set; }

            private List<Tuple<EventOfHurt, DateTime>> 
                myEventReferences;

            // the event(s) that triggered it, if any, and when rec'd
            public Tuple<EventOfHurt, DateTime>[] 
                EventReferences 
            { 
                get { return myEventReferences.ToArray(); } 
            }

            public DateTime Timestamp { get; private set; }
        #endregion

        // we'll be using factor methods to create events
        // so keep constructors private; no default constructor
        private EventOfHurt(
            EEventType evt,
            ESubsystem src, 
            ESubsystem dest = ESubsystem.Dispatcher
        )
        {
            EventType = evt;
            SourceSubsystem = src;
            DestSubsystem =  dest;

            EventId = Guid.NewGuid();
            Timestamp = DateTime.UtcNow;
        }

        // called to create a non-derived event for simple things; 
        // but keep other classes limited to calling specific factory
        // methods
        protected static EventOfHurt CreateGeneric(
            EEventType evt, ESubsystem src, 
            ESubsystem dest = ESubsystem.Dispatcher,
            Tuple<EventOfHurt, DateTime>[] reasons = null
        )
        {
            EventOfHurt RetVal;

            if (dest == null)
                dest = ESubsystem.Dispatcher;

            List<Tuple<EventOfHurt, DateTime>> ReasonList = 
                new List<Tuple<EventOfHurt,DateTime>>();

            if (reasons != null)
                ReasonList.AddRange(reasons);

            // the initializer after the constructor allows for a 
            // lot more flexibility than e.g., optional params
            RetVal = new EventOfHurt(evt, src) {
                myEventReferences = ReasonList
            };

            return RetVal;
        }

        // some of the specific methods can just return a generic
        // non-derived event
        public static EventOfHurt CreateTickerTimerEvent(
            EEventType evt, ESubsystem dest
        )
        {
            ESubsystem src = ESubsystem.TickerTimer;
            return CreateGeneric(evt, src, dest, null);
        }

        // some may return actual derived classes
        public static EventOfHurt CreatePlayerActionEvent(
            EEventType evt, ESubsystem dest,
            Tuple<EventOfHurt, DateTime>[] reasons
        )
        {
            PlayerEvent PE = new PlayerActionEvent(42);
            return PE;
        }
    }

    // could have some specific info relevant to player 
    // events in this class, world location, etc.
    public class PlayerEvent :
        EventOfHurt
    {
    };

    // and even further speciailzation here, weapon used
    // speed, etc. 
    public class PlayerActionEvent :
        PlayerEvent
    {
        public PlayerActionEvent(int ExtraInfo)
        {
        }
    };
}

namespace EntitiesOfHurt
{
    public class LatchedBool
    {
        private bool myValue = false;
        public bool Value
        {
            get { return myValue; }
            set {
                if (!myValue)
                    myValue = value;
            }
        }
    }

    public class EventOfHurtArgs :
        EventArgs
    {
        public EventOfHurtArgs(EventOfHurt evt)
        {
            myDispatchedEvent = evt;
        }

        private EventOfHurt myDispatchedEvent;
        public EventOfHurt DispatchedEvent
        {
            get { return myDispatchedEvent; }
        }
    }

    public class MultiDispatchEventArgs :
        EventOfHurtArgs
    {
        public MultiDispatchEventArgs(EventOfHurt evt) :
            base(evt)
        {
        }

        public LatchedBool isHandled; 
    }

    public interface IEventSink
    {
        // could do this via methods like this, or by attching to the
        // events in a source
        void MultiDispatchRecieve(object sender, MultiDispatchEventArgs e);
        void EventOfHurt(object sender, EventOfHurtArgs e);

        // to allow attaching an event source and notifying that
        // the events need to be hooked
        void AttachEventSource(IEventSource evtSource);
        void DetachEventSource(IEventSource evtSource);
    }

    // you could hook things up in your app so that most requests
    // go through the Dispatcher
    public interface IEventSource
    {
        // for IEventSinks to map
        event EventHandler<MultiDispatchEventArgs> onMultiDispatchEvent;
        event EventHandler<EventOfHurtArgs> onEventOfHurt;

        void FireEventOfHurt(EventOfHurt newEvent);
        void FireMultiDispatchEvent(EventOfHurt newEvent);

        // to allow attaching an event source and notifying that
        // the events need to be hooked
        void AttachEventSink(IEventSink evtSink);
        void DetachEventSink(IEventSink evtSink);
    }

    // to the extent that it works with your model, I think it likely
    // that you'll want to keep the event flow being mainly just
    // Dispatcher <---> Others and to minimize except where absolutely
    // necessary (e.g., performance) Others <---> Others.

    // DON'T FORGET THREAD SAFETY! :)
    public class Dispatcher : 
        IEventSource, IEventSink
    {
    }

    public class ProtocolDecoder :
        IEventSource
    {
    }

    public class ProtocolEncoder :
        IEventSink
    {
    }

    public class NetworkListener
    {
        // just have these as members, then you can have the
        // functionality of both on the listener, but the 
        // listener will not send or receive events, it will
        // focus on the sockets.

        private ProtocolEncoder myEncoder;
        private ProtocolDecoder myDecoder;
    }

    public class TheWorld :
        IEventSink, IEventSource
    {

    }

    public class Character
    {
    }

    public class NonPlayerCharacter :
        Character,
        IEventSource, IEventSink
    {
    }

    public class PlayerCharacter :
        Character,
        IEventSource, IEventSink
    {
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...