【问题标题】:Architecture for Game - To couple or not to couple?游戏架构——耦合还是不耦合?
【发布时间】:2009-08-20 19:01:59
【问题描述】:

我正在构建一个由手机组成的简单游戏——游戏中的角色 (Mobs)。每个暴徒都可以执行某些功能。为了向 Mob 提供该功能,我创建了一个 Behavior。

例如,假设一个暴徒需要在游戏场中移动,我会给它 MoveBehavior - 这将添加到暴徒类的内部行为列表中:

// Defined in the Mob class
List<Behavior> behaviors;

// Later on, add behavior...
movingMob.addBehavior(new MovingBehavior());

我的问题是这样的。大多数行为都会操纵有关暴民的某些东西。在 MoveBehavior 示例中,它将改变生物在世界中的 X、Y 位置。但是,每个行为都需要特定的信息,例如“movementRate”——movementRate 应该存储在哪里?

它应该存储在 Mob 类中吗?其他生物可能会尝试通过减慢/加速生物与它进行交互,并且在生物级别更容易访问......但并非所有生物都有移动率,因此会导致混乱。

还是应该将其存储在 MoveBehavior 类中?这将其隐藏起来,使其他生物更难与之交互 - 但它不会用额外和未使用的属性使不动的生物混乱(例如,不会移动的塔永远不需要使用运动速率)。

【问题讨论】:

  • 我想知道使用脚本语言(IronRuby、IronPython、F# 等)来处理游戏中所需的元数据是否是个好主意。

标签: c# architecture


【解决方案1】:

这是经典的“行为组合”问题。权衡是行为越独立,它们之间的交互就越困难。

从游戏编程的角度来看,最简单的解决方案是确定一组“核心”或“引擎”数据,并将其放入主对象中,然后让行为能够访问和修改该数据 - 可能通过功能接口。

如果您想要特定于行为的数据,那很好,但为了避免变量名称冲突,您可能希望使访问它的接口包含行为名称。喜欢:

obj.getBehaviorValue("movement", "speed")

obj.setBehaviorValue("movement", "speed", 4)

这样,两个行为都可以定义自己的变量,称为速度,而不会发生冲突。这种类型的交叉行为 getter 和 setter 允许在需要时进行通信。

我建议为此使用 Lua 或 Python 之类的脚本语言。

【讨论】:

    【解决方案2】:

    您可以从 WPF(附加属性)中借用一个模式。 WPF 人员需要一种在运行时将属性附加到控件的方法。 (例如,如果您将一个控件放在网格中,那么该控件最好有一个 Row 属性——他们伪使用附加属性来做到这一点。

    它的工作原理类似于:(请注意,这可能与 WPF 的实现不完全匹配,并且我省略了依赖项属性注册,因为您没有使用 XAML)

    public class MoveBehavior: Behavior { private static Dictionary<Mob, int> MovementRateProperty; public static void SetMovementRate(Mob theMob, int theRate) { MovementRateProperty[theMob] = theRate; } public static int GetMovementRate(Mob theMob) { // note, you will need handling for cases where it doesn't exist, etc return MovementRateProperty[theMob]; } }

    这里的问题是 Behavior 拥有该属性,但您不必去探索就可以得到它 这是一些检索暴徒移动率的代码:

    // retrieve the rate for a given mob int rate = MoveBehavior.GetMovementRate(theMob); // set the rate for a given mob MoveBehavior.SetMovementRate(mob, 5);

    【讨论】:

      【解决方案3】:

      如果它与行为相关,并且只在该行为的上下文中有意义,那么它应该作为它的一部分存储。

      移动速率只对可以移动的东西有意义。这意味着它应该存储为代表其移动能力的对象的一部分,这似乎是您的 MoveBehavior。

      如果这使其难以访问,那听起来更像是您的设计存在问题。那么问题就不是“我是否应该作弊,将一些变量放在 Mob 类中而不是它所属的行为”,而是“我如何让与这些个体行为的交互变得更容易”。

      我可以想出几种方法来实现这一点。显而易见的是 Mob 类上的一个简单成员函数,它允许您选择个人行为,如下所示:

      class Mob {
        private List<Behavior> behaviors;
        public T Get<T>(); // try to find the requested behavior type, and return it if it exists
      }
      

      然后其他人可以这样做:

      Mob m;
      MovementBehavior b = m.Get<MovementBehavior();
      if (b != null) {
        b.SetMovementRate(1.20f);
      }
      

      您也可以将其中的一些放在 Mob 类之外,创建一个辅助函数来修改移动速率(如果存在),否则不执行任何操作:

      static class MovementHelper {
        public static SetMovementRate(Mob m, float movementrate){
          MovementBehavior b = m.Get<MovementBehavior();
          if (b != null) {
            b.SetMovementRate(1.20f);
          }
        }
      }
      

      然后其他人可以像这样使用它:

      MovementHelper.SetMovementRate(m, 1.20f);
      

      这将提供修改行为的轻松访问,但不会弄乱 Mob 类。 (当然,将其转换为扩展方法会很诱人,但这可能会导致 Mob 类的公共接口中出现太多杂乱无章的情况。最好明确说明这是驻留在外部的辅助功能Mob 类本身)

      【讨论】:

      • 我和你在一起 jalf - 我觉得它应该是 MoveBehavior 的一部分。如果由于针对暴民的某些外部行动而需要更改速率,我该如何实现一种简单的交互方式?
      • 这在很大程度上取决于您设计的其余部分。简单的方法是给 Mob 类一个简单的 Get 函数,它允许您从中检索任何单个行为。如果我想改变 Mob m 的移动行为,我会调用 m.Get(),如果它不返回 null,我就有了我想要处理的行为。这将解决这个特定问题,但可能还有其他复杂性需要考虑。例如,如果同一生物上的两种不同行为需要共享数据,你会怎么做?假设每种行为都存在于真空中可能是不现实的。
      【解决方案4】:

      看看组件系统/实体系统设计:

      http://www.devmaster.net/articles/oo-game-design/

      到目前为止我见过的最好的。

      聪明的人说这是处理大型游戏的唯一方法,但它需要您转变对 OOP 的看法。

      【讨论】:

        【解决方案5】:

        那你想做什么?

        您存储运动速率数据的最简单方法是什么?

        如果只在 MoveBehavior 类中需要它,那么它应该在那里:

        public class MoveBehavior {
            public int MovementRate { get; set; }
        }
        

        如果 Mob 类本身需要它,那么通过 Mob 类将更容易公开:

        public class Mob {
            public int MovementRate { get; set; }
        }
        
        public class MoveBehavior {
            public MoveBehavior(Mob mob) { MobInEffect = mob; }
        
            public Mob MobInEffect {get; set;}
        
            // can access MovementRate through MovInEffect.MovementRate
        }
        

        因此,这完全取决于您尝试使用此行为逻辑实现的目标。我建议您推动设计决策,直到您真的需要以一种或另一种方式去做。先集中精力做简单,然后再重构。通常情况下,进行早期设计猜测会导致架构过于复杂。


        更务实的解决方案……

        我的意思是你首先在 Mob 类中实现你想要的移动:

        public class Mob {
        
            // Constructors and stuff here
        
            public void Move(long ticks) 
            {
                // do some voodoo magic with movement and MovementRate here
            }
        
            protected int MovementRate { get; set; }
        }
        

        当它起作用时,如果你真的需要,将那个实现撕掉到 MoveBehavior 类:

        public class Mob {
        
            // Constructors and stuff here
        
            public MoveBehavior Moving { set; get; }
        
            public void Move(long ticks) 
            {
                Moving.Move(ticks, this);
            }
        }
        
        public class MoveBehavior {
            protected int MovementRate { get; set; }
        
            public void Move(long ticks, Mob mob)
            {
                // logic moved over here now
            }
        }
        

        之后,如果您确实需要执行不止一种类型的行为,但它们共享一个公共接口,那么到那时创建该接口并让行为实现该接口。

        【讨论】:

          【解决方案6】:

          编辑:下面的答案只有在你没有为每个暴徒实例化一个新的 MovingBehavior 而是只有一个单独的 MovingBehavior 时才真正有意义。

          我会说暴民(呃,我讨厌游戏 NPC 的这个词,但这不是你的错)应该在调用 addBehavior() 时获得一个从 addBehavior() 返回的 BehaviorState 对象,并且它一直存在,并且与添加的行为有关。然后为MovingBehavior 提供一个接口,以便从movingMob 轻松检索其BehaviorState 对象,并将其存储在那里需要存储的任何内容。

          【讨论】:

          • 所以您建议 BehaviorState 负责存储 moveRate 并将状态存储在 Mob 对象中?一个拥有 4-5 种行为的暴徒会各自拥有自己的 BehaviorState,还是一个状态对象存储所有可能行为的所有可能数据?
          • 1) 对。 2)我认为每个添加的行为都会有自己的状态跟踪,以便可以根据行为的需要设计所涉及的数据结构。如果它只是一个状态对象,不妨给暴徒本身一些通用的状态跟踪功能,并让行为使用它。 (这也可以正常工作。)
          • sean riley 的回答几乎就是我所说的后一种选择。
          【解决方案7】:

          如果我要设计这样的东西,我会尝试使用接口来定义暴民的行为:

          public interface IMovable
          {
              int MovementRate { get; set; }
              void MoveTo(int x, int y);
          }
          
          public class Monster : Mob, IMovable
          {
              public int MovementRate { get; set; }
          
              public void MoveTo(int x, int y)
              {
                   // ...
              }
          }
          

          通过这种方式,您可以通过执行以下操作来检查生物是否可以移动:

          Monster m = new Monster();
          if (m is IMovable)
          {
              m.MoveTo(someX, someY);
          }
          

          【讨论】:

            【解决方案8】:

            恕我直言,移动速度与移动行为相关,而不是与暴民本身相关,正如你所说,它不一定移动。所以变量应该和行为相关联,movementRate的变化是他的Behavior的变化,而不是暴徒本身的变化。

            您还可以创建一个基础 Mob 类,并派生一个 MovingMob 类。但我想,这并不适用,一旦显然你可以任意组合不同的行为......

            -- 编辑--

            首先,显然你不会在同一个 Mob 中出现两次相同类型的行为(例如,没有一个 mob 具有相同类型的两个运动行为),因此在这种情况下,集合是更好的选择,因为它可以避免重复

            你可以在每个生物中都有一个方法,比如

                public Behavior GetBehavior(Type type)
                {
                    foreach (var behavior in behaviorHashSet)
                    {
                        if ( behavior.GetType() == type)
                            return behavior;
            
                    }
                    return null;
                }
            

            然后,一旦你有了一个 Mob,你就可以对这个行为做任何你想做的事情。此外,您可以更改 GetHashCode() 和 Equals() 方法以确保没有重复,或者使 GetBehavior 方法更快(恒定时间)

            【讨论】:

            • 所以在我有另一个暴徒的情况下,比如说一个让周围的所有暴徒移动得更快的暴徒——那个暴徒将如何操纵行为的移动速度?暴民知道其他暴民,但不知道其他暴民的行为(另外,循环遍历行为列表以查找和更改速率似乎过于复杂)。
            • 然后创建一种更简单的方法来访问个人行为。这种情况下的问题是你的底层设计。也许行为列表不应该是一个简单的列表。或者它应该是一个 List 加上一堆辅助函数(例如,一个通用的 Get() 成员函数)。或者它应该是字典而不是列表。或者,每个 Mob 类都有一堆独立行为的基本思想是有缺陷的,必须改变。为了影响他人的运动行为,您必须了解他们的运动行为是有道理的,不是吗?
            猜你喜欢
            • 2017-10-12
            • 2014-04-10
            • 2011-02-01
            • 1970-01-01
            • 1970-01-01
            • 2017-05-09
            • 2018-12-09
            • 1970-01-01
            • 2022-11-25
            相关资源
            最近更新 更多