Tutorial: Clearing a blit canvas by erasing only the portions that have changed (using damage maps or a dirty rect).

Tutorial: Clearing a blit canvas by erasing only the portions that have
changed. This is commonly called damage mapping or  dirty rect processing.

(Squize called this a Damage Map. I have heard that before but didn’t know that it was basically what I was doing here).

That’s a mouthful, but I had trouble coming up with a shorter, more
informative title…

DAMAGE MAPPING!

I am currently working on a mini-retro remake of the game Star Castle
called Solar
Fortress
. I am at the point in my code design where I need
to figure out how I am going to render my particle and special effects
to the screen. Most of the current game objects are standard sprites
with BitmapData making up their look (an embedded Bitmap Object with a
BitmapData attached). For the the special effects and particles, I am
planning to blit to a layer on top of the main screen. I was about to
start writing this code as I would any standard set of blit operations
when I was side tracked by an old book on game programming my Andre
LaMothe.

In this classic book on DOS games, Andre
spills the beans on a number of classic game programming secrets such
as blitting, sprites, image compression (RLE), transparency, and much
more.
While waiting for Steve to finish up a conference call, I sat at his
desk perusing this classic gem. In his chapter on sprites and
blitting, Andre wrote about a method for refreshing the a blit screen
that I had never tried. Usually, I refresh the entire viewable blit
canvas with the background layer before I start to blit the individual
sprites to the canvas. Andre’s method was quite different.

Inside the loop where he updates and blits each object, he first copies
the background under object and replaces the foreground screen location
of the object with the background, effectively erasing the object only.
Then, he copies the object to its new location on the screen. Thus, he
only  updates the screen portions that have changed.

I decided to test this method in AS3 against my normal copyPixels
screen erase and against a method of simply
clearing the background with a solid color using the
fillRect()
of the BitmapData object.

Below is a swf with the results of the tests. This swf blits 6,000
moving 10×10 squares to the screen
using my (and Chris Cutler’s)  optimized
sleep based active timer
. It runs though each of the
three
methods for 100 frames. Before and after each frame it uses a
getTimer()
operation to count the milliseconds passed and adds that to a total.
After 100 frames it divides that total number by 100 to get the average
number of milliseconds. It does this separately  for each of
the three different methods.

This
was written as a very quick and dirty test. The results in the
browsers and players I have seen generally show that the new method
(erase just the
portion that has changed)  to run faster than the other 2
methods.
They are all very close though.  This is a little surprising
to me because with 6,000 objects I would have thought the extra 5,999
blit
operations (although small) would take more more time than the one full
screen erase operation. If you run the test multiple times you will get
slightly varying results. Generally though, they methods rank like this:

Fastest: to Slowest:

1. Just erase screen parts that have changed

2. FillRect() with a solid color – unusable though if you background is
NOT a solid color or is a transparent layer on top of the game player
(like mine will be).

3. Use copyPiixels to re-draw the entire back ground each frame.

Here is an explanation of the three methods. If you need some basics
on my blitting code, try
this tutorial
. After these explanations, the
swf has been embedded for you to try. The entire main class follows
that so you can test out the code if you desire.

Method 1: CopyPixels
Entire Background

In this method (my normal method), at the start of each render cycle, I
erase the blit canvas by copying the entire background BitmapData to
it. I then blit each object into its new position.   With 6000
objects, this equals 6001 blit operations per frame cycle.

1
canvasBD.copyPixels(backgroundBD, backgroundBD.rect,backgroundPoint);

Then update each object and blit it to to the canvasBD
 

(loop through objects for run this code for
each)

1
2
3
blitPoint.x = tempObject.x;
blitPoint.y = tempObject.y;
canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);

Method 2: fillRect()

This method is only possible because I have a solid back background. I
would not be able to use the BitmapData.fillRect() method with a
detailed
background. There is a way to use the Sprite.beginBitmapFill() method,
but that was out of the scope of this test. It also would have been
invalid because it would have required a different canvas for the
Background (a separate Sprite). In my version of  this method,
at the start of the render cycle I simply fill the background canvas
with black and then do the 6000 blit operations.
 

1
canvasBD.fillRect(canvasBD.rect, 0xFF000000);

Then update each object and blit it to to the canvasBD:
 

(loop through objects for run this code for
each)

1
2
blitPoint.x = tempObject.x;
blitPoint.y = tempObject.y;

canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);[/cc]

Method 3: Erase Updated
Background Only

In this method, I do NOT erase or fill the background canvas before I
begin the 6000 blit operations. I actually do 12,000 blit operations.
For each object I first blit the the background under it to its current
position as an eraser, then update the object’s position and then blit
back into its new location.
 

Before Objects are updated

(loop through objects for run this code for
each)

1
2
3
4
blitPoint.x = tempObject.x;
blitPoint.y = tempObject.y;
canvasBD.copyPixels(backgroundBD, objectBD.rect, blitPoint);
 

Then update the  object positions, animation frame, etc and
then run this code:

1
2
3
blitPoint.x = tempObject.x;
blitPoint.y = tempObject.y;
canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);

What is it doing:  
While looping though all of the objects in the array (of particles for instance), it first blits from the background (backgroundBD) to the viewable canvas (canvasBD)

Next, it assumes that your code updates the position and or animation frame of the object and then it blits the bitmapData containing the object (objectBD in this example) to the canvas (canvasBD).


Results

Generally, I have found a 2 or 3 millisecond average speed increase per
frame tick by just erasing the portion of the screen for each object
that has changed. Also, the fillRect() operations seems to be a
little faster (generally) than the copyPixels operation.  This
is the result for 6000 objects, using my optimized timer for 100
frames.
I have provided the code so you can test it out (if you desire) with
more or less objects and more or less frames. I have found that the
results are a little different with fewer objects. With just 1-50
objects on the screen, Method 3 seems to be much faster, but as you add
objects (under 1000), the results are mixed. When you get up to 5000,
clearly the Erase Only the Updated Portion (Method 3) comes out on top
more often.

Still, I don’t see using any of these methods as a problem. I
am going to use the new method for my current game though just because
it is new and I can.

Test it for your self and let me know if you see similar results.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
     package
     {
         import flash.display.*;
         import flash.text.*;
         import flash.geom.*;
         import flash.events.*;
         import flash.events.TimerEvent;
         import flash.utils.Timer;
         import flash.utils.getTimer;
         /**
        * ...
        * @author Jeff Fulton
        */

         public class Main extends Sprite
         {
 

             private var mode:Function;
             //gameTimer
             public static const FRAME_RATE:int = 40;
             public var _period:Number = 1000 / FRAME_RATE;
             public var _beforeTime:int = 0;
             public var _afterTime:int = 0;
             public var _timeDiff:int = 0;
             public var _sleepTime:int = 0;
             public var _overSleepTime:int = 0;
             public var _excess:int = 0;
             public var gameTimer:Timer;
             private var rformat:TextFormat = new TextFormat("_sans","12","0xffffff","true");
             private var messagetext:TextField = new TextField();
             private var aKeyPress:Array=[];

             private var backgroundBD:BitmapData = new BitmapData(400, 400, false, 0x000000);
             private var canvasBD:BitmapData = new BitmapData(400, 400, true, 0xFF000000);
             private var canvasBitmap:Bitmap = new Bitmap(canvasBD);

             private var objectBD:BitmapData = new BitmapData(5, 5, false, 0xff0000);
             private var aObject:Array = [];
             private var tempObject:Object;
             private var blitPoint:Point = new Point();
             private var backgroundPoint:Point = new Point(0, 0);

             private var startTime:Number;
             private var endTime:Number;
             private var totalTime:Number;

             private var resultCopypixels:Number;
             private var resultBitmapfill:Number;
             private var resultBliterase:Number;

             private var numObjects:int = 6000;
             private var numFrames:int = 100;
             private var frameCtr:int = 0;

             public function Main()
             {

                 for (var ctr:int = 0; ctr < numObjects; ctr++) {
                     //trace("creating object: " + ctr);
                     var tempObjectCreate:Object = new Object();
                     tempObjectCreate.x = 0;
                     tempObjectCreate.y = 0;
                     tempObjectCreate.dx = 0;
                     tempObjectCreate.dy = 0;
                     tempObjectCreate.bitmapData = objectBD;
                     aObject.push(tempObjectCreate);
                 }

                 addChild(canvasBitmap);
                 mode = modeMenuSetup;
                 //gameTimer=new Timer(_period,0);
                 gameTimer=new Timer(_period,1);
                 gameTimer.addEventListener(TimerEvent.TIMER, runGame);
                 gameTimer.start();
             }

             public function runGame1(e:TimerEvent):void {
                 mode(0);
                 e.updateAfterEvent();
             }

             public function runGame(e:TimerEvent):void {
                 //trace("run game");
                 _beforeTime = getTimer();
              _overSleepTime = (_beforeTime - _afterTime) - _sleepTime;

                 //****run the current system function
                 mode(0);
                 //***********************************

                 _afterTime = getTimer();
              _timeDiff = _afterTime - _beforeTime;
              _sleepTime = (_period - _timeDiff) - _overSleepTime;
              if (_sleepTime <= 0) {                      
                _excess -= _sleepTime                    
                _sleepTime = 2;              
              }                    
              gameTimer.reset();              
              gameTimer.delay = _sleepTime;              
              gameTimer.start();                                    
              while (_excess > _period) {
                     //****run the current system function
                     mode(1);
                     //***********************************

                     _excess -= _period;
              }

                e.updateAfterEvent();
             }
            private function modeMenuSetup(updatetype:int):void {
                 stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownListener);
                 stage.addEventListener(KeyboardEvent.KEY_UP, keyUpListener);
                 messagetext.width = 250;
                 messagetext.height = 200;
                 messagetext.x = 60;
                 messagetext.y = 100;
                 messagetext.text = "This will test 3 different methods of
                \nrefreshing the background\n
                after a set of blit operations\n\nPress Space to Start Test"
;
                 rformat.align = "center";
                 messagetext.setTextFormat(rformat);
                 addChild(messagetext);

                 mode = modeMenuRun;

             }

             private function modeMenuRun(updatetype:int):void {
                 if (aKeyPress[32]) {
                     trace("space pressed");
                     mode = modeSetupCopypixels;
                 }
             }

             private function modeSetupCopypixels(updatetype:int):void {

                 for (var ctr:int = 0; ctr < numObjects;ctr++ ) {
                     tempObject = aObject[ctr];
                     tempObject.x = (Math.random() * 399);
                     tempObject.y = (Math.random() * 399);
                     var randInt:int=int(Math.random()*36);
                     tempObject.dx=Math.cos(2.0*Math.PI*((randInt*10)-90)/360.0);
                     tempObject.dy = Math.sin(2.0 * Math.PI * ((randInt * 10) - 90) / 360.0);
                 }
                 //trace("all object reset");
                 //trace("object 1 x=" + aObject[1].x);
                 messagetext.text="Running copyPixels test"
                 messagetext.setTextFormat(rformat);
                 frameCtr = 0;
                 resultCopypixels = 0;
                 totalTime = 0;
                 mode = modeRunCopypixels;
             }

             private function modeRunCopypixels(updateType:int):void {
                 startTime = getTimer();
                 switch (updateType) {

                     case 0:
                         backgroundBD.lock();
                         drawCopypixelsBackground();
                         updateAndDrawCopypixelsObjects();
                         backgroundBD.unlock();
                         break;
                     case 1:
                         updateAndDrawCopypixelsObjects();
                         break;

                 }
                 endTime = getTimer();
                 totalTime += (endTime-startTime);
                 frameCtr++;
                 //trace("frameCtr="+frameCtr);
                 if (frameCtr == numFrames) {

                     resultCopypixels = (totalTime/numFrames);
                     trace("copypixels result=" + resultCopypixels);
                     mode = modeSetupBitmapfill;
                 }
             }

             private function updateAndDrawCopypixelsObjects():void {

                 //trace("object 1 again x=" + aObject[1].x);
                 for (var ctr:int = 0; ctr < numObjects;ctr++ ) {                      
                    tempObject = aObject[ctr];                                            
                    tempObject.x += tempObject.dx;                      
                    tempObject.y += tempObject.dy;                    
                    if (tempObject.x > 399) {
                         tempObject.x = 0;
                     }else if (tempObject.x<0) {                          
                        tempObject.x = 399;                      
                     }                                            
 
                    if (tempObject.y > 399) {
                         tempObject.y = 0;
                     }else if (tempObject.y<0) {
                         tempObject.y = 399;
                     }

                     blitPoint.x = tempObject.x;
                     blitPoint.y = tempObject.y;
                     //trace("blitPoint=" + blitPoint.toString());
                     canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);

                 }

             }

             private function drawCopypixelsBackground():void {
                 canvasBD.copyPixels(backgroundBD, backgroundBD.rect,backgroundPoint);
             }

             private function modeSetupBitmapfill(updatetype:int):void {
                 for (var ctr:int = 0; ctr < numObjects;ctr++ ) {
                     tempObject = aObject[ctr];
                     tempObject.x = (Math.random() * 399);
                     tempObject.y = (Math.random() * 399);
                     var randInt:int=int(Math.random()*36);
                     tempObject.dx=Math.cos(2.0*Math.PI*((randInt*10)-90)/360.0);
                     tempObject.dy = Math.sin(2.0 * Math.PI * ((randInt * 10) - 90) / 360.0);
                 }
                 messagetext.text = "Running fillRect test";
                 messagetext.setTextFormat(rformat);
                 frameCtr = 0;
                 resultBitmapfill = 0;
                 totalTime = 0;
                 mode = modeRunBitmapfill;
             }

             private function modeRunBitmapfill(updateType:int):void {
                 startTime = getTimer();
                 switch (updateType) {

                     case 0:
                         backgroundBD.lock();
                         drawBitmapfillBackground();
                         updateAndDrawBitmapfillObjects();
                         backgroundBD.unlock();
                         break;
                     case 1:
                         updateAndDrawBitmapfillObjects();
                         break;

                 }
                 endTime = getTimer();
                 totalTime += (endTime-startTime);
                 frameCtr++;
                 //trace("frameCtr="+frameCtr);
                 if (frameCtr == numFrames) {
                     resultBitmapfill =(totalTime/numFrames);
                     trace("bitmapfill result=" + resultBitmapfill);
                     mode = modeSetupBliterase;
                 }
             }

             private function updateAndDrawBitmapfillObjects():void {

                 //trace("object 1 again x=" + aObject[1].x);
                 for (var ctr:int = 0; ctr < numObjects;ctr++ ) {                      
                    tempObject = aObject[ctr];                                            
                    tempObject.x += tempObject.dx;                      
                    tempObject.y += tempObject.dy;                      
                    if (tempObject.x > 399) {
                         tempObject.x = 0;
                    }else if (tempObject.x<0) {                          
                       tempObject.x = 399;                      
                    }                                            
                    if (tempObject.y > 399) {
                         tempObject.y = 0;
                     }else if (tempObject.y<0) {
                         tempObject.y = 399;
                     }

                     blitPoint.x = tempObject.x;
                     blitPoint.y = tempObject.y;
                     //trace("blitPoint=" + blitPoint.toString());
                     canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);

                 }

             }

             private function drawBitmapfillBackground():void {
                 canvasBD.fillRect(canvasBD.rect, 0xFF000000);
             }

             private function modeSetupBliterase(updatetype:int):void {
                 for (var ctr:int = 0; ctr < numObjects;ctr++ ) {
                     tempObject = aObject[ctr];
                     tempObject.x = (Math.random() * 399);
                     tempObject.y = (Math.random() * 399);
                     var randInt:int=int(Math.random()*36);
                     tempObject.dx=Math.cos(2.0*Math.PI*((randInt*10)-90)/360.0);
                     tempObject.dy = Math.sin(2.0 * Math.PI * ((randInt * 10) - 90) / 360.0);
                 }
                 messagetext.text = "Running bliterase test";
                 messagetext.setTextFormat(rformat);
                 frameCtr = 0;
                 resultBliterase = 0;
                 totalTime = 0;
                 startTime = getTimer();
                 mode = modeRunBliterase;
             }
           private function modeRunBliterase(updateType:int):void {
                 startTime = getTimer();
                 switch (updateType) {

                     case 0:
                         backgroundBD.lock();
                         drawBitmapfillBackground();
                         updateAndDrawBitmapfillObjects();
                         backgroundBD.unlock();
                         break;
                     case 1:
                         updateAndDrawBitmapfillObjects();
                         break;

                 }
                 endTime = getTimer();
                 totalTime += (endTime-startTime);
                 frameCtr++;
                 //trace("frameCtr="+frameCtr);
                 if (frameCtr == numFrames) {

                     endTime = getTimer();
                     resultBliterase =(totalTime/numFrames);
                     trace("bliterase result=" + resultBliterase);
                     mode = modeResultsSetup;
                 }
             }

             private function updateAndDrawBliteraseObjects():void {

                 //trace("object 1 again x=" + aObject[1].x);
                 for (var ctr:int = 0; ctr < numObjects; ctr++ ) {                      
                    blitPoint.x = tempObject.x;                      
                    blitPoint.y = tempObject.y;                      
                    canvasBD.copyPixels(backgroundBD, objectBD.rect, blitPoint);                                            
                    tempObject = aObject[ctr];                                          
                    tempObject.x += tempObject.dx;                      
                    tempObject.y += tempObject.dy;                      
                    if (tempObject.x > 399) {
                       tempObject.x = 0;
                    }else if (tempObject.x<0) {                          
                        tempObject.x = 399;                      
                    }                                            
                   
                     if (tempObject.y > 399) {
                        tempObject.y = 0;
                     }else if (tempObject.y<0) {
                         tempObject.y = 399;
                     }

                     blitPoint.x = tempObject.x;
                     blitPoint.y = tempObject.y;
                     canvasBD.copyPixels(objectBD, objectBD.rect, blitPoint);

                 }

             }

             private function modeResultsSetup(updateType:int):void {
                 canvasBD.copyPixels(backgroundBD, backgroundBD.rect,backgroundPoint);

                 messagetext.text = "Results (avg milliseconds):\nCopypixels: " + resultCopypixels +
             " milliseconds\nBitmapfill: " + resultBitmapfill + " milliseconds\nBliterase: " +
             resultBliterase + " milliseconds"+"\n\n[R] to run again.";
                 messagetext.setTextFormat(rformat);
                 mode = modeResultsrun;
             }

             private function modeResultsrun(updateType:int):void {
                 if (aKeyPress[82]) {
                     for (var ctr:int = 0; ctr < numObjects;ctr++ ) {
                         tempObject = aObject[ctr];
                         tempObject = null;
                     }
                     mode = modeMenuSetup;
                 }

             }

             private function keyDownListener(e:KeyboardEvent):void {
                 trace(e.keyCode);
                 aKeyPress[e.keyCode]=true;

             }

             private function keyUpListener(e:KeyboardEvent):void {
                 aKeyPress[e.keyCode]=false;
             }
         }

     }

That’s all of the code. You can use it as the Main class in a Flex
project or as the document class in a Flash project. It will work
either way.

Beyond:

There are other optimizations that can be used in conjunction with this
code to further control when the screen is updated. One thing I would
add is the ability to not update an object if it hasn’t changes. If an
object is simply stationary (x+dx=x and y+dy=y) on a frame, then do
NOTHING to it. Don’t erase it, don’t re-blit it, just leave it alone.
Another would be to further optimized the animation render by not
updating an object if the same frame of object animation is being
displayed  on this timer tick as was displayed in the previous
timer tick.

0saves
This entry was posted in Tutorial-Flash, Tutorials. Bookmark the permalink.