有没有一种快速的方法在Windows Forms中操作和缓存屏幕?

7
我正在开发一个用于学习目的的游戏,我想仅使用.NET框架和C#中的Windows Forms项目制作。
我希望将“屏幕”(可以在窗口上显示的内容)作为int[]获取。以缓冲方式修改数组并重新应用更改后的数组到“屏幕”上(以避免闪烁)。
我目前使用一个Panel,在其上绘制一个Bitmap,然后使用GraphicsBitmap转换为int[],接着我就可以修改它并重新应用到Bitmap并重新绘制。虽然这种方法能够工作,但速度非常慢,特别是因为我必须在每一帧都要将图像放大,因为我的游戏只有300x160,而屏幕却是900x500。
构建如下:
    // Renders 1 frame
    private void Render()
    {    
        // Buffer setup
        _bufferedContext = BufferedGraphicsManager.Current;
        _buffer = _bufferedContext.Allocate(panel_canvas.CreateGraphics(), new Rectangle(0, 0, _scaledWidth, _scaledHeight));

        _screen.clear();

        // Get position of player on map
        _xScroll = _player._xMap - _screen._width / 2;
        _yScroll = _player._yMap - _screen._height / 2;

        // Indirectly modifies the int[] '_pixels'
        _level.render(_xScroll, _yScroll, _screen);
        _player.render(_screen);

        // Converts the int[] into a Bitmap (unsafe method is faster)
        unsafe
        {
            fixed (int* intPtr = &_screen._pixels[0])
            {
                _screenImage = new Bitmap(_trueWidth, _trueHeight, _trueWidth * 4, PixelFormat.Format32bppRgb, new IntPtr(intPtr));
            }
        }

        // Draw generated image on buffer
        Graphics g = _buffer.Graphics;
        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

        g.DrawImage(_screenImage, new Rectangle(0, 0, 900, 506));

        // Update panel buffered
        _buffer.Render();
    }

有没有更快的方法在不使用外部库的情况下使这个工作?
3个回答

1

我不太确定有关不安全代码的事情,但我知道缓冲图形管理器。我认为你应该为它创建一个类,而不是每次都创建一个新的。并且在加载时确定所有精灵的宽度和高度,而不是缩放它们。这样可以加快我的小游戏引擎。

class Spritebatch
{
    private Graphics Gfx;
    private BufferedGraphics bfgfx;
    private BufferedGraphicsContext cntxt = BufferedGraphicsManager.Current;

    public Spritebatch(Size clientsize, Graphics gfx)
    {
        cntxt.MaximumBuffer = new Size(clientsize.Width + 1, clientsize.Height + 1);
        bfgfx = cntxt.Allocate(gfx, new Rectangle(Point.Empty, clientsize));
        Gfx = gfx;
    }

    public void Begin()
    {
        bfgfx.Graphics.Clear(Color.Black);
    }

    public void Draw(Sprite s)
    {
        bfgfx.Graphics.DrawImageUnscaled(s.Texture, new Rectangle(s.toRec.X - s.rotationOffset.Width,s.toRec.Y - s.rotationOffset.Height,s.toRec.Width,s.toRec.Height));
    }

    public void drawImage(Bitmap b, Rectangle rec)
    {
        bfgfx.Graphics.DrawImageUnscaled(b, rec);
    }

    public void drawImageClipped(Bitmap b, Rectangle rec)
    {
        bfgfx.Graphics.DrawImageUnscaledAndClipped(b, rec);
    }

    public void drawRectangle(Pen p, Rectangle rec)
    {
        bfgfx.Graphics.DrawRectangle(p, rec);
    }

    public void End()
    {
        bfgfx.Render(Gfx);
    }

}

这是我使用的一个示例。它被设置成模仿Xna中的Spritebatch。不缩放绘制图像将真正提高其速度。此外,创建一个缓冲图形和上下文的实例比每次渲染时创建一个新的实例要快。因此,我建议您将行g.DrawImage(_screenImage,new Rectangle(0,0,900,506))更改为DrawImageUnscaled(_screenImage,new Rectangle(0,0,900,506))。

编辑:如何在精灵加载时缩放代码的示例

    public Sprite(Bitmap texture, float x, float y, int width, int height)
    {
        //texture is the image you originally start with.

        Bitmap b = new Bitmap(width, height);
        // Create a bitmap with the desired width and height
        using (Graphics g = Graphics.FromImage(b))
        {
            g.DrawImage(texture, 0, 0, width, height);
        }
        // get the graphics from the new image and draw the old image to it
        //scaling it to the proper width and height
        Texture = b;
        //set Texture which is the final picture to the sprite.
        //Uppercase Texture is different from lowercase

1
我必须对图像进行缩放,因为我的游戏只有320x160像素大小,我想将其放大以适应用户的屏幕。 - Berntonline
@Berntonline 我知道你可能需要缩放图片。我在我的游戏中也需要这样做。你需要在开始时对它们进行缩放。例如: - John Thompson
我每帧都有一个新图像,所以我必须每次都放大它们,不是吗? - Berntonline

0

即使不进行任何插值,图像的缩放成本也足够昂贵。为了加快速度,您应该尽量减少内存分配:每帧创建全新的位图会导致对象创建和像素图缓冲区分配。这个事实抵消了您从BufferedGraphics获得的所有好处。我建议您执行以下操作:

  1. 仅在Render方法之外创建所需大小(等于屏幕大小)的Bitmap实例。
  2. 通过LockBits方法直接访问位图数据,并尝试使用最近的像素手动实现缩放。

当然,使用某种硬件加速来进行缩放操作是最优选项(例如,在OpenGL中,所有图像通常都使用纹理矩形绘制,并且在执行纹理采样时隐式地涉及“缩放”过程)。


0
我想知道为什么你称这个程序“非常慢”,因为我进行了一些测试,性能似乎并不差。此外,你是否单独测量了渲染代码中的 int[] '_pixels'(不幸的是你没有提供该代码)的性能,因为它可能是慢的部分。
关于你具体的问题。正如其他人所提到的,使用预分配的缓冲图形和位图对象会稍微加速一些。
但是你真的需要那个 int[] 缓冲吗?BufferedGraphics 已经在内部支持位图,所以实际上发生的是:

(1) 你填充 int[] 缓冲
(2) int[] 缓冲被复制到新的/预分配的 Bitmap
(3) 步骤2中的 Bitmap 被复制(应用比例)到 BufferedGraphics 内部位图(通过 DrawImage
(4) BufferedGraphics 内部位图被复制到屏幕上(通过 Render

正如您所看到的,有很多复制操作。 BufferedGraphics 的预期用途是:

(1) 您可以通过 BufferedGraphics.Graphics 属性的绘图方法填充 BufferedGraphics 内部位图。如果设置了,Graphics 将为您执行缩放(以及其他转换)
(2) 通过 Render,将 BufferedGraphics 内部位图复制到屏幕上

我不知道您的绘图代码在做什么,但如果您能承受它,这绝对应该提供最佳性能。

以下是我的快速测试,如果您感兴趣:

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Threading;
using System.Windows.Forms;

namespace Test
{
    enum RenderMode { NewBitmap, PreallocatedBitmap, Graphics }
    class Screen
    {
        Control canvas;
        public Rectangle area;
        int[,] pixels;
        BitmapData info;
        Bitmap bitmap;
        BufferedGraphics buffer;
        float scaleX, scaleY;
        public RenderMode mode = RenderMode.NewBitmap;
        public Screen(Control canvas, Size size)
        {
            this.canvas = canvas;
            var bounds = canvas.DisplayRectangle;
            scaleX = (float)bounds.Width / size.Width;
            scaleY = (float)bounds.Height / size.Height;
            area.Size = size;
            info = new BitmapData { Width = size.Width, Height = size.Height, PixelFormat = PixelFormat.Format32bppRgb, Stride = size.Width * 4 };
            pixels = new int[size.Height, size.Width];
            bitmap = new Bitmap(size.Width, size.Height, info.PixelFormat);
            buffer = BufferedGraphicsManager.Current.Allocate(canvas.CreateGraphics(), bounds);
            buffer.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
            ApplyMode();
        }
        public void ApplyMode()
        {
            buffer.Graphics.ResetTransform();
            if (mode == RenderMode.Graphics)
                buffer.Graphics.ScaleTransform(scaleX, scaleY);
        }
        public void FillRectangle(Color color, Rectangle rect)
        {
            if (mode == RenderMode.Graphics)
            {
                using (var brush = new SolidBrush(color))
                    buffer.Graphics.FillRectangle(brush, rect);
            }
            else
            {
                rect.Intersect(area);
                if (rect.IsEmpty) return;
                int colorData = color.ToArgb();
                var pixels = this.pixels;
                for (int y = rect.Y; y < rect.Bottom; y++)
                    for (int x = rect.X; x < rect.Right; x++)
                        pixels[y, x] = colorData;
            }
        }
        public unsafe void Render()
        {
            if (mode == RenderMode.NewBitmap)
            {
                var bounds = canvas.DisplayRectangle;
                using (var buffer = BufferedGraphicsManager.Current.Allocate(canvas.CreateGraphics(), bounds))
                {
                    Bitmap bitmap;
                    fixed (int* pixels = &this.pixels[0, 0])
                        bitmap = new Bitmap(info.Width, info.Height, info.Stride, info.PixelFormat, new IntPtr(pixels));
                    buffer.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
                    buffer.Graphics.DrawImage(bitmap, bounds);
                    buffer.Render();
                }
            }
            else
            {
                if (mode == RenderMode.PreallocatedBitmap)
                {
                    fixed (int* pixels = &this.pixels[0, 0])
                    {
                        info.Scan0 = new IntPtr(pixels); info.Reserved = 0;
                        bitmap.LockBits(area, ImageLockMode.WriteOnly | ImageLockMode.UserInputBuffer, info.PixelFormat, info);
                        bitmap.UnlockBits(info);
                    }
                    buffer.Graphics.DrawImage(bitmap, canvas.DisplayRectangle);
                }
                buffer.Render();
            }
        }
    }
    class Game
    {
        [STAThread]
        public static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var game = new Game();
            game.Run();
        }
        Form form;
        Control canvas;
        Screen screen;
        Level level;
        Player player;
        private Game()
        {
            form = new Form();
            canvas = new Control { Parent = form, Bounds = new Rectangle(0, 0, 900, 506) };
            form.ClientSize = canvas.Size;
            screen = new Screen(canvas, new Size(300, 160));
            level = new Level { game = this };
            player = new Player { game = this };
        }
        private void Run()
        {
            bool toggleModeRequest = false;
            canvas.MouseClick += (sender, e) => toggleModeRequest = true;
            var worker = new Thread(() =>
            {
                int frameCount = 0;
                Stopwatch drawT = new Stopwatch(), applyT = new Stopwatch(), advanceT = Stopwatch.StartNew(), renderT = Stopwatch.StartNew(), infoT = Stopwatch.StartNew();
                while (true)
                {
                    if (advanceT.ElapsedMilliseconds >= 3)
                    {
                        level.Advance(); player.Advance();
                        advanceT.Restart();
                    }
                    if (renderT.ElapsedMilliseconds >= 8)
                    {
                        frameCount++;
                        drawT.Start(); level.Render(); player.Render(); drawT.Stop();
                        applyT.Start(); screen.Render(); applyT.Stop();
                        renderT.Restart();
                    }
                    if (infoT.ElapsedMilliseconds >= 1000)
                    {
                        double drawS = drawT.ElapsedMilliseconds / 1000.0, applyS = applyT.ElapsedMilliseconds / 1000.0, totalS = drawS + applyS;
                        var info = string.Format("Render using {0} - Frames:{1:n0} FPS:{2:n0} Draw:{3:p2} Apply:{4:p2}",
                            screen.mode, frameCount, frameCount / totalS, drawS / totalS, applyS / totalS);
                        form.BeginInvoke(new Action(() => form.Text = info));
                        infoT.Restart();
                    }
                    if (toggleModeRequest)
                    {
                        toggleModeRequest = false;
                        screen.mode = (RenderMode)(((int)screen.mode + 1) % 3);
                        screen.ApplyMode();
                        frameCount = 0; drawT.Reset(); applyT.Reset();
                    }
                }
            });
            worker.IsBackground = true;
            worker.Start();
            Application.Run(form);
        }
        class Level
        {
            public Game game;
            public int pos = 0; bool right = true;
            public void Advance() { Game.Advance(ref pos, ref right, 0, game.screen.area.Right - 1); }
            public void Render()
            {
                game.screen.FillRectangle(Color.SaddleBrown, new Rectangle(0, 0, pos, game.screen.area.Height));
                game.screen.FillRectangle(Color.DarkGreen, new Rectangle(pos, 0, game.screen.area.Right, game.screen.area.Height));
            }
        }
        class Player
        {
            public Game game;
            public int x = 0, y = 0;
            public bool right = true, down = true;
            public void Advance()
            {
                Game.Advance(ref x, ref right, game.level.pos, game.screen.area.Right - 5, 2);
                Game.Advance(ref y, ref down, 0, game.screen.area.Bottom - 1, 2);
            }
            public void Render() { game.screen.FillRectangle(Color.Yellow, new Rectangle(x, y, 4, 4)); }
        }
        static void Advance(ref int pos, ref bool forward, int minPos, int maxPos, int delta = 1)
        {
            if (forward) { pos += delta; if (pos < minPos) pos = minPos; else if (pos > maxPos) { pos = maxPos; forward = false; } }
            else { pos -= delta; if (pos > maxPos) pos = maxPos; else if (pos < minPos) { pos = minPos; forward = true; } }
        }
    }
}

与“像素”相关的操作并不占用太多时间,我已经测试过了。 - Berntonline

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