-1

I am recreating a simple tile based game (ref: Javidx9 cpp tile game) on c# winforms and the screen flickers as i move, I have DoubleBuffered = true. i will show an example with textures and one without.

TEXTURES > e.Graphics.DrawImage()

NO TEXTURES > e.Graphics.FillRectangle()

in the code I made a GameManager, CameraManager, PlayerModel, lastly the form OnPaint that draws the game information. the way it works is the GameManager tells the Player to update itself depending on user input (move, jump, ect...), then tells the Camera to update depending on the players position. at first i called the GameManager.Update() from the Paint event but then i separated the Paint event from the GameManager and made the GameManager update asynchronous because the Paint event updates too slow. thats when the problem started.

//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();
        }

P.S. i use winforms because i dont work with GUIs much and its the one im most familiar with and this is just something quick i wanted to try out but i've never had this issue. i tried a couple of things but nothing worked so this is my last resort. if you think i should use another GUI let me know and ill look into it. also if you think my code is ugly lmk why.

insignia
  • 51
  • 1
  • 10
  • i dont know if i should note this but i have tried optimized double buffer and other control styles. nothing works :/ – insignia May 21 '21 at 23:54
  • One path to smooth Win32-ish graphics is careful invalidation of areas; making sure that only areas that need to be redrawn are redrawn. It may help you here. Take a look at my answer to this: https://stackoverflow.com/questions/67541811/how-should-i-buffer-drawn-rectangles-to-improve-performance-c-net-winforms-gd/67542354#67542354 – Flydog57 May 22 '21 at 00:12
  • Draw on a `PictureBox` and it won't flicker. – John Alexiou May 22 '21 at 01:34
  • 2
    The code is so badly organized. Which event did you hook `Draw(object sender, PaintEventArgs e)` to and why do you call `Invalidate` inside? The latter triggers far too many redraws definitely leading to flickers. – Lex Li May 22 '21 at 01:40
  • 1
    `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();` - You should never call an Invalidate in a Paint event! – TaW May 22 '21 at 04:51
  • I hooked up Draw to Paint event somewhere else in the code. i didnt know i couldnt call Invalidate() inside the event, could someone please clear up why i shouldnt do that? also im sorry for messy code, im trying to get better. – insignia May 22 '21 at 05:12
  • The official sample is clear enough, https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invalidate?view=net-5.0#System_Windows_Forms_Control_Invalidate, though you need to study more so as to know the problem of calling `Invalidate` in the wrong places. – Lex Li May 22 '21 at 05:22
  • i've tried calling invalidate somewhere else and even with a delay, yet the flicker stays. i dont think invalidate is the problem. – insignia May 22 '21 at 05:31

1 Answers1

0

Fill the form with a PictureBox and hook into the .Paint() event. For some reason, there is no flicker drawing on a PictureBox compared to drawing on a form.

Also having a game loop improves things a lot. I am getting 600+ fps with my example code.

scr

full code below:

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; }
}
John Alexiou
  • 28,472
  • 11
  • 77
  • 133
  • 1
    _For some reason, there is no flicker drawing on a PictureBox . . ._. Because it's [double-buffered](https://referencesource.microsoft.com/#system.windows.forms/winforms/Managed/System/WinForms/PictureBox.cs,128) by default. You can use the Form as the drawing canvas. Set its `DoubleBuffered` property to `true` to get the flickering reduced. – dr.null May 22 '21 at 03:21
  • Alternative [BufferedGraphics](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.bufferedgraphics?view=net-5.0). – dr.null May 22 '21 at 03:31
  • 1
    He wrote that his Form is double-buffered. The actual issue is Invalidating in the Paint event. – TaW May 22 '21 at 04:52
  • 1
    Thank you very much, i tried your method and unfortunatly it still happens. – insignia May 22 '21 at 05:19
  • Do **NOT** use `PictureBox` if all you need is just the `Paint` event. Use it only if you want to use its `Image` and the `SizeMode` properties. To draw on WM_PAINT you can use _literally_ any control. The cleanest way is to derive from `Control`, set double buffering and override `OnPaint`. `PictureBox.OnPaint` is already quite [complex](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/PictureBox.cs,1292) (even if only two checks will effectively execute if there is no image), whereas `Panel` has no overridden `OnPaint` at all, for example. – György Kőszeg May 22 '21 at 08:55
  • `Refresh` is also a bad practice. It forces an extraordinary repaint. Instead, use `Invalidate` to let Windows update the UI in a regular WM_PAINT session. If `Invalidate` has no effect it indicates that you do something wrong (the UI thread is blocked somewhere). – György Kőszeg May 22 '21 at 09:09
  • @GyörgyKőszeg for a game you can't for windows to decide when the repaint. You need a continuous refresh. That is why I hook into `.ApplicationIdle` event to force a refresh of the screen when possible. – John Alexiou May 22 '21 at 15:21
  • @JohnAlexiou: I look it differently. I tend to use dedicated threads for the 'game loop' that can prepare all information for the next frame. An `Invalidate` can be called from any thread without the cross-thread exception. Including the repaint session in the game loop typically slows down _all_ events of the game on a slower computer, not just the FPS. But time should elapse with the same speed everywhere, even if frames are dropped. Your animation now freezes when you resize the form, for example. That's not how a game should work IMHO. – György Kőszeg May 22 '21 at 16:23