如何为简单游戏设计一个解耦的类结构?

5

现在我有六个类:

  1. Listener - 管理套接字连接
  2. World - 实体和任务的集合
  3. Ticker - 协调更新世界
  4. MessageProcessor - 接收玩家命令
  5. Intelligence - 管理非玩家角色的行为
  6. Tasks - 跟踪和执行任务

但它们像一团乱麻,彼此相互引用... World是数据模型,由MessageProcessor、Intelligence和Tasks类修改。 Ticker协调这三个类更新世界。 Listener被MessageProcessor用于传入消息,并由其他类用于推送更新。

如何改善这种情况?


如果您不介意编辑您的问题并包含更多关于已经变得混乱的内容,那将会很有帮助。我认为我可能很难从段落中想象出来,因为对我来说它听起来并不那么复杂。例如,如果您可以谈谈对您有问题的任何事情,那将是一个很大的帮助。 - shelleybutterfly
你应该能够使用接口实现松耦合。正如@shelleybutterfly所说,如果你发布一些代码,我可以帮你重构一下。 - Jethro
2个回答

4

我不久前给出了一个相关答案。主题是关于改善代码的可测试性,通常的解决方案是松耦合。那个先前答案的重点是将与网络相关的代码与世界和逻辑分离开来,因为网络代码不能进行单元测试,而且模拟也很麻烦。

所提供的解决方案是使用接口来处理传入的消息,这样您就可以将MessageProcessor(在其他帖子中称为Handler)与网络代码解耦,同样地,将UpdateNotifier与World解耦。

enter image description here

虚线只是通过接口或委托处理的间接引用。现在,世界和网络组件之间不存在直接关系,使其可测试。这实际上只是模型视图适配器模式的一个应用。

M-V-A

这似乎与您所描述的设计并不相似,除非您缺少一些接口。使用基于接口的UpdateNotifiers推送更新的这种接口模式,我基本上重用了处理NPC、任务或任何其他在其他地方处理的相同架构。您可以选择特定区域需要的事件通知器,并为它们实现具体的Notifier类,从而在同一模型上拥有多个适配器。

enter image description here

这其实比看起来的要复杂。World对象没有直接依赖其他任何东西,每个类最多只有一个直接依赖。您还可以将计时器与World隔离开来,因为它可能在那里不需要 - 但也许最大的障碍之一是处理不同适配器之间的同步。


对于一个简单的模型,漂亮地呈现可以加一分。对于没有过度规范化UML图表的图示,可以点赞! :) 只需要盒子、线条和文本,这就足够了。(“为什么我必须在编写程序之前编写程序?”)this.mySoapboxes.ComeDown(sb => sb.Title=="UML considered <strike>harmful</strike> overkill"); - shelleybutterfly
我使用UML的唯一原因是因为它比MSPaint更容易绘制 :p - Mark H
哈哈 :) 不,那不是UML,那只是方框、文本和线条。这才是UML 哈哈 - shelleybutterfly

3

嗯,我不确定你遇到了什么问题的完整情况,但根据你目前所提供的一些信息,我有几个可能性。可能会重复建议一些已经完成的事情,因为我不确定从简略描述中是否足够了解全貌。

模型

从你的描述中,我认为最重要的是你需要开始使用类模型实现常见功能;你需要使用接口或基类从中派生出高级对象。

这样可以在很少额外工作的情况下以一致的方式处理事情。我认为“架构层次”的概念可以作为第一步考虑它的方式(例如,低级硬件、套接字处理等,然后是中间层的事情,例如游戏中发生的事情以及游戏机制背后的细节等,高级层次的事情,例如PC或NPC可以做什么,环境正在做什么等,并且永远不要“跳”层次)。但是,重要的是找到适合你的游戏的正确抽象方法,并始终保持组织良好,以便你永远不会感觉到你正在处理的代码正在做两种完全不同类型的事情。

首先,让我们看看自然而然地有很多事物与世界状态交互的事实。对于这样的问题,最好将大量的“东西”分解到一个类中,然后主要只让一个类来完成这项工作。理想情况下,在其自己的小组中实现事件通信/消息传递,这样就不需要用琐碎的处理方式污染您的高级对象。

例如,你想在高级对象中集中关注某些事物正在做什么:在AI中可能是“开始移动到位置”,“设置我的速度”,“停止移动”;在环境子系统中是“开始下雨”,“增加风速”,“调暗灯光”;在用户类中是“开火”,“睡眠”,“施法”。但我不希望我的任何高级类甚至知道诸如“向世界发送消息”,“重置口渴计时器”,“接收套接字数据”,“健康周期滴答声”之类的事情。(这些只是阐述,而非建议。;D)

事件

例如,我认为让一个对象负责向世界分派事件可能是有价值的,这样你就不再需要每个人都互相交流了。我可能会创建一组通用的处理事件的东西,例如 EventMain 和一个 enumEvents,你可以使用它来给每种类型的事件赋予特殊的 ID。然后将 Event 作为需要额外功能的特定事件的基类。(我考虑了 ID 和派生模型,这样像 Dispatcher 这样只需要知道事件的基本信息的东西就不必也要了解派生类的信息。例如,Dispatcher 可以接收事件并发送出去,而无需了解派生事件的内部信息。这可能有用,也可能没有用,但拥有这些选项很好。)你还可以拥有一个 EventDispatcher,它有一个事件队列,用于发送到其他子系统。
你需要为接收和发送事件创建一个共同的东西。你可以创建一个独立的 EventSourcerEventSinker 类,可以在任何生成或接收事件的类中设置它们。或者,你可以使用 IEventSourceIEventSink,这样你可以在多个类上实现一个公共接口,或者使用一个公共类 EventSourceAndSink 来实现两者,并将其作为你的基类模型的一部分,这样任何可能需要处理事件的东西都可以从中派生出来。
我可能会创建 ProtocolEncoderProtocolDecoder 类。你总是可以将它们合并为一个对象,但如果充分做好了,将它们作为两个独立的代码片段也可能是有价值的。你还可以拥有一个 ProtocolHelper 类,它将两者之间的任何共同点因素提取出来。编码器的唯一工作是从网络接收消息并将其转换为游戏事件,然后将其传递给 EventDispatcher。解码器类将从需要发送到网络的调度程序中获取事件,并获取其中的数据并将其发送出去。
如何到达目的地
既然你已经有了可用的代码,我建议你只需在自然的位置开始执行。这可能是使你陷入困境的事情,或者是你注意到在不同的地方非常相似的事情,你可以使用一个类或其他类型的抽象将其变得规律化,然后将旧的东西拿出来,放入新的东西。一旦你找到了一个可行的第一版类模型,那么应该会根据你已经拥有的想法给你带来更多的想法,随着你的前进不断重新考虑你的模型,修复问题,重复上述步骤。
不需要太多工作,事实上,我在编写代码时最令人满意的时刻之一就是当我能够进行整洁的重构,将以前丑陋混乱的代码变得更易于理解,并用更少的代码行数代替了难以理解的代码,为下一步的添加铺平了道路,使其成为一种愉悦而不是又一次“天啊,我不必再碰那个代码了吧?”的时刻。
祝好运,以下是我所说的事情的名义指南;第一部分更加详细,因为主要事件类是更重要的概念之一,然后我试图简要概述类和它们如何相互作用。我相信我可以花更多的时间来做这件事,但现在我只想说:如果你有问题,请问我,我会尽力给你一个好的答案:)

代码中的思想

哦,还有一件值得注意的事情是,如果你有多个线程,我没有处理增加的复杂性;如果你这样做,管理所有这些东西的事情从简单到复杂都有,但这是另一种练习。:)
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
    {
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接