10

我最近开始使用 Microsoft XNA 和 C# 开发一个小游戏以供自己娱乐。我的问题是关于设计游戏对象和继承它的对象。我将把游戏对象定义为可以在屏幕上渲染的东西。因此,为此,我决定创建一个基类,所有其他需要渲染的对象都将继承它,称为 GameObject。下面的代码是我做的类:

class GameObject
{
    private Model model = null;
    private float scale = 1f;
    private Vector3 position = Vector3.Zero;
    private Vector3 rotation = Vector3.Zero;
    private Vector3 velocity = Vector3.Zero;
    private bool alive = false;
    protected ContentManager content;

    #region Constructors
    public GameObject(ContentManager content, string modelResource)
    {
        this.content = content;
        model = content.Load<Model>(modelResource);
    }
    public GameObject(ContentManager content, string modelResource, bool alive)
        : this(content, modelResource)
    {
        this.alive = alive;
    }
    public GameObject(ContentManager content, string modelResource, bool alive, float scale)
        : this(content, modelResource, alive)
    {
        this.scale = scale;
    }
    public GameObject(ContentManager content, string modelResource, bool alive, float scale, Vector3 position)
        : this(content, modelResource, alive, scale)
    {
        this.position = position;
    }
    public GameObject(ContentManager content, string modelResource, bool alive, float scale, Vector3 position, Vector3 rotation)
        : this(content, modelResource, alive, scale, position)
    {
        this.rotation = rotation;
    }
    public GameObject(ContentManager content, string modelResource, bool alive, float scale, Vector3 position, Vector3 rotation, Vector3 velocity)
        : this(content, modelResource, alive, scale, position, rotation)
    {
        this.velocity = velocity;
    }
    #endregion
}

我省略了执行旋转、移动和绘制对象等操作的额外方法。现在如果我想创建另一个对象,比如船,我会创建一个 Ship 类,它会继承 GameObject。下面的示例代码:

class Ship : GameObject
{
    private int num_missiles = 20; // the number of missiles this ship can have alive at any given time
    private Missile[] missiles;
    private float max_missile_distance = 3000f; // max distance a missile can be from the ship before it dies

    #region Constructors
    public Ship(ContentManager content, string modelResource)
        : base(content, modelResource)
    {
        InitShip();
    }
    public Ship(ContentManager content, string modelResource , bool alive)
        : base(content, modelResource, alive)
    {
        InitShip();
    }
    public Ship(ContentManager content, string modelResource, bool alive, float scale)
        : base(content, modelResource, alive, scale)
    {
        InitShip();
    }
    public Ship(ContentManager content, string modelResource, bool alive, float scale, Vector3 position)
        : base(content, modelResource, alive, scale, position)
    {
        InitShip();
    }
    public Ship(ContentManager content, string modelResource, bool alive, float scale, Vector3 position, Vector3 rotation)
        : base(content, modelResource, alive, scale, position, rotation)
    {
        InitShip();
    }
    public Ship(ContentManager content, string modelResource, bool alive, float scale, Vector3 position, Vector3 rotation, Vector3 velocity)
        : base(content, modelResource, alive, scale, position, rotation, velocity)
    {
        InitShip();
    }
    #endregion
}

同样,我省略了任何额外的特定于舰船的方法,例如发射导弹。您认为这种设计是好的还是应该以某种方式改进或完全改变?似乎子类的构造函数很乱,但也许这是唯一的方法。我从来没有做过这样的事情,我想知道我是否偏离了轨道。


感谢所有留下答案的人。他们都非常有帮助。似乎有一个普遍的共识,那就是改变它以使用 MVC 模式是最好的。我将进一步研究如何做到这一点。我还将删除大部分构造函数,并且将只有一个构造函数,因为 modelResource 之后的所有参数都不是创建对象所必需的,并且它们都可以在以后通过方法调用进行更改。

4

11 回答 11

7

就我个人而言,我发现你的构造函数的数量有点令人反感,但这是你的选择,从根本上没有错 :)

关于从公共基础派生游戏对象的一般策略,这是一种非常常见的做事方式。标准,甚至。我倾向于开始使用更类似于 MVC 的东西,使用仅包含数据的非常轻量级的“模型”游戏对象。

我在 XNA 中看到的其他常见方法包括/您可能需要考虑的事情:

  • IRenderable 接口的实现,而不是在基类或派生类中执行任何渲染代码。例如,您可能想要从不渲染的游戏对象——航点或类似的东西。
  • 你可以让你的基类抽象,你可能不想实例化一个游戏对象。

不过,再一次,小问题。你的设计很好。

于 2008-10-07T20:13:44.257 回答
5

由于您问的是与设计相关的问题,因此我将尝试提供一些提示,而不是您发布的大量代码,因为其他人已经开始这样做了。

游戏非常适合模型-视图-控制器模式。您正在明确谈论显示部分,但请考虑一下:那里有向量等,这通常是您正在建模的一部分。将游戏世界和其中的对象视为模型。他们有一个位置,他们可能有速度和方向。这就是 MVC 的模型部分。

我注意到你希望让船着火。您可能希望机械师在控制器类中使飞船开火,例如需要键盘输入的东西。这个控制器类将向模型部分发送消息。您可能想要的一个合理且简单的想法是在模型上使用一些 fireAction() 方法。在您的基本模型上,这可能是一个可覆盖的(虚拟的、抽象的)方法。在船上,它可能会为游戏世界增添一颗“子弹”。

看到我已经写了这么多关于模型行为的文章,还有另一件事对我有很大帮助:使用策略模式使类的行为可交换。如果您认为 AI 并希望在未来某个时候改变行为,那么策略模式也很棒。您现在可能不知道,但一个月后您可能希望将弹道导弹作为默认导弹,如果您使用了策略模式,以后可以轻松更改它。

所以最终你可能想要真正做一个显示类之类的东西。它将具有您命名的原语:旋转、平移等。并且您想从中派生以添加更多功能或更改功能。但是想一想,一旦你拥有了不止一艘船,一种特殊的船,另一种特殊的船,你会遇到推导地狱,你会复制很多代码。再次,使用策略模式。记住要保持简单。Displayble 类需要具备什么?

  • 意味着知道它相对于屏幕的位置。它可以,这要归功于它的世界对象模型和类似于游戏世界模型的东西!
  • 它必须知道要为其模型及其尺寸显示的位图。
  • 它必须知道是否有任何东西应该阻止它绘制,如果绘制引擎没有处理(即另一个对象遮挡它)。
于 2008-10-07T22:14:04.217 回答
5

不仅构造函数的数量是一个潜在的问题(尤其是必须为每个派生类重新定义它们),而且构造函数的参数数量也可能成为一个问题。即使您使可选参数可以为空,以后也很难阅读和维护代码。

如果我写:

new Ship(content, "resource", true, null, null, null, null);

第二个NULL有什么作用?

如果参数列表超过四个或五个参数,使用结构来保存参数会使代码更具可读性(但更冗长):

GameObjectParams params(content, "resource");
params.IsAlive = true;
new Ship(params);

有很多方法可以做到这一点。

于 2008-10-07T21:47:29.933 回答
4

这种设计没有分离对象的 UI 部分和对象的行为部分,可以说是“模型”和“视图”。这不一定是坏事,但如果继续这种设计,您可能会发现以下重构很困难:

  • 通过更改所有艺术资产重新设计游戏
  • 更改许多不同的游戏内对象的行为,例如确定对象是否存在的规则
  • 更改为不同的精灵引擎。

但是,如果您对这些折衷感到满意并且此设计对您有意义,那么我认为它没有什么严重的错误。

于 2008-10-07T20:05:53.383 回答
3

我对您在矢量中旋转的存储方法非常感兴趣。这是如何运作的?如果你存储 X、Y、Z 轴的角度(所谓的欧拉角),你应该重新考虑这个想法,如果你想创建一个 3D 游戏,因为你会在第一次渲染邪恶的GIMBAL LOCK后不久遇到

就我个人而言,我不喜欢那里有那么多构造函数,因为您可以提供 1 个构造函数并将所有不需要的参数设置为 null。这样,您的界面就会保持清洁。

于 2008-10-07T20:20:19.063 回答
2

I also heavily recommend looking into a component based design using composition rather than inheritance. The gist of the idea is to model object based on their underlying properties, and allow the properties to do all of the work for you.

In your case, a GameObject may have properties like being able to move, being renderable, or having some state. These seperate functions (or behaviors) can be modeled as objects and composed in the GameObject to build the functionality you desire.

Honestly though, for a small project you're working on I would focus on making the game functional rather than worry about details like coupling and abstraction.

于 2008-10-07T21:14:02.280 回答
1

The people here talking about separating Model from View are correct. To put it in more game developer oriented terms, you should have separate classes for GameObject and RenderObject. Think back to your main game loop:

// main loop
while (true) {
    ProcessInput();  // handle input events
    UpdateGameWorld();  // update game objects
    RenderFrame();  // each render object draws itself
}

You want the process of updating your game world and rendering your current frame to be as separate as possible.

于 2008-10-07T20:56:35.653 回答
1

Alternatively you can make a struct or helper class to hold all the data and have the many constructors in the struct, that way you only have to have tons constructors in 1 object rather than in every class that inherits from GameObject

于 2008-10-07T21:14:40.047 回答
1

根据调用构造函数的频率,您可以考虑将所有额外参数设为可为空,并在它们没有值时将 null 传递给它们。Messier 创建对象,更少的构造函数

于 2008-10-07T20:14:07.440 回答
1

我第一次在 XNA 中创建游戏时走这条路,但下一次我会做更多的模型/视图类型的方法。Model 对象将是您在 Update 循环中处理的内容,而您的 Views 将在您的 Draw 循环中使用。

当我想使用我的精灵对象来处理 3D 和 2D 对象时,我遇到了不将模型与视图分离的麻烦。它很快就变得非常混乱。

于 2008-10-07T20:18:57.040 回答
1

第一场比赛?开始简单。您的基础游戏对象(在您的情况下更恰当地称为 DrawableGameObject [注意 XNA 中的 DrawableGameComponent 类])应该只包含属于所有游戏对象的信息。您的 live 和 velocity 变量并不真正属于。正如其他人所提到的,您可能不想混合绘图和更新逻辑。

Velocity 应该在 Mobile 类中,该类应该处理更新逻辑以更改 GameObject 的位置。您可能还需要考虑跟踪加速度,并在玩家按住按钮时稳定增加,并在玩家释放按钮后稳定减少(而不是使用恒定加速度)......但这更多是游戏设计决定而不是班级组织决定。

“活着”的含义可能因上下文而异。在一个简单的太空射击游戏中,不存在的东西可能应该被摧毁(可能用几块太空碎片代替?)。如果是重生的玩家,您需要确保玩家与飞船分开。船没有重生,船被摧毁,玩家得到一艘新船(在内存中回收旧船而不是删除它并创建新船将使用更少的周期 - 你可以简单地将船移到无人船- 完全着陆或将其从游戏场景中拉出并在重生后将其放回)。

另一件你应该注意的事情......不要让你的玩家在你的垂直轴上直接向上或向下看,否则你会遇到,正如彼得帕克所说,万向节锁定问题(你不想搞砸如果您是为了好玩而进行游戏开发!)。更好的是……从 2D 开始(或者至少,将运动限制在 2 维)!当您不熟悉游戏开发时,这会容易得多。

不过,我建议在进行更多编程之前遵循XNA Creators Club 入门指南。您还应该考虑寻找一些其他更通用的游戏开发指南,以获得一些替代建议。

于 2008-10-07T21:51:49.690 回答