开发游戏 - 如何处理需要多个游戏循环的事情?

3
我正在使用C#,但这适用于使用任何语言开发游戏。
大多数游戏使用“游戏循环”,它看起来可能是这样的:
while (game is running)
{
  UpdateEnvironment();
  DrawEnvironment();
}

我很难理解一些需要多个游戏循环才能完成的事情如何适应公式。例如,让一个能量球从一个方块飘到另一个方块;或者让玩家移动一个方块(不是从一个方块跳到另一个方块,而是通过动画实现)。
我想到的最好的方法是获取自上次循环以来经过的时间,并将其传递给对象/方法,以便它可以完成它的功能。但这使得完成以下任务变得困难:
AI.MoveTo(10, 20); // Pathfind, then walk the path to this tile.
Player.Shoot(); // Shoot a bullet, and detect collisions and update along the way.

我在哪里可以找到关于“执行需要多个游戏循环才能完成的事件”的更多信息?

请查看http://gafferongames.com/game-physics/fix-your-timestep/。 - WW.
5个回答

1

实际上,你所说的使用经过时间是相当准确的。如果你熟悉C#,并且还没有这样做,我强烈建议你研究一下XNA或者如果你愿意花一点钱,可以考虑"TheGameCreators"的Dark GDK .NET。

无论如何,每个游戏循环中的想法是,使用自上次更新以来经过的时间更新任何“活动”对象。“活动”对象是任何仍然被认为是活动的并需要更新的东西(例如敌人、玩家、正在飞行中的子弹、仍在爆炸的爆炸等)。你根据经过的时间、碰撞、火灾造成的损坏等确定每个对象应该具有的下一个状态、位置、健康状况等,然后实现该下一个状态。最后,你调用每个对象的绘制过程,并以它们的新状态呈现它们。

对于像玩家射击这样的事情,你可以这样做。注意,这更多的是伪代码而不是实际代码。希望它能给你一个想法。

//Generic Game Class
public class MySweetGame : Game
{
    Player _player = new Player(this);
    List<Bullet> _bullets = new List<Bullet>();
    public List<Bullet> AllBullets()
    {
        get { return _bullets; }
    }


    public void Update()
    {
        //You would obviously need some keyboard/mouse class to determine if a click took place
        if(leftMouseButtonClicked)
        {
            _player.Shoot();
        }

        //This would be assuming you have an object collection or array to hold all the bullets currently 'live' like the above '_bullets' collection
        //This is also assuming elapseGameTime is some built in game time management, like in XNA
        foreach(Bullet blt in _bullets)
        {
            blt.Update(elapsedGameTime);
        }
    }
}

//Generic Player Class
public class Player()
{
    Vector2 _position = new Vector2(0,0);
    int _ammunition = 50;
    MySweetGame _game;

    public Player(MySweetGame game)
    {
        _game = game;
    }

    public void Shoot()
    {
        if(_ammunition > 0){
            _game.AllBullets.Add(new Bullet(50, _position));
            _ammunition--;
        }
    }
}

//Generic Bullet Class
public class Bullet()
{
    float _metersPerSecond = 0;
    Vector2 _position = new Vector2(0, 0);

    public Bullet(float metersPerSecond, Vector3 position)
    {
        _metersPerSecond = metersPerSecond;
        _position = position;
    }

    //Here is the meat you wanted
    //We know the speed of the bullet, based on metersPerSecond - which we set when we instantiated this object
    //We also know the elapsedGameTime since we last called update
    //So if only .25 seconds have passed - we only moved (50 * .25) - and then update our position vector
    public void Update(float elapsedGameTime)
    {
        distanceTraveled = metersPerSecond * elapsedGameTime;
        _position.x += distanceTraveled;
    }
}

1

您可能不会使用事件;相反,MoveTo或Shoot应该被视为状态的变化。您的AI对象需要一个由此类变量组成的状态:

class AI
{
   StateEnum State; //Idle, Moving, Attacking, Dying, etc.
   PointF Location;
   PointF Velocity;
   PointF Destination;

在您的MoveTo方法中,您需要设置对象的状态--类似以下内容:
   void MoveTo(x, y)
   {
      Destination = new PointF(x, y);
      Velocity = new PointF(0.5, 0);
      State = StateEnum.Moving;
   }

在其Update方法中,您将更新位置。
   void Update()
   {
      switch (State)
      {
         case StateEnum.Moving:
            Location.Offset(Velocity); //advance 0.5 pixels to the right
            break;
         default:
            break;
      }
   }
}

该方法将基于某个计时器(例如每秒60个刻度)从游戏循环中调用,因此实际上,对象每秒移动30个像素。如果它有动画帧,则只需随着刻度倒数并根据需要更改帧。

至于寻路,要从一个瓷砖移动到另一个瓷砖,您可以在每个瓷砖上更新速度,以便对象朝着所需方向移动。


1
除了WesleyJohnson和Gannon的解决方案之外,您还可以使用基于任务的方法。 WesleyJohnson和Gannon的解决方案具有较少的复杂性,这是一件好事,特别是当游戏角色的行为是静态定义时。例如简单的射击游戏。但是,当您希望通过脚本动态定义行为或者当您的角色具有复杂的行为时,您可能希望将行为管理外部化。 否则,您的演员更新函数必须具有复杂的状态管理。
常见的方法是拥有一个名为Task(或Process或Job)的基类,以及特定的长时间运行的任务子类Job。例如,您可以拥有MoveActorTask、PlayAnimationTask等。通过结果代码和标记是否完成,您还可以链接任务,以便它们按顺序执行,等待前面的任务完成,使用composite tasks
以下是我们使用的内容,稍作编辑以便更好地阅读,并剥离了一些可能会混淆的高级选项:
class Task
{
public:

    /**
     * Constructor.
     *
     *  @param isDiscardable Set this true, if the Task's goal can be reached in a single step.
     *         For instance if a Task is supposed to slowly close a window by fading
     *         its alpha to 0, then it is discardable, and Task#discard will just finish
     *         the process by closing the window.
     *
     *  @param destroyWhenDone Set this to true, when the TaskScheduler shall delete the
     *         Task, after execution is finished. This should usually be the case, but
     *         sometimes it is sensible to pool a number of Jobs for reuse.
     */
    Task(bool isDiscardable, 
        bool destroyWhenDone);

    virtual ~Task();

    /**
     * This is the function in which the Task is supposed to do whatever it is supposed to do.
     * This function is called by the TaskScheduler at most once per frame. The frequency depends
     * on the Job's priority given with TaskScheduler#addTask.
     * @param time the time source time, since the last call of this function.
     * @return true, when the Task is done, false else. If false is returned, the Task will be
     * rescheduled for another execution.
     */
    virtual bool execute(Time time) = 0;

    virtual TimeSource::TimeSourceType getTimeSource() const = 0;

    /// Returns whether the Task can be removed from the queue by the scheduler,
    bool isDiscardable() const;

    /// Returns true, if the Task shall be deleted, if the Job is finished. Returns false else.
    bool destroyWhenDone() const;

    /// Finish whatever the Task is doing. It won't get a chance to continue.
    /// Overloaded functions must *not* call this implementation.
    virtual void discard();

protected:
    bool mIsDiscardable;
    bool mDestroyWhenDone;
};

任务由一个TaskScheduler管理。每一帧,TaskScheduler调用所有任务的execute函数(轮流调度),或者你可以采用不同的调度策略。

1

一种方法是存储完整的待处理操作,然后让游戏循环仅执行其中一小部分。从操作中引用的游戏对象知道它们所处的状态,因此下一个要执行的部分是已知的。只要还有剩余的任务,就将该操作添加回待处理操作队列中,在下一个循环中执行,并且当操作完成时,不再将其添加回队列。

因此,在您的MoveTo示例中,要存储的操作是移动到10,20,每次跨越游戏循环时,AI向其移动一点。您的Shoot示例可能更好地描述为子弹沿着某个方向行进,然后它击中的任何物体都决定了是否继续执行该操作。

我没有做过游戏开发,因此不知道在该领域中是否是这样做的,但这是我在事件驱动系统中处理类似问题的方式。


-2

考虑操作系统如何允许多个程序在单个处理器上运行:

  • 程序1正在运行
  • 程序1被中断
  • 内核保存程序1的状态(CPU寄存器等内容)
  • 内核加载程序2的状态
  • 程序2恢复

这种“中断/保存/恢复/恢复”方法是针对那些难以分解成部分的任务的“最坏情况”选项。在某个时刻(也许基于任务运行的时间长度),您可以保存任务所需的所有变量,并停止运行代码。稍后,您可以恢复状态并恢复代码。

然而,通常可以通过设计系统来减少需要采取类似方法的需求。例如,设计动画使其可以逐帧处理。


这听起来像是一个非常糟糕的想法。对于如此简单的事情,例如每帧增加动画,抢占式多任务处理过于复杂。 - Mike Cooper
我只是用它作为解释的方式。对于像路径寻找这样的事情,如果计算时间过长,你可能希望中断数字计算,一种合作性的让步吧。 - Artelius

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