Есть ли быстрый способ манипулировать и буферизовать экран в 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. Я советую вам сделать следующее:
- Создайте экземпляр Bitmap необходимого размера (равного размеру экрана) только один раз, вне метода Render.
- Используйте прямой доступ к растровым данным через метод LockBits и попытайтесь реализовать масштабирование вручную, используя ближайший пиксель.
Конечно, использование некоторого аппаратного ускорения для операции масштабирования является наиболее предпочтительным вариантом (например, в opengl все изображения обычно рисуются с использованием текстурированных прямоугольников, и рендеринг таких прямоугольников неявно включает в себя процесс "масштабирования", когда выполняется выборка текстуры),