Вопрос:
Я работаю над приложением Winforms и могу использовать некоторые советы.
У меня есть несколько сотен 50х50 спрайтов, которые со временем ходят на игровом поле 2000×2000. Вначале я создал его с помощью спрайтов, которые были автоматически создаются ящиками для изображений, которые были добавлены в форму и перемещены. Он выполнил свою работу, но он был мерцающим и медленным.
После довольно многого поискового запроса это выглядело как создание буфера кадров и рисование непосредственно на нем, а затем применение буфера к статическому изображению на форме, казалось, было способом.
Поэтому я все это выстраиваю, и он заканчивается гораздо медленнее, чем использование ящиков с картинками. Кажется, это связано с размером буфера 2000×2000 (требуется около 100 мс для создания буфера каждый раз).
Код для рисования экрана:
private void animateAmoebas() { for (int animationStep = 0; animationStep < 100; animationStep = animationStep + animationStepSize) { Image buffer = new Bitmap(2000, 2000); buffer = imageBKG; //Redraw the grid pattern. foreach (Amoeba _Amoeba in amoebaPool)//Ameboa is a class object that has AI in it to detirmine the actions of the Amoeba. { //PBL (PictureBoxLoader) is an object that contains the sprite image, plus the cordinates for that sprite in that frame. pbl = _Amoeba.animateSprite(animationStep,pbl); drawSprite(pbl, buffer);//Draw the sprite to the buffer } refreshScreen(buffer);//Copy the buffer to the picturebox } } private void drawSprite(PictureBoxLoader pbLoader, Image _buffer) { using (Graphics formGraphics = Graphics.FromImage(_buffer)) { Point imgPoint = new Point(pbLoader.imgX, pbLoader.imgY); formGraphics.DrawImageUnscaled(pbLoader.imgImage, imgPoint); } } private void refreshScreen(Image _image) { pictureBox_BKG.Image = _image; this.Refresh(); }
Любые предложения для лучшего способа сделать это?
Я попытался сделать статическое создание imagebuffer раньше времени и просто перерисовать фон. Это помогает, но оно все еще значительно медленнее, чем использование ящиков с картинками. Хотя, по общему признанию, метод выше позволяет использовать прозрачные пленки.
Лучший ответ:
Вы не должны использовать PictureBox вообще. Просто выведите из Control и переопределите OnPaint. Вы можете нарисовать изображение буфера внутри OnPaint а затем нарисовать изображение в элементе управления.
public class SpriteCanvas : Control { private const int AnimationSteps = 100; private const int AnimationStepSize = 4; private System.Windows.Forms.Timer _timer; private Bitmap _buffer; private int _animationStep = 0; public SpriteCanvas() { _buffer = new Bitmap(2000, 2000, PixelFormat.Format32bppPArgb); _timer = new System.Windows.Forms.Timer(); _timer.Interval = 10; _timer.Tick += (s, e) => { _animationStep += AnimationStepSize; if (_animationStep > AnimationSteps) _animationStep = 0; this.Invalidate(); }; _timer.Start(); } protected override void OnPaint(PaintEventArgs e) { using (var g = Graphics.FromImage(_buffer)) { // draw sprites based on current _animationStep value // g.DrawImage(…) } e.Graphics.DrawImage(_buffer, new Rectangle(0, 0, _buffer.Width, _buffer.Height), new Rectangle(0, 0, _buffer.Width, _buffer.Height), GraphicsUnit.Pixel); } protected override void Dispose(bool disposing) { base.Dispose(disposing); _timer.Dispose(); _buffer.Dispose(); } }
У вас много других проблем в коде. Прежде всего, я предполагаю, что вы рисуете все в потоке пользовательского интерфейса. Это не-нет. Вы должны вызвать Invalidate на элементе управления, когда хотите его перерисовать. Вы должны сделать это по таймеру.
Вы также создаете новый буфер изображения на каждой итерации цикла, и вы сразу же отбрасываете его, даже не удаляя его:
Image buffer = new Bitmap(2000, 2000); buffer = imageBKG; //Redraw the grid pattern.
Класс Bitmap реализует IDisposable и вы всегда должны обертывать его в блок using или вызывать Dispose на нем, когда он вам больше не нужен. В вашем случае вы, вероятно, только хотите создать 1 растровое изображение, чтобы действовать как ваш буфер, и вы должны распоряжаться им, когда ваш Control находится.
Другая ошибка, которую вы делаете, – это вызвать Refresh, это вызовет синхронную краску, которая в вашем случае заставит ваш элемент управления замерзнуть. Я не думаю, что у вас есть все основания для этого. Используйте Invalidate вместо Refresh.
Вы также называете Graphics.FromImage каждый раз, когда вы рисуете один спрайт. Таким образом, вы вызываете это сотни раз за кадр. Очевидно, вы не хотите этого делать. Вы должны вызывать только один раз за ничью.
Ответ №1
Вы инициализируете новый битмап (2000, 2000) на каждом шаге анимации и никогда не используете его повторно, что разрушит хакер в сборщике мусора. Вместо этого сохраните экземпляр вашего буфера в качестве переменной-члена и сохраните пустой спрайт того же размера. В начале каждого цикла рисования нарисуйте пустой спрайт на буфер, затем нарисуйте спрайты амебы.
Это все равно может вызвать мерцание из-за несоответствий между частотой обновления экрана и скоростью вытягивания. Чтобы исправить это, используйте двойную буферизацию. Здесь некоторый псевдо-С# для простого метода двойной буферизации.
private Image backBuffer = new Bitmap(2000, 2000); private Image frontBuffer = new Bitmap(2000, 2000); private Image clearSprite = new Bitmap(2000, 2000); // Draws 1 frame private void Draw() { // Clear the back buffer backBuffer.Draw(clearSprite); // Draw sprites foreach (var sprite in sprites) { backBuffer.Draw(sprite); } // Swap buffers frontBuffer.Draw(backBuffer); }
Вы также запускаете всю анимацию, не прибегая к управлению другим методом, поэтому я бы также рекомендовал перейти к асинхронной модели, где у вас есть менеджер чертежей, который отвечает за управление буферами, и ему предлагается запустить его функцию рисования с заданным интервалом, Каждая амеба должна контролировать, какому фрейму она включена, и она должна быть способна нарисовать себя в буфере, когда она была передана менеджером рисования.