MonoGame 绘制函数不重绘

MonoGame draw function is not redrawing

我最近开始玩 monogame 并了解它是如何工作的,我想制作一个排序算法的可视化。

当我启动程序时,draw() 函数不会以当前状态重绘列。

它显示第一个状态,在循环结束时跳转到排序状态。

我是不是漏掉了什么?

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();

    if (Keyboard.GetState().IsKeyDown(Keys.Enter))
    {
        int temp;
        for (int j = 0; j <= MainArray.Length - 2; j++)
        {
            for (int i = 0; i <= MainArray.Length - 2; i++)
            {   
                if (MainArray[i] > MainArray[i + 1])
                {
                    temp = MainArray[i + 1];
                    MainArray[i + 1] = MainArray[i];
                    MainArray[i] = temp;

                    Draw(gameTime);
                    Thread.Sleep(300);
                }
            }
        }
    }
    
    // TODO: Add your update logic here

    base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);

    spriteBatch.Begin();
    for (int i = 0; i < MainArray.Length; i++)
    {
        sprite.Draw(new Vector2(i * 40 + 20, -10), spriteBatch, new Rectangle(i * 40 + 30, 0, 30, MainArray[i] * 8));
    }
    spriteBatch.End();

    // TODO: Add your drawing code here

    base.Draw(gameTime);
}

调用 Draw() 只会将您要求它绘制的内容添加到一种 “绘制缓冲区” 中,该缓冲区存储应绘制的内容。它不是直接显示屏幕上的像素。

在屏幕上显示像素的操作是由Monogame调用Update()后在内部完成的。在您的代码中,您正在用新的排序状态覆盖 "绘图缓冲区"。当需要在屏幕上显示像素时,Monogame 会获取 “绘图缓冲区” 上的任何内容并进行渲染。这就是为什么你只能看到最后的状态。

Monogame 游戏的内部循环大致如下所示:

public void Tick()
{
  (...)
  DoUpdate() // Calls "Update()"
  (...)
  DoDraw()   // Calls "Draw()"
             // Nested in some other methods, it also calls "Platform.Present()",
             // which is the place where pixels are displayed on the screen
  (...)
}

这是我从the source code of Monogame's Game.cs file中了解到的。

解决此问题的一种方法是 运行 另一个线程中的排序算法,并在需要时更新要显示的值,如下所示:

protected override void Initialize()
{
    latestArray = MainArray;

    sortingThread = new Thread(new ThreadStart(Sorting));
    sortingThread.IsBackground = true;
    sortingThread.Start();
}

private void Sorting()
{
    float temp;
    for (int j = 0; j <= MainArray.Length - 2; j++) {
        for (int i = 0; i <= MainArray.Length - 2; i++) {
            if (MainArray[i] > MainArray[i + 1]) {
                temp = MainArray[i + 1];
                MainArray[i + 1] = MainArray[i];
                MainArray[i] = temp;

                latestArray = MainArray;

                Thread.Sleep(10);
            }
        }
    }

    Console.WriteLine("Sorting done !");
}

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);
    spriteBatch.Begin();
    displayedArray = latestArray;
    for (int i = 0; i < displayedArray.Length; i++) {
        // Draw column
    }
    spriteBatch.End();
}

您的排序很好。

对于 MonoGame 和一般的游戏编程,您必须记住 Update() 和 Draw() 已经循环,并自动调用。

您应该永远不要在游戏 1 中自行调用 Update() 或 Draw()。由于 Draw() 实际上并未在屏幕上呈现任何内容,因此您正在浪费 CPU 个周期。

与其他编程范式不同,游戏循环(更新和绘制)无所不包,什么都不等待。虽然可以打破这条规则,但不应该在大多数情况下。


要将时间方面应用到传统循环,必须跨调用 Update() 来完成。

当应用于嵌套循环时,循环的顺序会反转,因为内部循环比外部循环运行“更快”,或者说更常见。

对循环实施了延迟机制(计时器),以减慢帧从默认的 60 fps(16.66667 毫秒)到合理且可见的(~2.9 fps)每帧 300 毫秒的变化。


你的代码有一个错误:冒泡排序在最坏的情况下需要 n * n 次迭代(从大到小排序),你的代码 运行s: n(n-1) 次, loop for j 在达到阈值之前终止一次迭代。我已经解决了这个问题。


以下代码使用这些输入约定:

  1. 在开始之前等待按下“Enter”。
  2. 随后的“输入”将 pause/resume 进度。
  3. 在最后停止,在下一个“Enter”重新开始

此代码还提供了提前退出条件,要求每次通过都进行更改(交换),否则退出。此代码使冒泡排序复杂度更接近 n log n.

额外需要 Game1 class 关卡变量,在以下行添加:

public class Game1:Game 
{

请注意,以下变量名称可能不符合某些命名准则。请相应调整。

private int j = 0, i = 0;  // move outside since they must carry across calls of Update()
private bool isRunning = false; //Indicate status, stop when paused or done.
private double timer = 0; //accumulator for time in ms
private int timeBetweenFrames = 300;  //in ms, when to run next loop iteration
private KeyboardState ks = new KeyboardState(), oks;  // needed for key press code

更新()

protected override void Update(GameTime gameTime)
{
    oks = ks;
    ks = Keyboard.GetState();
    if(ks.IsKeyDown(Keys.Escape) Exit(); 
    // new exit code, better due to the single Getstate() call per step/tick/iteration.

    if (ks.IsKeyDown(Keys.Enter) && oks.IsKeyUp(Keys.Enter)) // wait for an Enter press  
    //must be released then pressed before it fires again
    // without the additional check this will fire 60 times per second
    {
        timer = 0; 
        // i = 0; j = 0; //for reset instead of pause on enter
        isRunning = !isRunning;
    }
    if (isRunning)
    {
        timer += gameTime.ElapsedGameTime.TotalMilliseconds; 
               // should be 16.6(1/60) for the default 60 fps
        if (timer >= timeBetweenFrames) //wait for timer
        {
            timer=0; // reset timer and do a loop iteration
            //inner loop first, outer is incremented/checked in the else
           int temp=-1; // -1 to allow early exit
           if(++i <= MainArray.Length - 2)
           {
                // loop body
                if (MainArray[i] > MainArray[i + 1])
                {
                    temp = MainArray[i + 1];
                    MainArray[i + 1] = MainArray[i];
                    MainArray[i] = temp;
                }
            }
            else // outer increment and check
            {
                if(++j >= MainArray.Length || (i < MainArray.Length - 1 && temp == -1))
                // corrected bug and provide early exit
                // bug in worst case, reverse order source, needs n*n runs
                {
                    //Done
                    isRunning = false; // stop running
                    //reset for next run
                    i = 0;
                    j = 0;
                    timer=0;
                }
            }
        }
    }
    base.Update(gameTime);
}