Есть ли быстрый способ манипулировать и буферизовать экран в Windows Forms?

Я работаю над игрой в учебных целях, я хочу сделать это только с помощью.NET-Framework и проекта Windows Forms в C#.

Я хочу получить "экран" (что-то, что может быть отображено в окне) как int[], Измените массив и повторно примените измененный массив к "экрану" буферизованным способом (чтобы он не мерцал).

Я в настоящее время использую Panel, который я рисую Bitmap на с Graphics, Bitmap превращается в 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 ответа

Я не уверен насчет небезопасного кода, но я знаю о буферизованном графическом менеджере. Я думаю, вы должны создать для него класс вместо того, чтобы каждый раз создавать новый. Кроме того, необходимо определить ширину и высоту всех ваших спрайтов при загрузке, а не масштабировать их. Это ускорило мой маленький игровой движок.

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

}

Это пример того, что я использовал. Это настроено, чтобы подражать Spritebatch в Xna. Рисование немасштабированных изображений действительно увеличит его скорость. Также создание одного экземпляра буферизованной графики и контекста будет происходить быстрее, чем создание нового каждый раз, когда вам придется рендерить. Поэтому я бы посоветовал вам изменить строку g.DrawImage(_screenImage, new Rectangle(0, 0, 900, 506)); DrawImageUnscaled(_screenImage, новый прямоугольник (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

Мне интересно, почему вы называете это "очень медленным", потому что я провел несколько тестов, и производительность не кажется плохой. Также вы измерили производительность вашего кода рендеринга в int[] '_pixels' (к сожалению, вы не предоставили этот код) отдельно от растровых операций, потому что это может быть медленной частью.
По поводу вашего конкретного вопроса. Как уже упоминалось, использование предварительно выделенной буферизованной графики и растровых объектов немного ускорит его.
Но тебе это действительно нужно? int[] буфер? BufferedGraphics внутренне уже поддерживается растровым изображением, поэтому на самом деле происходит следующее:

(1) Вы заполняете int[] буфер
(2) int[] буфер копируется в новый / предварительно выделенный Bitmap
(3) Bitmap с шага 2 копируется (применяя масштаб) к BufferedGraphics внутреннее растровое изображение (через DrawImage)
(4) BufferedGraphics внутреннее растровое изображение копируется на экран (через Render)

Как видите, операций копирования очень много. Предполагаемое использование BufferedGraphics является:

(1) Вы заполняете BufferedGraphics внутреннее растровое изображение с помощью методов рисования BufferedGraphics.Graphics имущество. Если настройка, то Graphics сделает масштабирование (а также другие преобразования) для вас.
(2) BufferedGraphics внутреннее растровое изображение копируется на экран (через Render)

Я не знаю, что делает ваш код рисования, но если вы можете себе это позволить, это определенно должно обеспечить наилучшую производительность.

Вот мой быстрый и грязный тест на случай, если вы заинтересованы в:

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; } }
        }
    }
}

Масштабирование изображения достаточно дорого, даже если это делается без какой-либо интерполяции. Чтобы ускорить процесс, вы должны минимизировать распределение памяти: когда вы создаете новый растровый рисунок в каждом кадре, это приводит к созданию объекта и выделению буфера растрового изображения. Этот факт сводит на нет все преимущества, которые вы получаете от BufferedGraphics. Я советую вам сделать следующее:

  1. Создайте экземпляр Bitmap необходимого размера (равного размеру экрана) только один раз, вне метода Render.
  2. Используйте прямой доступ к растровым данным через метод LockBits и попытайтесь реализовать масштабирование вручную, используя ближайший пиксель.

Конечно, использование некоторого аппаратного ускорения для операции масштабирования является наиболее предпочтительным вариантом (например, в opengl все изображения обычно рисуются с использованием текстурированных прямоугольников, и рендеринг таких прямоугольников неявно включает в себя процесс "масштабирования", когда выполняется выборка текстуры),

Другие вопросы по тегам