游戏架构 - 耦合还是非耦合?

10

我正在制作一个简单的游戏,其中包含移动的游戏角色(Mobs)。每个Mob都可以执行某些功能。为了赋予Mob这种功能,我创建了一个行为(Behavior)。

比如说,如果一个Mob需要在游戏场地中移动,我会给它MoveBehavior——这会被添加到Mob类的内部行为列表中:

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

// Later on, add behavior...
movingMob.addBehavior(new MovingBehavior());
我的问题是,大多数行为将操作有关生物的某些内容。在MoveBehavior示例中,它将更改生物在世界中的X,Y位置。但是,每个行为都需要特定的信息,例如“movementRate”,那么应该将movementRate存储在哪里?
它应该被存储在Mob类中吗?其他Mob可能会尝试通过减速/加速来与其交互,而在Mob级别上更容易访问... 但并非所有Mobs都具有movementRate,因此它会引起杂乱。
还是应该将其存储在MoveBehavior类中?这样做可以将其隐藏起来,使其他Mobs更难以进行交互 - 但它不会用额外和未使用的属性来弄乱不动的Mob(例如,不移动的塔永远不需要使用movementRate)。

我在想是否使用脚本语言(IronRuby、IronPython、F#等)来完成游戏中所需的元数据是一个好主意。 - CookieOfFortune
8个回答

4
这是经典的“行为组合”问题。权衡之处在于,行为越独立,它们相互交互就越困难。
从游戏编程的角度来看,最简单的解决方案是确定一组“核心”或“引擎”数据,并将其放入主对象中,然后使行为能够访问和修改该数据-可能通过一个功能接口。
如果您需要行为特定的数据,那很好,但为了避免变量名称冲突,您可能希望使访问它的接口包括行为名称。例如:
obj.getBehaviorValue("movement", "speed")

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

这样,两种行为都可以定义自己的名为“speed”的变量而不会冲突。这种跨行为的getter和setter在需要通讯时允许通讯。

我建议使用像Lua或Python这样的脚本语言来实现。


2
如果涉及到某种行为,并且只有在该行为的上下文中才有意义,那么它应该作为该行为的一部分存储。
移动速度只对可以移动的东西有意义。这意味着它应该作为代表其移动能力的对象的一部分进行存储,这似乎是您的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的一部分。你有什么想法,如何能够轻松地与速率进行交互,以便在某些外部动作对移动对象产生影响时,速率需要更改? - bugfixr
这在很大程度上取决于您设计的其余部分。简单的方法是给Mob类一个简单的Get函数,允许您从中检索任何单个行为。如果我想要更改Mob m的移动行为,我会调用m.Get<MovementBehavior>(),如果它不返回null,则有我想要处理的行为。这将解决此特定问题,但可能需要考虑其他复杂性。例如,如果同一mob上的两种不同行为需要共享数据,那么您会怎么做?假设每个行为都存在于真空中可能并不现实。 - jalf

2

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

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

迄今为止,我看到的最好的设计。

聪明人说这是处理大型游戏的唯一方法,但它需要转变你对面向对象编程的思考方式。


2
你可以借鉴WPF的模式(附加属性)。WPF的开发者需要一种在运行时将属性附加到控件的方法。(例如,如果你将一个控件放在网格中,那么这个控件就应该有一个"行"属性 -- 他们用附加属性伪实现了这个功能。)
它的工作原理大概是这样的: (请注意,这可能不完全符合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) { //注意,你需要处理不存在等情况 return MovementRateProperty[theMob]; } }
这里的关键是行为拥有属性,但你不必深入其中。下面是一些检索mob移动速率的代码:

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

0

在我看来,移动速率与移动行为相关联,而不是与Mob本身相关联,正如你所说,它不一定会移动。因此,变量应该与行为相关联,移动速率的变化是对其行为的改变,而不是对Mob本身的改变。

您还可以创建一个基本的Mob类,并派生出一个MovingMob类。但我想,这并不适用于所有情况,因为显然您可以拥有任意组合的不同行为...

-- 编辑 --

首先,显然您不会在同一种Mob中两次使用相同类型的行为(例如,没有Mob同时具有两个movementBehaviors),因此在这种情况下,集合是更好的选择,因为它避免了重复。

您可以在每个Mob中都有一个方法,例如

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

        }
        return null;
    }

一旦你有了一个Mob,你就可以随心所欲地使用这个行为。此外,你还可以更改GetHashCode()和Equals()方法以确保没有重复项,或者使GetBehavior方法更快(常数时间)。


那么,假设我有另一个怪物,比如让周围所有怪物移动速度加快的怪物 - 那么该怪物如何操纵行为的移动速率?该怪物知道其他怪物的存在,但不知道其他怪物的行为(此外,循环遍历行为列表以查找并更改速率似乎过于复杂)。 - bugfixr
然后创建一种更简单的访问个体行为的方式。问题在这种情况下是你的基础设计。也许行为列表不应该是一个简单的列表,或者它应该是一个列表加上一堆辅助函数(比如说一个通用的Get<BehaviorType>()成员函数),或者它应该是一个字典而不是一个列表。或者,每个Mob类都有一堆独立的行为这个基本想法是有缺陷的,必须改变。为了影响其他人的移动行为,了解他们的移动行为是有意义的,不是吗? - jalf

0

那么你想做什么?

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

如果只需要在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
    }
}

如果您确实需要执行多种类型的行为,但它们共享一个公共接口,则可以创建该接口并让这些行为实现它。

0
如果我要设计类似的东西,我会尝试使用接口来定义怪物具有哪些行为:
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);
}

0

编辑:以下答案只有在你不为每个移动的怪物实例化一个新的MovingBehavior时才真正有意义,而是只有一个单例的MovingBehavior。

我想说的是,当调用addBehavior()时,应该给怪物(啊,我讨厌这个游戏NPC的词汇,但这不是你的错)返回一个BehaviorState对象,并将其保存下来,与添加的行为相关联。然后提供一个接口,使MovingBehavior可以轻松地从movingMob中检索其BehaviorState对象,并在那里存储它所需的任何信息。


那您的建议是让 BehaviorState 负责存储 moveRate 并将状态存储在 Mob 对象中?如果一个 Mob 拥有 4-5 个 Behaviors,它们会各自拥有自己的 BehaviorState 还是一个单一的状态对象会存储所有可能的数据以支持所有可能的 behaviors? - bugfixr
  1. 好的。
  2. 我在想每个添加的行为都应该有自己的状态跟踪,这样涉及到的数据结构就可以根据行为的需求进行设计。如果只是一个状态对象,那么还不如给怪物本身一些通用的状态跟踪功能,让行为使用它。(这也可能很好用。)
- chaos
Sean Riley的回答基本上就是我所说的后一种选择。 - chaos

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