【问题标题】:C# Winform OnPaint flickeringC# Winform OnPaint 闪烁
【发布时间】:2021-05-22 10:19:36
【问题描述】:

我正在 c# winforms 上重新创建一个基于图块的简单游戏(参考:Javidx9 cpp tile game),当我移动时屏幕会闪烁,我有 DoubleBuffered = true。我将展示一个带有纹理的示例和一个没有纹理的示例。

纹理 > e.Graphics.DrawImage()

无纹理 > e.Graphics.FillRectangle()

在代码中我做了一个GameManager、CameraManager、PlayerModel,最后是绘制游戏信息的OnPaint表单。它的工作方式是 GameManager 告诉玩家根据用户输入(移动、跳跃等)更新自身,然后告诉相机根据玩家的位置进行更新。起初我从 Paint 事件中调用 GameManager.Update(),但后来我将 Paint 事件与 GameManager 分开,并使 GameManager 更新异步,因为 Paint 事件更新太慢。那是问题开始的时候。

//GameManager
public void CreateSoloGame(MapModel map)
        {
            CurrentMap = map;
            ResetPlayer();
            _inGame = true;

            new Task(() =>
            {
                while (_inGame)
                {
                    Elapsed = _stopwatch.ElapsedMilliseconds;
                    _stopwatch.Restart();
                    int i = 0;

                    Step(Elapsed);
                    while (i < _gameTime) //_gameTime controls the speed of the game
                    {
                        i++;
                    }
                }
            }).Start();
        }

public void Step(double elapsed)
        {
            Player.Update(CurrentMap, elapsed);
            Camera.SetCamera(Player.Position, CurrentMap);            
        }
//PlayerModel
public void DetectCollision(MapModel CurrentMap, double Elapsed)
        {
            //adds velocity to players position
            float NextPlayerX = Position.X + (VelX * (float)Elapsed);
            float NextPlayerY = Position.Y + (VelY * (float)Elapsed);

            //collision detection
            OnFloor = false;

            if (VelY > 0)
            {
                //bottom
                if (CurrentMap.GetTile((int)(Position.X + .1), (int)(NextPlayerY + 1)) == '#' || CurrentMap.GetTile((int)(Position.X + .9), (int)(NextPlayerY + 1)) == '#')
                {
                    NextPlayerY = (int)NextPlayerY;
                    VelY = 0;
                    OnFloor = true;
                    _jumps = 2;
                }
            }
            else
            {
                //top
                if (CurrentMap.GetTile((int)(Position.X + .1), (int)NextPlayerY) == '#' || CurrentMap.GetTile((int)(Position.X + .9), (int)NextPlayerY) == '#')
                {
                    NextPlayerY = (int)NextPlayerY + 1;
                    VelY = 0;
                }
            }

            if (VelX < 0)
            {
                //left
                if (CurrentMap.GetTile((int)NextPlayerX, (int)Position.Y) == '#' || CurrentMap.GetTile((int)NextPlayerX, (int)(Position.Y + .9)) == '#')
                {
                    NextPlayerX = (int)NextPlayerX + 1;
                    VelX = 0;
                }
            }
            else
            {
                //right
                if (CurrentMap.GetTile((int)(NextPlayerX + 1), (int)Position.Y) == '#' || CurrentMap.GetTile((int)(NextPlayerX + 1), (int)(Position.Y + .9)) == '#')
                {
                    NextPlayerX = (int)NextPlayerX;
                    VelX = 0;
                }
            }

            //updates player position
            Position = new PointF(NextPlayerX, NextPlayerY);
        }

        public void Jump()
        {
            if (_jumps > 0)
            {
                VelY = -.06f;
                _jumps--;
            }
        }

        public void ReadInput(double elapsed)
        {
            //resets velocity back to 0 if player isnt moving
            if (Math.Abs(VelY) < 0.001f) VelY = 0;
            if (Math.Abs(VelX) < 0.001f) VelX = 0;

            //sets velocity according to player input - S and W are used for no clip free mode
            //if (UserInput.KeyInput[Keys.W]) _playerVelY -= .001f;
            //if (UserInput.KeyInput[Keys.S]) _playerVelY += .001f;
            if (Input.KEYINPUT[Keys.A]) VelX -= .001f * (float)elapsed;
            else if (Input.KEYINPUT[Keys.D]) VelX += .001f * (float)elapsed;
            else if (Math.Abs(VelX) > 0.001f && OnFloor) VelX += -0.06f * VelX * (float)elapsed;

            //resets jumping
            if (!OnFloor)
                VelY += .0004f * (float)elapsed;

            //limits velocity
            //if (_playerVelY <= -.014) _playerVelY = -.014f; //disabled to allow jumps
            if (VelY >= .05) VelY = .05f;

            if (VelX >= .02 && !Input.KEYINPUT[Keys.ShiftKey]) VelX = .02f;
            else if (VelX >= .005 && Input.KEYINPUT[Keys.ShiftKey]) VelX = .005f;

            if (VelX <= -.02 && !Input.KEYINPUT[Keys.ShiftKey]) VelX = -.02f;
            else if (VelX <= -.005 && Input.KEYINPUT[Keys.ShiftKey]) VelX = -.005f;
        }

        public void Update(MapModel map, double elapsed)
        {
            ReadInput(elapsed);
            DetectCollision(map, elapsed);
        }
//CameraManager
public void SetCamera(PointF center, MapModel map, bool clamp = true)
        {
            //changes the tile size according to the screen size
            TileSize = Input.ClientScreen.Width / Tiles;

            //amount of tiles along thier axis
            TilesX = Input.ClientScreen.Width / TileSize;
            TilesY = Input.ClientScreen.Height / TileSize;

            //camera offset
            OffsetX = center.X - TilesX / 2.0f;
            OffsetY = center.Y - TilesY / 2.0f;

            //make sure the offset does not go beyond bounds
            if (OffsetX < 0 && clamp) OffsetX = 0;
            if (OffsetY < 0 && clamp) OffsetY = 0;

            if (OffsetX > map.MapWidth - TilesX && clamp) OffsetX = map.MapWidth - TilesX;
            if (OffsetY > map.MapHeight - TilesY && clamp) OffsetY = map.MapHeight - TilesY;

            //smooths out movement for tiles
            TileOffsetX = (OffsetX - (int)OffsetX) * TileSize;
            TileOffsetY = (OffsetY - (int)OffsetY) * TileSize;
        }
//Form Paint event
private void Draw(object sender, PaintEventArgs e)
        {
            Brush b;
            Input.ClientScreen = ClientRectangle;

            for (int x = -1; x < _camera.TilesX + 1; x++)
            {
                for (int y = -1; y < _camera.TilesY + 1; y++)
                {
                    switch (_map.GetTile(x + (int)_camera.OffsetX, y + (int)_camera.OffsetY))
                    {
                        case '.':
                            //e.Graphics.DrawImage(sky, x * _camera.TileSize - _camera.TileOffsetX, y * _camera.TileSize - _camera.TileOffsetY, _camera.TileSize, _camera.TileSize);
                            //continue;
                            b = Brushes.MediumSlateBlue;
                            break;
                        case '#':
                            //e.Graphics.DrawImage(block, x * _camera.TileSize - _camera.TileOffsetX, y * _camera.TileSize - _camera.TileOffsetY, _camera.TileSize, _camera.TileSize);
                            //continue;
                            b = Brushes.DarkGray;
                            break;
                        case 'o':
                            b = Brushes.Yellow;
                            break;
                        case '%':
                            b = Brushes.Green;
                            break;
                        default:
                            b = Brushes.MediumSlateBlue;
                            break;
                    }

                    e.Graphics.FillRectangle(b, x * _camera.TileSize - _camera.TileOffsetX, y * _camera.TileSize - _camera.TileOffsetY, (x + 1) * _camera.TileSize, (y + 1) * _camera.TileSize);                    
                }
            }

            e.Graphics.FillRectangle(Brushes.Purple, (_manager.Player.Position.X - _camera.OffsetX) * _camera.TileSize, (_manager.Player.Position.Y - _camera.OffsetY) * _camera.TileSize, _camera.TileSize, _camera.TileSize);
            //e.Graphics.DrawImage(chef, (_manager.Player.Position.X - _camera.OffsetX) * _camera.TileSize, (_manager.Player.Position.Y - _camera.OffsetY) * _camera.TileSize, _camera.TileSize, _camera.TileSize);
            Invalidate();
        }

附:我使用 winforms,因为我不经常使用 GUI,它是我最熟悉的一个,这只是我想快速尝试的东西,但我从来没有遇到过这个问题。我尝试了几件事,但没有任何效果,所以这是我最后的手段。如果您认为我应该使用另一个 GUI,请告诉我并且我会调查它。另外,如果您认为我的代码很丑,请告诉我为什么。

【问题讨论】:

  • 我不知道是否应该注意这一点,但我已经尝试过优化的双缓冲和其他控制样式。没有任何效果:/
  • 平滑 Win32-ish 图形的一个途径是小心地使区域失效;确保只重绘需要重绘的区域。它可以在这里帮助你。看看我对此的回答:stackoverflow.com/questions/67541811/…
  • PictureBox上画图就不会闪烁了。
  • 代码组织得很糟糕。您将Draw(object sender, PaintEventArgs e) 挂接到哪个事件,为什么在里面调用Invalidate?后者触发了太多的重绘,肯定会导致闪烁。
  • e.Graphics.FillRectangle(Brushes.Purple, (_manager.Player.Position.X - _camera.OffsetX) * _camera.TileSize, (_manager.Player.Position.Y - _camera.OffsetY) * _camera.TileSize, _camera.TileSize, _camera.TileSize); //e.Graphics.DrawImage(chef, (_manager.Player.Position.X - _camera.OffsetX) * _camera.TileSize, (_manager.Player.Position.Y - _camera.OffsetY) * _camera.TileSize, _camera.TileSize, _camera.TileSize); Invalidate(); - 你不应该在 Paint 事件中调用 Invalidate!

标签: c# winforms


【解决方案1】:

PictureBox 填写表单并挂钩.Paint() 事件。出于某种原因,与在表单上绘图相比,PictureBox 上的绘图没有闪烁。

还有一个游戏循环可以改善很多事情。我的示例代码获得了 600+ fps。

完整代码如下:

public partial class RunningForm1 : Form
{
    static readonly Random rng = new Random();
    float offset;
    int index;
    Queue<int> town;
    const int grid = 12;

    Color[] pallete;

    FpsCounter clock;

    #region Windows API - User32.dll
    [StructLayout(LayoutKind.Sequential)]
    public struct WinMessage
    {
        public IntPtr hWnd;
        public Message msg;
        public IntPtr wParam;
        public IntPtr lParam;
        public uint time;
        public System.Drawing.Point p;
    }

    [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
    [DllImport("User32.dll", CharSet = CharSet.Auto)]
    public static extern bool PeekMessage(out WinMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
    #endregion


    public RunningForm1()
    {
        InitializeComponent();

        this.pic.Paint += new PaintEventHandler(this.pic_Paint);
        this.pic.SizeChanged += new EventHandler(this.pic_SizeChanged);

        //Initialize the machine
        this.clock = new FpsCounter();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        Setup();
        System.Windows.Forms.Application.Idle += new EventHandler(OnApplicationIdle);
    }

    void UpdateMachine()
    {
        pic.Refresh();
    }

    #region Main Loop

    private void OnApplicationIdle(object sender, EventArgs e)
    {
        while (AppStillIdle)
        {
            // Render a frame during idle time (no messages are waiting)
            UpdateMachine();
        }
    }

    private bool AppStillIdle
    {
        get
        {
            WinMessage msg;
            return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
        }
    }

    #endregion

    private void pic_SizeChanged(object sender, EventArgs e)
    {
        pic.Refresh();
    }

    private void pic_Paint(object sender, PaintEventArgs e)
    {
        // Show FPS counter
        var fps = clock.Measure();
        var text = $"{fps:F2} fps";
        var sz = e.Graphics.MeasureString(text, SystemFonts.DialogFont);
        var pt = new PointF(pic.Width - 1 - sz.Width - 4, 4);
        e.Graphics.DrawString(text, SystemFonts.DialogFont, Brushes.Black, pt);

        // draw on e.Graphics
        e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
        e.Graphics.TranslateTransform(0, pic.ClientSize.Height - 1);
        e.Graphics.ScaleTransform(1, -1);
        int wt = pic.ClientSize.Width / (grid - 1);
        int ht = pic.ClientSize.Height / (grid + 1);
        SolidBrush fill = new SolidBrush(Color.Black);
        for (int i = 0; i < town.Count; i++)
        {
            float x = offset + i * wt;
            var building = new RectangleF(
                x, 0,
                wt, ht * town.ElementAt(i));
            fill.Color = pallete[(index + i) % pallete.Length];
            e.Graphics.FillRectangle(fill, building);
        }
        offset -= 0.4f;

        if (offset <= -grid - wt)
        {
            UpdateTown();
            offset += wt;
        }

    }

    private void Setup()
    {
        offset = 0;
        index = 0;
        town = new Queue<int>();
        pallete = new Color[]
        {
            Color.FromKnownColor(KnownColor.Purple),
            Color.FromKnownColor(KnownColor.Green),
            Color.FromKnownColor(KnownColor.Yellow),
            Color.FromKnownColor(KnownColor.SlateBlue),
            Color.FromKnownColor(KnownColor.LightCoral),
            Color.FromKnownColor(KnownColor.Red),
            Color.FromKnownColor(KnownColor.Blue),
            Color.FromKnownColor(KnownColor.LightCyan),
            Color.FromKnownColor(KnownColor.Crimson),
            Color.FromKnownColor(KnownColor.GreenYellow),
            Color.FromKnownColor(KnownColor.Orange),
            Color.FromKnownColor(KnownColor.LightGreen),
            Color.FromKnownColor(KnownColor.Gold),
        };

        for (int i = 0; i <= grid; i++)
        {
            town.Enqueue(rng.Next(grid) + 1);
        }
    }

    private void UpdateTown()
    {
        town.Dequeue();
        town.Enqueue(rng.Next(grid) + 1);
        index = (index + 1) % pallete.Length;
    }


}

public class FpsCounter
{
    public FpsCounter()
    {
        this.PrevFrame = 0;
        this.Frames = 0;
        this.PollOverFrames = 100;
        this.Clock = Stopwatch.StartNew();
    }
    /// <summary>
    /// Use this method to poll the FPS counter
    /// </summary>
    /// <returns>The last measured FPS</returns>
    public float Measure()
    {
        Frames++;
        PrevFrame++;
        var dt = Clock.Elapsed.TotalSeconds;

        if (PrevFrame > PollOverFrames || dt > PollOverFrames / 50)
        {
            LastFps = (float)(PrevFrame / dt);
            PrevFrame = 0;
            Clock.Restart();
        }

        return LastFps;
    }
    public float LastFps { get; private set; }
    public long Frames { get; private set; }
    private Stopwatch Clock { get; }
    private int PrevFrame { get; set; }
    /// <summary>
    /// The number of frames to average to get a more accurate frame count.
    /// The higher this is the more stable the result, but it will update
    /// slower. The lower this is, the more chaotic the result of <see cref="Measure()"/>
    /// but it will get a new result sooner. Default is 100 frames.
    /// </summary>
    public int PollOverFrames { get; set; }
}

【讨论】:

  • 由于某种原因, PictureBox 上没有闪烁绘图。 . ..因为默认情况下它是double-buffered。您可以将表单用作绘图画布。将其DoubleBuffered 属性设置为true 以减少闪烁。
  • 他写道,他的表单是双缓冲的。实际问题是在 Paint 事件中无效。
  • 非常感谢,我试过你的方法,不幸的是它仍然发生。
  • 如果您只需要Paint 事件,不要使用PictureBox。仅当您想使用其ImageSizeMode 属性时才使用它。要在 WM_PAINT 上绘图,您可以使用 literally 任何控件。最干净的方法是从Control 派生,设置双缓冲并覆盖OnPaintPictureBox.OnPaint 已经相当 complex (即使在没有图像的情况下只有两个检查会有效执行),而 Panel 根本没有覆盖 OnPaint,例如。
猜你喜欢
  • 2012-02-27
  • 2012-05-12
  • 2012-04-14
  • 1970-01-01
  • 1970-01-01
  • 2015-08-03
  • 2014-11-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多