Lazarus

Programming => General => Topic started by: knuckles on May 13, 2017, 09:52:59 pm

Title: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 13, 2017, 09:52:59 pm
I have been looking at various different programs such as Tiled, RPG Maker, Game Maker etc to see how they draw tiles and sprites onto the levels etc. Most modern applications seem to utilize hardware acceleration either through OpenGL or DirectX.

I was looking closely at some older versions of Game Maker because they does not use hardware acceleration and I was interested to find out how it is able to draw lots of sprites on the levels (or in this case rooms) and move and delete them without too much slow down. Using DeDe and Olly it wasn't too hard to easily see that the room editor actually uses nothing more than a TImage and drawing directly onto its internal bitmap canvas, for example Image1.Canvas.Draw(X, Y, Sprite).

What stumped me though is how exactly does it achieve the moving and deleting of the drawn sprites from the internal bitmap of the level/room? You can hold the Ctrl key down and click and move one of the drawn sprites that is under the mouse, or delete the sprite under the mouse etc but how is everything handled here?

See if I was planning on creating a level editor I would have an array or pointer list storing sprite information such as X and Y position and then draw everything onto a canvas such as paintbox etc, then moving a sprite would simply be a case of changing the X and Y position of that sprite and invalidating.

Out of curiosity though and from a technical point of view, how do you think the level editor in old Game Maker versions achieves the drawing, moving and deleting of sprites onto a TImage canvas? See, the sprites are already drawn onto the internal bitmap so I don't see an obvious way of how the internal bitmap is updated when a sprite is moved or deleted?

Maybe I am missing the obvious and it is not really that technical at all but as a little exercise, it would be interesting to see how it is possible to draw directly onto a TImage canvas and then move/delete just like the Game Maker level (room) editor does without slowdown.

Ideas and suggestions welcome :)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Thaddy on May 13, 2017, 10:02:47 pm
The technique used is based on regions and clipping, i.e. the objects are drawn in a paint event or a forced paintcall from background to foreground, where the smaller objects are painted within their own boundaries. Note that on moving the objects only the area that is affected by the movement needs to be repainted based on the region it occupies on the total canvas. This is highly efficient. There's a bit more to it, but that's the theory.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 13, 2017, 10:44:48 pm
The technique used is based on regions and clipping, i.e. the objects are drawn in a paint event or a forced paintcall from background to foreground, where the smaller objects are painted within their own boundaries. Note that on moving the objects only the area that is affected by the movement needs to be repainted based on the region it occupies on the total canvas. This is highly efficient. There's a bit more to it, but that's the theory.

Ok that's a great start, good insight.

I'm trying to understand the design choice, why would the developer decide to draw directly to a image canvas and not a paintbox for example, seems rather odd to me and that prompted this question of how can you move already painted objects on a bitmap around, it confused me somewhat.

Maybe I can attempt to write a short demo project to further understand this approach..
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 14, 2017, 12:24:28 am
Ok here is the starting blocks of the test project...

For this test i chose to store sprites in an array, each sprite has its own bitmap and information regarding X and Y position which will be needed to move to a new position etc.

There are placeholders for moving and deleting which I will try and implement tomorrow as its getting late here.

Source:
Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  9.   StdCtrls;
  10.  
  11. type
  12.   TSprite = record
  13.     Bitmap: TBitmap;
  14.     X: Integer;
  15.     Y: Integer;
  16.   end;
  17.  
  18.   TForm1 = class(TForm)
  19.     imgCanvas: TImage;
  20.     imgSprite1: TImage;
  21.     imgSprite2: TImage;
  22.     RadioButton1: TRadioButton;
  23.     RadioButton2: TRadioButton;
  24.     procedure FormCreate(Sender: TObject);
  25.     procedure FormDestroy(Sender: TObject);
  26.     procedure imgCanvasMouseDown(Sender: TObject; Button: TMouseButton;
  27.       Shift: TShiftState; X, Y: Integer);
  28.     procedure imgCanvasMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  29.     procedure imgCanvasMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  30.   private
  31.     FDrawing: Boolean;
  32.     FMoving: Boolean;
  33.     FDeleting: Boolean;
  34.     FSprites: array of TSprite;
  35.     function GetSprite: TBitmap;
  36.     procedure DrawSprite(ASprite: TBitmap; AX, AY: Integer);
  37.   public
  38.     { public declarations }
  39.   end;
  40.  
  41. var
  42.   Form1: TForm1;
  43.  
  44. implementation
  45.  
  46. {$R *.lfm}
  47.  
  48. { TForm1 }
  49.  
  50. function TForm1.GetSprite: TBitmap;
  51. begin
  52.   if RadioButton1.Checked then
  53.     Result := imgSprite1.Picture.Bitmap
  54.   else if RadioButton2.Checked then
  55.     Result := imgSprite2.Picture.Bitmap
  56. end;
  57.  
  58. procedure TForm1.DrawSprite(ASprite: TBitmap; AX, AY: Integer);
  59. begin
  60.   SetLength(FSprites, Length(FSprites) + 1);
  61.   FSprites[High(FSprites)].Bitmap := TBitmap.Create;
  62.   FSprites[High(FSprites)].Bitmap.Assign(ASprite);
  63.   FSprites[High(FSprites)].X := AX;
  64.   FSprites[High(FSprites)].Y := AY;
  65.  
  66.   imgCanvas.Canvas.Draw(AX, AY, ASprite);
  67. end;
  68.  
  69. procedure TForm1.FormCreate(Sender: TObject);
  70. begin
  71.   SetLength(FSprites, 0);
  72. end;
  73.  
  74. procedure TForm1.FormDestroy(Sender: TObject);
  75. var
  76.   I: Integer;
  77. begin
  78.   for I := Low(FSprites) to High(FSprites) do
  79.     FSprites[I].Bitmap.Free;
  80.   SetLength(FSprites, 0);
  81. end;
  82.  
  83. procedure TForm1.imgCanvasMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  84. begin
  85.   FDrawing := Button = mbLeft;
  86.   FMoving := (ssCtrl in Shift) and (Button = mbLeft);
  87.   FDeleting := Button = mbRight;
  88.  
  89.   if FDrawing then
  90.   begin
  91.     DrawSprite(GetSprite, X, Y);
  92.   end;
  93.  
  94.   if FMoving then
  95.   begin
  96.     //
  97.   end;
  98.  
  99.   if FDeleting then
  100.   begin
  101.     //
  102.   end;
  103. end;
  104.  
  105. procedure TForm1.imgCanvasMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  106. begin
  107.   if FDrawing then
  108.   begin
  109.     DrawSprite(GetSprite, X, Y);
  110.   end;
  111.  
  112.   if FMoving then
  113.   begin
  114.     //
  115.   end;
  116.  
  117.   if FDeleting then
  118.   begin
  119.     //
  120.   end;
  121. end;
  122.  
  123. procedure TForm1.imgCanvasMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  124. begin
  125.   FDrawing := False;
  126.   FMoving := False;
  127.   FDeleting := False;
  128. end;
  129.  
  130. end.

LFM:
Code: Pascal  [Select][+][-]
  1. object Form1: TForm1
  2.   Left = 369
  3.   Height = 529
  4.   Top = 130
  5.   Width = 634
  6.   Caption = 'Form1'
  7.   ClientHeight = 529
  8.   ClientWidth = 634
  9.   OnCreate = FormCreate
  10.   OnDestroy = FormDestroy
  11.   LCLVersion = '1.6.4.0'
  12.   object imgCanvas: TImage
  13.     Left = 8
  14.     Height = 450
  15.     Top = 9
  16.     Width = 618
  17.     OnMouseDown = imgCanvasMouseDown
  18.     OnMouseMove = imgCanvasMouseMove
  19.     OnMouseUp = imgCanvasMouseUp
  20.   end
  21.   object imgSprite1: TImage
  22.     Left = 32
  23.     Height = 24
  24.     Top = 472
  25.     Width = 24
  26.     Picture.Data = {
  27.       07544269746D6170F6060000424DF80600000000000036000000280000001800
  28.       000018000000010018000000000000000000120B0000120B0000000000000000
  29.       0000FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF797877787776
  30.       767675FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00
  31.       FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF7B
  32.       7A7981807FC0C0BF9A9A99807F7E73737270706FFF00FFFF00FFFF00FFFF00FF
  33.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00
  34.       FFFF00FF7D7B7A929191EDEDEDF2F2F2E0E0E0DADADAC1C1C19998987979786D
  35.       6D6C6C6B6AFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  36.       FF00FFFF00FFFF00FF7F7E7D939392F6F6F6FFFFFFF4F4F4E5E5E5E0E0E0D9D9
  37.       D9D4D4D42380230E690D346A33516950686867676766FF00FFFF00FFFF00FFFF
  38.       00FFFF00FFFF00FFFF00FFFF00FF807F7EACACABF6F6F6FFFFFFFFFFFFF7F7F7
  39.       EAEAEAE5E5E5E0E0E0D9D9D9248124016801016801016801789D789696967574
  40.       74646463626260FF00FFFF00FFFF00FFFF00FF828180ADACACFFFFFFFFFFFFFF
  41.       FFFFFFFFFFFAFAFAEFEFEFEAEAEAE5E5E5DEDEDE258225016801016801016801
  42.       86AB86BFBFBFBDBDBDB1B1B160605FFF00FFFF00FFFF00FF838382CECECEFFFF
  43.       FFFFFFFFFFFFFFFFFFFFFFFFFFFCFCFCF4F4F4EFEFEFEAEAEAE5E5E526832601
  44.       680101680101680189AF89C4C4C4BFBFBFBDBDBD60605FFF00FFFF00FF868483
  45.       B7B7B6FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFEFEFAFAFAF4F4F4EFEF
  46.       EFEAEAEA2786270168010168010168018EB28EC9C9C9C4C4C4BFBFBF60605FFF
  47.       00FFFF00FF868483C0BFBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
  48.       FEFEFEFAFAFAF4F4F4EFEFEF28872801680101680101680191B791CECECEC9C9
  49.       C9C4C4C460605FFF00FFFF00FF868483C0BFBFFFFFFFFFFFFFFFFFFFEAF0EAAF
  50.       C5AFAFC5AFFFFFFFFEFEFEFEFEFEFAFAFAF4F4F4288828016801016801016801
  51.       95BB95D4D4D4CECECEC9C9C960605FFF00FFFF00FF868483C0BFBFFFFFFFFFFF
  52.       FFAFC5AF114711003A00003A00467446FEFEFEFEFEFEFEFEFEFAFAFA29892901
  53.       680101680101680198BF98D9D9D9D3D3D3CECECE60605FFF00FFFF00FF868483
  54.       C0BFBFEAF0EA56815600360007660C139020004B00004100467446FEFEFEFEFE
  55.       FEFEFEFEFAFAFACEE2CE81BA81509E50ADCAADDEDEDED8D8D8D3D3D360605FFF
  56.       00FFFF00FF868483C0BFBF114711003F00148F232DD44E28CB44139320004B00
  57.       003F00467446FEFEFEFEFEFEFEFEFEFAFAFAF4F4F4EEEEEEE9E9E9E3E3E3DEDE
  58.       DED8D8D860605F003500FF00FF868483003500075F0C2BC44A33DC582DD44E28
  59.       CB4423C43B108F1C004B00003A00467446FEFEFEFEFEFEFEFEFEFAFAFAF3F3F3
  60.       EEEEEEE9E9E9E3E3E387A187063A06004100FF00FF003A001B992F3FED6C39E5
  61.       6333DC582DD44E28CB4423C43B1DBC330D8A17004B00003A00467446FEFEFEFE
  62.       FEFEFEFEFEFAFAFAF3F3F3EEEEEE4E784E003500004600FF00FFFF00FF056409
  63.       45F6773FED6C39E56333DC582DD44E28CB4423C43B1DBC3318B42A0B8612004B
  64.       00003A00467446FEFEFEFEFEFEFEFEFED0DCD01C4E1C003600004900FF00FFFF
  65.       00FFFF00FF05640934D1593FED6C39E56333DC582DD44E28CB4423C43B1DBC33
  66.       18B42A13AC2209810F004B00003A00467446FEFEFE9AB59A073A07003A00004B
  67.       00FF00FFFF00FFFF00FFFF00FFFF00FF0564092FCB5139E56333DC582DD44E28
  68.       CB4423C43B1DBC3318B42A13AC220FA41A067D0A004B00003A001C501C003000
  69.       004100004B00FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF05640934D9
  70.       5A33DC582DD44E28CB4423C43B1DBC3318B42A19B52C0DA1180B9D1304780700
  71.       4B00003C00004200004B00FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  72.       FF00FFFF00FF086A0D2ED1502DD44E28CB4423C43B1DBC332CD14B34DD590187
  73.       0309991007960D027404004B00004B00FF00FFFF00FFFF00FFFF00FFFF00FFFF
  74.       00FFFF00FFFF00FFFF00FFFF00FFFF00FF07690C29CA4728CB4423C43B1DBC33
  75.       2DD34D34DD59008100008300058209005601FF00FFFF00FFFF00FFFF00FFFF00
  76.       FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF05640924
  77.       C23F23C43B1DBC332DD34D34DD59008100008100056409FF00FFFF00FFFF00FF
  78.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00
  79.       FFFF00FFFF00FF0462071FBB351BB42E0564090F6919014F02016402056409FF
  80.       00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  81.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF056409056409FF00FF0048000042
  82.       00004A01FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF
  83.       00FF
  84.     }
  85.     Transparent = True
  86.   end
  87.   object RadioButton1: TRadioButton
  88.     Left = 16
  89.     Height = 19
  90.     Top = 496
  91.     Width = 59
  92.     Caption = 'Sprite 1'
  93.     Checked = True
  94.     TabOrder = 1
  95.     TabStop = True
  96.   end
  97.   object imgSprite2: TImage
  98.     Left = 112
  99.     Height = 24
  100.     Top = 472
  101.     Width = 24
  102.     Picture.Data = {
  103.       07544269746D6170F6060000424DF80600000000000036000000280000001800
  104.       000018000000010018000000000000000000120B0000120B0000000000000000
  105.       0000FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  106.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00
  107.       FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF
  108.       00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  109.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFA47874A47874A478
  110.       74A47874A47874A47874A47874A47874A47874A47874A47874A47874A47874A4
  111.       7874A47874A47874A47874A47874A47874A47874A47874FF00FFFF00FFA47874
  112.       BE9281F0C39BFFD1A2FFD1A1FED1A1FFD1A2FFD1A2FFD1A1FFD1A1FFD1A2FFD1
  113.       A1FFD1A1FFD1A1FFD1A1FFD1A2FFD1A2FFD1A2FFD1A2FFD2A2F0C39ABE9281A4
  114.       7874FF00FFA47874E2BA9CB98E81F9D2ABFFD7ADFED7ADFFD7AEFFD7ADFFD7AE
  115.       FED7ADFFD6ADFED7AEFED7AEFFD7ADFFD7AEFFD7ADFED7ADFFD7AEFFD7ADFAD2
  116.       ABB98E81E2B99CA47874FF00FFA47874FEDEBCD0A996C9A191FFDEBBFFDEBCFF
  117.       DEBCFFDEBCFEDEBCFEDEBCFFDEBBFFDEBCFEDEBCFFDEBCFEDEBCFEDEBCFFDEBC
  118.       FFDEBCFFDEBCC8A191D0A996FEDEBCA47874FF00FFA47874FEE6CCFCE3CABE98
  119.       8EDCBCAAFEE6CBFFE5CBFFE6CBFEE6CBFEE5CCFFE5CCFFE5CCFFE6CBFEE5CCFE
  120.       E6CCFEE6CBFFE5CBFFE5CCDCBCAABE988DFDE3C9FFE6CBA47874FF00FFA47874
  121.       FEEDDBFFEDDBF4DFD0B58E87EED7C7FFEDDBFFEDDBFEEDDBFFEDDBFEEDDBFFED
  122.       DBFEEDDBFEEDDBFEEDDBFEEDDBFEEDDBEDD7C8B58E87F3DFCFFEEDDBFEEDDBA4
  123.       7874FF00FFA47874FEF4E9FEF4EAFEF4E9E5D2CAB6918CF8ECE3FFF4EAFEF4EA
  124.       FEF4EAFEF4E9FEF4EAFEF4E9FEF4EAFEF4E9FEF4E9F9ECE3B6918CE6D2C9FEF4
  125.       E9FEF4EAFEF4E9A47874FF00FFA47874FEFAF5FEFAF6FEFAF5FFF9F6D2BAB6C5
  126.       A7A3FEFAF6FEFAF6FEFAF5FEF9F5FEFAF6FEFAF5FEFAF6FEFAF5FFFAF6C5A7A3
  127.       D2BBB7FEF9F6FEFAF6FEFAF5FEFAF5A47874FF00FFA47874FEFEFEFEFEFEFEFE
  128.       FEFEFEFEFDFCFCBFA09ED8C5C4FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE
  129.       FEFED6C3C1C0A29FFDFCFCFEFEFEFEFEFEFEFEFEFEFEFEA47874FF00FFA47874
  130.       FEFEFEFEFEFEFEFEFEFDFCFCD4BFBEBE9F9CAD8582AD8582AD8582AD8582AD85
  131.       82AD8582AD8582AD8582AD8582D0B9B7FDFCFCFEFEFEFEFEFEFEFEFEFEFEFEA4
  132.       7874FF00FFA47874FEFEFEFEFEFEF4F0EFBFA09DC4A7A5F7F4F4FEFEFEFEFEFE
  133.       FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEF7F4F4C4A7A5BFA09DF4F0EFFEFE
  134.       FEFEFEFEFEFEFEA47874FF00FFA47874FEFEFEE4D8D6B48F8CD9C6C5FEFEFEFE
  135.       FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE
  136.       D9C6C5B48F8CE4D8D6FEFEFEFEFEFEA47874FF00FFA47874D3BEBCB89693BEBE
  137.       BEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE
  138.       FEFEFEFEFEFEFEFEFEFEFEEEE6E5B89693D3BEBCFEFEFEA47874FF00FFA47874
  139.       B59D9EB8ECECBEBEBEFFFFFFFCFFFFFBFFFFF8FFFFF8FFFFF3FFFFF3FFFFF3FF
  140.       FFF5FFFFF5FFFFF7FFFFF8FFFFF7FFFFF3FFFFEDF8F8DCE8E8C5C7C7C3B1B1A4
  141.       7874FF00FFA47874B9AAADBAFEFFBEBEBEFEFFFFFCFFFFFCFFFFFCFFFFFFFFFF
  142.       FFFFFFFFFFFFFCFFFFF2F9F9DFE6E7CFD8D9C6CCCDBFC4C5BEBFBFBCBFC0B5C0
  143.       C4AFBEC3B6A6A8A47874FF00FFFF00FFA47874C8ABABBEBEBEFFFFFFFFFFFFFF
  144.       FFFFEEF0F0D9DFE0CCD2D3C2C9C9BCC0C1BCBEBFB6BFC1ACC0C5A0C0C996C0CE
  145.       8CC2D881C9E792BCD4B99BA0A47874FF00FFFF00FFFF00FFFF00FFA47874D3AB
  146.       AABFC4C5BDC9C9BEBEBEB8C6C8AFC9CBA5CDD299D5DA8FDCE387E1ED88E1F08A
  147.       DEF08BDCF189E0F987E0FDA7BFD0A47874A47874FF00FFFF00FFFF00FFFF00FF
  148.       FF00FFFF00FFA47874AC8D8BAEC6CA8BF4FC85F8FF8AEFFB8BF0FC8BF2FD8DF3
  149.       FF8DF4FF8EF1FE8FEDFD89F0FF90E3F7AFBBC6A47874FF00FFFF00FFFF00FFFF
  150.       00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFA47874C1AAAB9DDCE285FFFF
  151.       8BF8FF8DF3FF8DF3FF8DF3FF8AF7FF87FAFF9ED6E2C1A8ABA47874FF00FFFF00
  152.       FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFA4
  153.       7874A47874ADC9CD90F0F887FCFF87FCFF88FAFF8EF2FCADC9CDA47874A47874
  154.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00
  155.       FFFF00FFFF00FFFF00FFFF00FFA47874A47874B1C0C3AACACFB2BFC1A47874A4
  156.       7874FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF
  157.       FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFA47874A478
  158.       74A47874FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF
  159.       00FF
  160.     }
  161.     Transparent = True
  162.   end
  163.   object RadioButton2: TRadioButton
  164.     Left = 96
  165.     Height = 19
  166.     Top = 496
  167.     Width = 59
  168.     Caption = 'Sprite 2'
  169.     TabOrder = 0
  170.   end
  171. end

I will take some time to think about what you said in your post Thaddy and hopefully I can fully understand it and try and come up with a solution tomorrow.

Thanks :)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 14, 2017, 05:23:20 am
I updated your code, but it's not finished yet. I'm busy at the moment.

Code: Pascal  [Select][+][-]
  1. procedure TForm1.DeleteLastSprite;
  2. begin
  3.   if High(FSprites) < 0 then Exit;
  4.   with FSprites[High(FSprites)] do
  5.   begin
  6.     imgCanvas.Canvas.Brush.Color := clBlack;;
  7.     imgCanvas.Canvas.Rectangle(X, Y, X+W, Y+H);
  8.   end;
  9.   SetLength(FSprites, Length(FSprites) - 1);
  10.   UpdateInfoCount;
  11.  
  12.   //
  13.   //  Todo: Need to check and redraw other affected sprites
  14.   //
  15.  
  16. end;

You can download and test the attached code.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 14, 2017, 03:51:33 pm
Great Handoko thanks for helping out :)

I added a function to get the sprite underneath the mouse if it is of any use:

Code: Pascal  [Select][+][-]
  1. TSprite = record
  2.   ID: Integer;
  3.   ...


Code: Pascal  [Select][+][-]
  1. procedure TForm1.DrawSprite(ASprite: TBitmap; AX, AY: Integer);
  2. begin
  3.   ...
  4.   FSprites[High(FSprites)].ID := Length(FSprites);
  5.   ...
  6. end;


Code: Pascal  [Select][+][-]
  1. function TForm1.GetSprite(AX, AY: Integer): TSprite;
  2. var
  3.   I: Integer;
  4.   SpriteRect: TRect;
  5. begin
  6.   for I := Length(FSprites) -1 downto 0 do
  7.   begin
  8.     SpriteRect.Left := FSprites[I].X;
  9.     SpriteRect.Top := FSprites[I].Y;
  10.     SpriteRect.Right := SpriteRect.Left + FSprites[I].Bitmap.Width;
  11.     SpriteRect.Bottom := SpriteRect.Top + FSprites[I].Bitmap.Height;
  12.     if PtInRect(SpriteRect, Point(AX, AY)) then
  13.     begin
  14.       Result := FSprites[I];
  15.       Break;
  16.     end;
  17.   end;
  18. end;

Example:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.imgCanvasMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  2. var
  3.   Sprite: TSprite;
  4. begin
  5.   Sprite := GetSprite(X, Y);
  6.   Caption := 'Sprite: ' + IntToStr(Sprite.ID);
  7.   ...
  8. end;

That code could also be updated if it is useful as I noticed you added W and H property to our sprite record so there is no longer any need to get the size information from the bitmap as I have done above.

EDIT: To avoid any confusion I had renamed the original GetSprite function to GetSpriteBitmap
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 14, 2017, 11:15:37 pm
Ok, im probably not doing a very good job here and maybe using a object list would have been better than an array (i am not as useful working with arrays)

I attempted to update your code Handoko, it now deletes the sprite under the mouse (although the elements need deleting from the FSprites array too, currently I am unsure how to solve that).

It is a small update but slowly we can try and bring everything together.

I assume to make the main bitmap undisturbed we can somehow use copyrect, maybe this is not the same as what Thaddy mentioned but perhaps the principle is the same maybe?
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 15, 2017, 09:17:50 am
My suggestion is like this:

For example, the sprite #2 will be deleted, so:

1. Detect which sprites are affected. Using bounding box trick, the result is #1 and #3.
2. Draw sprite #1 and #3 on a virtual screen.
3. Copy the virtual screen exactly the bounding box size and position back to the screen.
4. Because sprite #4 is outside the bounding box, so it will be ignored.

This trick should has less performance impact when the sprites count increasing a lot. Because usually overlapping sprites are not much.

We should make sure the drawing on the virtual screen and copying back process are optimized for performance.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 15, 2017, 10:55:50 am
Such programs simply simulate how Windows UI itself works. It's not that hard. Something like:

1) Just make list of sprites, stored in Z order.
2) When user clicks - test all sprites in this list till hit.
3) To determine hit - first perform bounding box test. Something like "if Sprite.Rect.Contains(ClickPoint) then...".
4) If necessary, perform some extra hit test - such alpha test in order to exclude transparent pixels. Something like "if Sprite.HitTest(ClickPoint)".
5) If it's hit - perform some actions.
6) If you need to redraw canvas - just draw sprites in reversed order. You may do it in direct order, but you'd need Z Buffer then.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 15, 2017, 11:11:12 am
knuckles has implemented your point 1 and 2. Now he should continue to the step 3 - the bounding box test.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 15, 2017, 03:05:09 pm
My example:
Code: Pascal  [Select][+][-]
  1. unit TestMain;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, fgl;
  9.  
  10. type
  11.  
  12.   { TSpriteTestForm }
  13.  
  14.   TSprite = class
  15.   protected
  16.     FBitmap:TBitmap;
  17.   public
  18.     Rect:TRect;
  19.     Highlighted:Boolean;
  20.     constructor Create(ABitmap:TBitmap;AIndex:Integer;ARect:TRect);
  21.     destructor Destroy;override;
  22.     procedure Draw(ACanvas:TCanvas);
  23.     function HitTest(APoint:TPoint):Boolean;
  24.     property Bitmap:TBitmap read FBitmap;
  25.   end;
  26.  
  27.   TSpriteList = specialize TFPGList<TSprite>;
  28.  
  29.   TSpriteTestForm = class(TForm)
  30.     SpriteImage: TImage;
  31.     procedure FormCreate(Sender: TObject);
  32.     procedure FormDestroy(Sender: TObject);
  33.     procedure SpriteImageMouseDown(Sender: TObject; Button: TMouseButton;
  34.       Shift: TShiftState; X, Y: Integer);
  35.     procedure SpriteImageMouseMove(Sender: TObject; Shift: TShiftState; X,
  36.       Y: Integer);
  37.     procedure SpriteImageMouseUp(Sender: TObject; Button: TMouseButton;
  38.       Shift: TShiftState; X, Y: Integer);
  39.   private
  40.     { private declarations }
  41.   public
  42.     { public declarations }
  43.     Drag:Boolean;
  44.     DragSprite:TSprite;
  45.     DragStart:TPoint;
  46.     SpriteStart:TRect;
  47.     SpriteBitmap:TBitmap;
  48.     SpriteList:TSpriteList;
  49.     procedure UpdateRect;
  50.   end;
  51.  
  52. var
  53.   SpriteTestForm: TSpriteTestForm;
  54.  
  55. implementation
  56.  
  57. {$R *.lfm}
  58.  
  59. { TSpriteTestForm }
  60.  
  61.  
  62. procedure TSpriteTestForm.FormCreate(Sender: TObject);
  63.   var Bitmap:TBitmap;
  64. begin
  65.   Randomize;
  66.   Bitmap := TBitmap.Create;
  67.   Bitmap.Width := 640;
  68.   Bitmap.Height := 480;
  69.   Bitmap.Canvas.Brush.Color := clGreen;
  70.   Bitmap.Canvas.FillRect(0, 0, 640, 480);
  71.   SpriteImage.Picture.Bitmap := Bitmap;
  72.   SpriteBitmap := TBitmap.Create;
  73.   SpriteBitmap.LoadFromFile('MS.bmp');
  74.   SpriteList := TSpriteList.Create;
  75. end;
  76.  
  77. procedure TSpriteTestForm.FormDestroy(Sender: TObject);
  78.   var I:Integer;
  79. begin
  80.   SpriteBitmap.Free;
  81.   for I := 0 to SpriteList.Count - 1 do begin
  82.     SpriteList[I].Free;
  83.   end;
  84.   SpriteList.Free;
  85. end;
  86.  
  87. procedure TSpriteTestForm.SpriteImageMouseDown(Sender: TObject;
  88.   Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  89.   var Sprite:TSprite;I:Integer;
  90. begin
  91.   if Button = mbRight then begin
  92.     Sprite := TSprite.Create(SpriteBitmap, Random((12 * 8) - 1), TRect.Create(TPoint.Create(X - 40, Y - 40), 81, 81));
  93.     SpriteList.Insert(0, Sprite);
  94.     SpriteImageMouseMove(Sender, Shift, X, Y);
  95.   end;
  96.   DragSprite := nil;
  97.   Drag := False;
  98.   if Button = mbLeft then begin
  99.     for I := 0 to SpriteList.Count - 1 do begin
  100.       if SpriteList[I].HitTest(TPoint.Create(X, Y)) then begin
  101.         DragSprite := SpriteList[I];
  102.         DragStart := TPoint.Create(X, Y);
  103.         SpriteStart := SpriteList[I].Rect;
  104.         Break;
  105.       end;
  106.     end;
  107.   end;
  108. end;
  109.  
  110. procedure TSpriteTestForm.SpriteImageMouseMove(Sender: TObject;
  111.   Shift: TShiftState; X, Y: Integer);
  112.   var I:Integer;Found:Boolean;
  113.   Rect:TRect;Dist:TPoint;
  114. begin
  115.   Rect := TRect.Create(0, 0, 0, 0);
  116.   if Assigned(DragSprite) then begin
  117.     Dist := TPoint.Create(X, Y) - DragStart;
  118.     if not Drag then begin
  119.       if (Abs(Dist.X) > 5) or (Abs(Dist.Y) > 5) then begin
  120.         Drag := True;
  121.       end;
  122.     end
  123.     else begin
  124.       Rect.Union(DragSprite.Rect);
  125.       DragSprite.Rect := SpriteStart;
  126.       DragSprite.Rect.Offset(Dist);
  127.       Rect.Union(DragSprite.Rect);
  128.     end;
  129.   end;
  130.   Found := False;
  131.   for I := 0 to SpriteList.Count - 1 do begin
  132.     if Found then begin
  133.       if SpriteList[I].Highlighted then begin
  134.         SpriteList[I].Highlighted := False;
  135.         Rect.Union(SpriteList[I].Rect);
  136.       end;
  137.     end
  138.     else begin
  139.       if SpriteList[I].HitTest(TPoint.Create(X, Y)) then begin
  140.         if not SpriteList[I].Highlighted then begin
  141.           SpriteList[I].Highlighted := True;
  142.           Rect.Union(SpriteList[I].Rect);
  143.         end;
  144.         Found := True;
  145.       end
  146.       else begin
  147.         if SpriteList[I].Highlighted then begin
  148.           SpriteList[I].Highlighted := False;
  149.           Rect.Union(SpriteList[I].Rect);
  150.         end;
  151.       end;
  152.     end;
  153.   end;
  154.   UpdateRect;
  155. end;
  156.  
  157. procedure TSpriteTestForm.SpriteImageMouseUp(Sender: TObject;
  158.   Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  159.   var I:Integer;
  160. begin
  161.   if Button = mbLeft then begin
  162.     if not Drag then begin
  163.       for I := 0 to SpriteList.Count - 1 do begin
  164.         if SpriteList[I].HitTest(TPoint.Create(X, Y)) then begin
  165.           SpriteList.Move(I, 0);
  166.         end;
  167.       end;
  168.       SpriteImageMouseMove(Sender, Shift, X, Y);
  169.     end;
  170.     DragSprite := nil;
  171.   end;
  172. end;
  173.  
  174. procedure TSpriteTestForm.UpdateRect;
  175.   var I:Integer;
  176. begin
  177.   with SpriteImage.Picture.Bitmap.Canvas do begin
  178.     Brush.Color := clGreen;
  179.     FillRect(ClipRect);
  180.   end;
  181.   for I := SpriteList.Count - 1 downto 0 do begin
  182.     SpriteList[I].Draw(SpriteImage.Picture.Bitmap.Canvas);
  183.   end;
  184. end;
  185.  
  186. constructor TSprite.Create(ABitmap:TBitmap;AIndex:Integer;ARect:TRect);
  187.   var Dest, Src:TRect;
  188. begin
  189.   Rect := ARect;
  190.   FBitmap := TBitmap.Create;
  191.   FBitmap.Width := 81;
  192.   FBitmap.Height := 81;
  193.   Src := TRect.Create(TPoint.Create(0, 0), 81, 81);
  194.   Dest := Src;
  195.   Src.Offset(5 + 81 * (AIndex mod 12), 5 + 81 * (AIndex div 12));
  196.   FBitmap.Canvas.CopyRect(Dest, ABitmap.Canvas, Src);
  197.   FBitmap.Mask(clBlack);
  198. end;
  199.  
  200. destructor TSprite.Destroy;
  201. begin
  202.   Bitmap.Free;
  203.   inherited Destroy;
  204. end;
  205.  
  206. procedure TSprite.Draw(ACanvas:TCanvas);
  207. begin
  208.   ACanvas.Draw(Rect.Left, Rect.Top, Bitmap);
  209.   if Highlighted then begin
  210.     ACanvas.DrawFocusRect(Rect);
  211.   end;
  212. end;
  213.  
  214. function TSprite.HitTest(APoint:TPoint):Boolean;
  215. begin
  216.   Result := False;
  217.   if Rect.Contains(APoint) then begin
  218.     APoint := APoint - Rect.TopLeft;
  219.     Result := Bitmap.Canvas.Pixels[APoint.X, APoint.Y] <> clBlack;
  220.   end;
  221. end;
  222.  
  223. end.
  224.  
Controls:
1) Mouse move - topmost sprite is highlighted
2) Left mouse click - bring sprite under cursor to front
3) Left click and drag >5 pixels - drag sprite under cursor
4) Right click - add new random sprite at mouse cursor

Clipping isn't supported, so drawing isn't as optimal, as I wanted it to be.  :( I wanted to implement explicit clipping, but canvas doesn't support it and now I don't have time to change my code.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 15, 2017, 04:21:24 pm
I will see if I can extend our test demo later on Handoko because if I am able to do it myself (with the help already provided) I will understand the concept and everything a bit more better.

Mr.Madguy thanks for contributing, I will use your pointers to also assist me and I will check out your solution later on.

Thanks all :)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 15, 2017, 04:24:57 pm
@Mr.Madguy

Your code works. You're really a master in graphics programming. I found a lot of new things to learn inside your code.

Thank you.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 15, 2017, 04:31:48 pm
There are a lot of things we can learn from Mr.Madguy's code. I managed to make it works but with some modification to load 32 x 32 image that I found from here:

https://opengameart.org/content/dungeon-crawl-32x32-tiles

Anyone who want to try the code, can download it here (the spritesheet image is included).

Controls:
1) Mouse move - topmost sprite is highlighted
2) Left mouse click - bring sprite under cursor to front
3) Left click and drag >5 pixels - drag sprite under cursor
4) Right click - add new random sprite at mouse cursor
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 15, 2017, 05:37:17 pm
Code is actually very clunky, because I had no time to create new control from scratch. In real situation new control should be created and all painting should be performed in it's Paint method. All updating should be performed indirectly - via invalidating regions. In this case Windows will handle all visibility detection. Another bad thing - Canvas supports implicit clipping only, i.e. setting ClipRect has no effect (and in Delphi it's documented as read-only property). I.e. no such things, as IntersectClipRect. That's why I have to update whole picture every time - I tried to use smart updating, but order of sprites was sometimes braking due to lack of explicit clipping. Whole sprites were update instead of only needed parts of them. As the result - some other sprites were affected too.

Graphics - is actually my specialization. Here is my best program: SmileManager (http://my-files.ru/ske0yl). It uses my own component, that can animate unlimited (limited by amount of RAM only) amount of GIF files. I wanted to use idea of this component to create universal control framework, but it was too complex task and I gave up at some point...  %) But you should know, that it's very old - it was designed for WinXP, but actually aimed at still supporting Win9x.  ::) And due to this fact some things are broken on Vista+. For example "Add smile" dialog doesn't work. You need to drag GIF files from explorer directly to program window. Also quality of my code wasn't as good, as it's now - it was very clunky and ugly.  :D
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 15, 2017, 06:39:15 pm
You used generic, which is the thing I haven't try. I know how to do BoundingBox test but I haven't known how to solve the alpha issue. In just hours, you can write a working code that solves several advanced problems. I salute you.

I can start your SmileManager on Linux. :D
Unfortunately nothing more it can do. The add smile feature didn't work and the drag gif file didn't work too. You built it in 2005? Wow, you now must be a very experienced programmer.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 15, 2017, 07:07:39 pm
You used generic, which is the thing I haven't try. I know how to do BoundingBox test but I haven't known how to solve the alpha issue. In just hours, you can write a working code that solves several advanced problems. I salute you.

I can start your SmileManager on Linux. :D
Unfortunately nothing more it can do. The add smile feature didn't work and the drag gif file didn't work too. You built it in 2005? Wow, you now must be a very experienced programmer.
No, 2005 - is version of QIP (http://forum.qip.ru/). Program was build around 2007, if I remember correctly.

Strange. Add smile is broken - I know. It uses some crazy code to support both Unicode and Ansi in dialogs at the same time. It was time, when Unicode wasn't directly supported by Delphi, so some other ways of achieving this goal, like using TNT controls, should have been used. Does Win emulator on Linux support WM_DROPFILES? Does it have some sort of Win9x/WinXP compatibility mode? Unfortunately I can't recompile this program to fix bugs with dialogs, cuz some controls are missing.

Code is old, but it's very good. It's actually multithreaded. It uses 3 threads: timer thread, render thread and main thread. Due to this reason component should be very responsive even under high loads and on very slow machines.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 15, 2017, 07:41:45 pm
Ahhh. I've found test utility for this component. Here it is. (http://my-files.ru/nh4qlw) Unfortunately one of crappy anti-viruses on VirusTotal treats one of GIF files as OMG virus  :o, so I can't send it to you with my smile pack, cuz file hosting deletes my file in this case. You need to download it separately. Here for example. (http://forum.qip.ru/showthread.php?t=35295) Then copy contents of Animated folder to MyChat\Smilies folder of my program. Don't change structure of folders - it's hardcoded. Does Win emulator on Linux support "Layered windows" feature (implemented in XP)? Without it you won't see, how cool animated drag and drop feature works.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 16, 2017, 01:09:49 am
Very impressive Mr.Madguy :)

I tried doing this myself but could not come up with anything as good as yours, graphics programming is not a strong point of mine but I can look at your code and gain some understanding from it.

There is a slow down with lots of sprites and then moving but you have already explained this. I'm curious as to how it could be optimised, the old Game Maker editor seems to pull it off with hundreds of sprites on screen and moving one causes no slowdown, and that is done using the internal bitmap canvas of a TImage. I wonder how it was able to efficiently do that?
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 16, 2017, 06:59:15 am
Very impressive Mr.Madguy :)

I tried doing this myself but could not come up with anything as good as yours, graphics programming is not a strong point of mine but I can look at your code and gain some understanding from it.

There is a slow down with lots of sprites and then moving but you have already explained this. I'm curious as to how it could be optimised, the old Game Maker editor seems to pull it off with hundreds of sprites on screen and moving one causes no slowdown, and that is done using the internal bitmap canvas of a TImage. I wonder how it was able to efficiently do that?
Yeah, that's because I can't update just one sprite. When I do it - I also need to update all sprites, visible in this region. But they can't be clipped and therefore their order against sprites, not visible in this region, also breaks. But I actually know, how to solve this problem. If I won't be busy today, I'll improve my program a little bit. May be even implement same algorithm via new control.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 16, 2017, 10:23:06 am
Yeah, here it is. That's how it should be implemented in real situation. Good thing - implicit clipping is now supported. Bad thing - I've found out, that not only explicit clipping isn't supported by Canvas, but partial invalidation isn't supported either, so it's dead lock - WinAPI InvalidateRect has to be used to achieve desired performance. That means - no optimization for Linux.  :'( It's still possible to implement partial updating on Linux, but it has to be performed manually.

Hit detection is still "raw", i.e. designed for worst case scenario, where all sprites can have arbitrary positions. In order to further improve performance, hit detection should take specifics of specific task into account, such as whether some sprites can overlap (foreground sprites) or not (background ones). Also, if you have enough memory, you can use the same technique, that is used in modern 3D games - selection buffer.

Main program:
Code: Pascal  [Select][+][-]
  1. unit TestMain;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, SpriteControl;
  9.  
  10. type
  11.  
  12.   { TSpriteTestForm }
  13.  
  14.   TSpriteTestForm = class(TForm)
  15.     procedure FormCreate(Sender: TObject);
  16.     procedure FormDestroy(Sender: TObject);
  17.     procedure SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  18.       Shift: TShiftState; X, Y: Integer);
  19.   private
  20.     { private declarations }
  21.   public
  22.     { public declarations }
  23.     SpriteControl:TSpriteControl;
  24.     SpriteBitmap:TBitmap;
  25.     function CreateSprite(AIndex:Integer):TBitmap;
  26.   end;
  27.  
  28. var
  29.   SpriteTestForm: TSpriteTestForm;
  30.  
  31. implementation
  32.  
  33. {$R *.lfm}
  34.  
  35. { TSpriteTestForm }
  36.  
  37. procedure TSpriteTestForm.FormCreate(Sender: TObject);
  38. begin
  39.   Randomize;
  40.   SpriteBitmap := TBitmap.Create;
  41.   SpriteBitmap.LoadFromFile('Sprites.bmp');
  42.   SpriteControl := TSpriteControl.Create(Self);
  43.   with SpriteControl do begin
  44.     Parent := Self;
  45.     OnMouseUp := @SpriteControlMouseUp;
  46.     Align := alClient;
  47.     Bitmap.Width := 640;
  48.     Bitmap.Height := 480;
  49.     BackgroundColor := clGreen;
  50.   end;
  51. end;
  52.  
  53. procedure TSpriteTestForm.FormDestroy(Sender: TObject);
  54. begin
  55.   SpriteBitmap.Free;
  56. end;
  57.  
  58. procedure TSpriteTestForm.SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  59.   Shift: TShiftState; X, Y: Integer);
  60. begin
  61.   if Button = mbRight then begin
  62.     SpriteControl.AddSprite(TSprite.Create(CreateSprite(Random(12 * 8 - 1)), TPoint.Create(X - 40, Y - 40)));
  63.   end;
  64. end;
  65.  
  66. function TSpriteTestForm.CreateSprite(AIndex:Integer):TBitmap;
  67.   var Src, Dest:TRect;
  68. begin
  69.   Result := nil;
  70.   if (AIndex >= 0) and (AIndex < 12 * 8) then begin
  71.     Result := TBitmap.Create;
  72.     with Result do begin
  73.       Width := 81;
  74.       Height := 81;
  75.       Dest := TRect.Create(TPoint.Create(0, 0), 81, 81);
  76.       Src := Dest;
  77.       Src.Offset(5 + 81 * (AIndex mod 12), 5 + 81 * (AIndex div 12));
  78.       Canvas.CopyRect(Dest, SpriteBitmap.Canvas, Src);
  79.       Transparent := True;
  80.       TransparentColor := clBlack;
  81.       Mask(clBlack);
  82.     end;
  83.   end;
  84. end;
  85.  
  86. end.
  87.  

And sprite control:
Code: Pascal  [Select][+][-]
  1. unit SpriteControl;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses {$ifdef windows}Windows,{$endif} Classes, Controls, Graphics, Fgl;
  8.  
  9. type
  10.   TSprite = class
  11.   protected
  12.     FBitmap:TBitmap;
  13.   public
  14.     Rect:TRect;
  15.     constructor Create(ABitmap:TBitmap;ACoord:TPoint);
  16.     destructor Destroy;override;
  17.     procedure Draw(ACanvas:TCanvas;ARect:TRect);
  18.     procedure Move(APoint:TPoint);
  19.     function HitTest(APoint:TPoint):Boolean;
  20.     property Bitmap:TBitmap read FBitmap;
  21.   end;
  22.  
  23.   TSpriteList = specialize TFPGObjectList<TSprite>;
  24.  
  25.   TSpriteControl = class(TCustomControl)
  26.   protected
  27.     FBitmap:TBitmap;
  28.     FSprites:TSpriteList;
  29.     FSpriteUnderCursor:TSprite;
  30.     FUpdateCount:Integer;
  31.     FClickedSprite:TSprite;
  32.     FClickedPoint:TPoint;
  33.     FClickedCoord:TPoint;
  34.     FDragging:Boolean;
  35.     procedure Paint;override;
  36.     procedure MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  37.     procedure MouseMove(Shift: TShiftState; X,Y: Integer);override;
  38.     procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  39.     procedure SetSpriteUnderCursor(ASprite:TSprite);
  40.   public
  41.     BackgroundColor:TColor;
  42.     constructor Create(AOwner: TComponent);override;
  43.     destructor Destroy;override;
  44.     procedure AddSprite(ASprite:TSprite);
  45.     procedure UpdateSprite(ASprite:TSprite);
  46.     property Bitmap:TBitmap read FBitmap;
  47.     property Sprites:TSpriteList read FSprites;
  48.     property SpriteUnderCursor:TSprite read FSpriteUnderCursor write SetSpriteUnderCursor;
  49.   end;
  50.  
  51.  
  52. implementation
  53.  
  54. uses SysUtils;
  55.  
  56. constructor TSprite.Create(ABitmap:TBitmap;ACoord:TPoint);
  57. begin
  58.   inherited Create;
  59.   FBitmap := ABitmap;
  60.   Rect := TRect.Create(ACoord, Bitmap.Width, Bitmap.Height);
  61. end;
  62.  
  63. destructor TSprite.Destroy;
  64. begin
  65.   FBitmap.Free;
  66.   inherited Destroy;
  67. end;
  68.  
  69. procedure TSprite.Draw(ACanvas:TCanvas;ARect:TRect);
  70. begin
  71.   if not TRect.Intersect(ARect, Rect).IsEmpty then begin
  72.     ACanvas.Draw(Rect.Left, Rect.Top, Bitmap);
  73.   end;
  74. end;
  75.  
  76. procedure TSprite.Move(APoint:TPoint);
  77. begin
  78.   Rect := TRect.Create(APoint, Bitmap.Width, Bitmap.Height);
  79. end;
  80.  
  81. function TSprite.HitTest(APoint:TPoint):Boolean;
  82. begin
  83.   Result := Rect.Contains(APoint);
  84.   if Result and Bitmap.Transparent then begin
  85.     APoint := APoint - Rect.TopLeft;
  86.     Result := Bitmap.Canvas.Pixels[APoint.X, APoint.Y] <> Bitmap.TransparentColor;
  87.   end;
  88. end;
  89.  
  90. constructor TSpriteControl.Create(AOwner: TComponent);
  91. begin
  92.   inherited Create(AOwner);
  93.   FBitmap := TBitmap.Create;
  94.   FSprites := TSpriteList.Create;
  95.   FUpdateCount := 1;
  96. end;
  97.  
  98. destructor TSpriteControl.Destroy;
  99. begin
  100.   FBitmap.Free;
  101.   FSprites.Free;
  102.   inherited Destroy;
  103. end;
  104.  
  105. procedure TSpriteControl.AddSprite(ASprite:TSprite);
  106. begin
  107.   Sprites.Insert(0, ASprite);
  108.   UpdateSprite(ASprite);
  109. end;
  110.  
  111. procedure TSpriteControl.UpdateSprite(ASprite:TSprite);
  112. begin
  113.   {$ifdef windows}
  114.     InvalidateRect(Handle, ASprite.Rect, False);
  115.   {$else}
  116.     Invalidate;
  117.   {$endif}
  118.   Inc(FUpdateCount);
  119. end;
  120.  
  121. procedure TSpriteControl.Paint;
  122.   var Rect, ClipRect, PaintRect:TRect;
  123.   I:Integer;Sprite:TSprite;
  124.   FocusSprite:TSprite;
  125. begin
  126.   inherited Paint;
  127.   Rect := TRect.Create(TPoint.Create(0, 0), Bitmap.Width, Bitmap.Height);
  128.   ClipRect := Canvas.ClipRect;
  129.   PaintRect := Rect * ClipRect;
  130.   if FUpdateCount > 0 then begin
  131.     with Bitmap.Canvas do begin
  132.       Brush.Color := BackgroundColor;
  133.       FillRect(PaintRect);
  134.     end;
  135.     FocusSprite := nil;
  136.     for I := Sprites.Count - 1 downto 0 do begin
  137.       Sprite := Sprites[I];
  138.       Sprite.Draw(Bitmap.Canvas, PaintRect);
  139.       if Sprite = SpriteUnderCursor then begin
  140.         FocusSprite := Sprite;
  141.       end;
  142.     end;
  143.     if Assigned(FocusSprite) then begin
  144.       with Bitmap.Canvas do begin
  145.         DrawFocusRect(FocusSprite.Rect);
  146.       end;
  147.     end;
  148.     FUpdateCount := 0;
  149.   end;
  150.   Canvas.CopyRect(PaintRect, Bitmap.Canvas, PaintRect);
  151.   if not PaintRect.Contains(ClipRect) then begin
  152.     Canvas.Brush.Color := Color;
  153.     Rect := ClientRect;
  154.     Rect.Left := Bitmap.Width;
  155.     Rect.Intersect(ClipRect);
  156.     Canvas.FillRect(Rect);
  157.     Rect := ClientRect;
  158.     Rect.Top := Bitmap.Height;
  159.     Rect.Width := Bitmap.Width;
  160.     Rect.Intersect(ClipRect);
  161.     Canvas.FillRect(Rect);
  162.   end;
  163. end;
  164.  
  165. procedure TSpriteControl.MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  166.   var I:Integer;Sprite, HitSprite:TSprite;
  167. begin
  168.   inherited MouseDown(Button, Shift, X, Y);
  169.   if Button = mbLeft then begin
  170.     HitSprite := nil;
  171.     for I := 0 to Sprites.Count - 1 do begin
  172.       Sprite := Sprites[I];
  173.       if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  174.         HitSprite := Sprite;
  175.         FClickedPoint := TPoint.Create(X, Y);
  176.         FClickedCoord := Sprite.Rect.TopLeft;
  177.         Break;
  178.       end;
  179.     end;
  180.     FClickedSprite := HitSprite;
  181.   end;
  182. end;
  183.  
  184. procedure TSpriteControl.MouseMove(Shift: TShiftState; X,Y: Integer);
  185.   var I:Integer;Sprite, HitSprite:TSprite;
  186. begin
  187.   inherited MouseMove(Shift, X, Y);
  188.   if Assigned(FClickedSprite) then begin
  189.     if not FDragging then begin
  190.       if (Abs(X - FClickedPoint.X) > 5) or (Abs(Y - FClickedPoint.Y) > 5) then begin
  191.         FDragging := True;
  192.       end;
  193.     end;
  194.     if FDragging then begin
  195.       UpdateSprite(FClickedSprite);
  196.       FClickedSprite.Move(FClickedCoord + TPoint.Create(X, Y) - FClickedPoint);
  197.       UpdateSprite(FClickedSprite);
  198.       SpriteUnderCursor := FClickedSprite;
  199.       Exit;
  200.     end;
  201.   end;
  202.   HitSprite := nil;
  203.   for I := 0 to Sprites.Count - 1 do begin
  204.     Sprite := Sprites[I];
  205.     if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  206.       HitSprite := Sprite;
  207.     end;
  208.   end;
  209.   SpriteUnderCursor := HitSprite;
  210. end;
  211.  
  212. procedure TSpriteControl.MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  213. begin
  214.   inherited MouseUp(Button, Shift, X, Y);
  215.   if Button = mbLeft then begin
  216.     if FDragging then begin
  217.       FDragging := False;
  218.     end
  219.     else begin
  220.       if Assigned(FClickedSprite) then begin
  221.         Sprites.Move(Sprites.IndexOf(FClickedSprite), 0);
  222.         UpdateSprite(FClickedSprite);
  223.       end;
  224.     end;
  225.     FClickedSprite := nil;
  226.   end;
  227. end;
  228.  
  229. procedure TSpriteControl.SetSpriteUnderCursor(ASprite:TSprite);
  230.   var OldSprite:TSprite;
  231. begin
  232.   if SpriteUnderCursor <> ASprite then begin
  233.     OldSprite := SpriteUnderCursor;
  234.     FSpriteUnderCursor := ASprite;
  235.     if Assigned(OldSprite) then begin
  236.       UpdateSprite(OldSprite);
  237.     end;
  238.     if Assigned(SpriteUnderCursor) then begin
  239.       UpdateSprite(SpriteUnderCursor);
  240.     end;
  241.   end;
  242. end;
  243.  
  244. end.
  245.  
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 16, 2017, 03:15:38 pm
Excellent work Mr.Madguy  ;D 8-)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 16, 2017, 03:54:58 pm
There was a time, when I was really interested in gamedev. And games - are even more complex, cuz there you have to also deal with timers, animations, controls, sounds and game logic. I even tried to make some games via GDI, but then realized, that it's way to slow for it.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 16, 2017, 04:09:15 pm
Yes GDI is not great for graphics and game performance, hardware acceleration is the way to go, but this is why I was curious as to how Game Maker could draw and move sprites rather fast on a TImage canvas and of course not disturb the main image, I assume your techninque is not too different to how Game Maker achieved it?
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Handoko on May 16, 2017, 04:40:02 pm
I think those non-hardware accelerated 2D games use their own graphics engine. Those classic 2D game developers should had developed their own graphics library before OpenGL/DirectX become famous.

Have you consider about BGRABitmap or Graphics32? Here I quote their description:

Quote
BGRABitmap is a set of units designed to modify and create images with transparency (alpha channel). Direct pixel access allows fast image processing.

Quote
Graphics32 is a graphics library for Delphi and Lazarus. Optimized for 32-bit pixel formats, it provides fast operations with pixels and graphic primitives. In most cases Graphics32 considerably outperforms the standard TBitmap/TCanvas methods.

They both mentioned using fast/direct pixel access.

I wrote simple games for my 8088 machine. Using BIOS graphics functions were too slow. So I wrote my own graphics module using direct mapping to the video buffer. Not very fast (because that cpu is extremely slow) but much better than Turbo Basic default drawing functions and BIOS graphics functions.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 16, 2017, 06:29:01 pm
Yes GDI is not great for graphics and game performance, hardware acceleration is the way to go, but this is why I was curious as to how Game Maker could draw and move sprites rather fast on a TImage canvas and of course not disturb the main image, I assume your techninque is not too different to how Game Maker achieved it?
Only consoles, like NES, had hardware sprite support. I.e. when you could move sprites completely independently from background. All, you needed to do - to set up some PPU registers. No manual drawing at all. That's why consoles with 8bit 1Mhz processors had better games, than IBM PC. Because CPU driven graphics on 4-plane 1 bit per pixel EGA video adapters - was just complete nightmare.

And since then all graphic libraries, be it GDI, DirectDraw, Direct3D, OpenGL or even simply direct writing to video memory - work exactly the same way. What way? You have to redraw whole picture from scratch every time, you need to update it. And as every new sprite overwrites all previous data - you have to do it in back to front order. Remember Paint? PhotoShop or any other raster image editor? Any graphic application works exactly the same way. That's it. The only difference between different methods - is how you optimize this process to minimize redrawing to areas, that really need it.
I think those non-hardware accelerated 2D games use their own graphics engine. Those classic 2D game developers should had developed their own graphics library before OpenGL/DirectX become famous.

Have you consider about BGRABitmap or Graphics32? Here I quote their description:

Quote
BGRABitmap is a set of units designed to modify and create images with transparency (alpha channel). Direct pixel access allows fast image processing.

Quote
Graphics32 is a graphics library for Delphi and Lazarus. Optimized for 32-bit pixel formats, it provides fast operations with pixels and graphic primitives. In most cases Graphics32 considerably outperforms the standard TBitmap/TCanvas methods.

They both mentioned using fast/direct pixel access.

I wrote simple games for my 8088 machine. Using BIOS graphics functions were too slow. So I wrote my own graphics module using direct mapping to the video buffer. Not very fast (because that cpu is extremely slow) but much better than Turbo Basic default drawing functions and BIOS graphics functions.
I guess, this two libraries have software image processing? Mmm. You should understand, that software image processing isn't worth it. In all areas, where software image processing is faster, than GDI - it's better to use graphic libraries, like Direct3D or OpenGL.

GDI - is actually very high level abstraction layer around graphic driver. GDI is hardware accelerated. Bitmaps are stored in video memory for example. It's actually good for it's intended task - for UI. It's so slow just because it was designed with trying to be as hardware independent, as possible, in mind. Because, as you know, higher abstraction level is - higher it's overhead cost and therefore performance penalty. Remember Java?

Yeah. BIOS functions were slow just because they were altering just one bit in video memory (4-plane 1bpp EGA for example), but had to calculate it's address, bitmask to access it and set up video adapter registers for every single pixel. My library had to overcome this problem too. But still - horizontal moving of sprites and horizontal scrolling were limited to 8-pixel blocks only. It's actually very noticeable in some old DOS games - that horizontal scrolling is quantized.

I haven't actually finished this game - I wasn't experienced enough to implement V-Sync back then. Without it game was flickering when pages were switched and copying pages was way to slow - it was taking whole CPU time. And after trying to develop games for NES... NES is heaven for game development. Just heaven. My DOS game program is very big and complex - memory manager, file manager, graphic library, timer IRQ, keyboard IRQ, etc. And this small C program for NES achieves exactly the same goal + much easier animation, music and sound. It took two days since downloading NES dev tools till achieving same and even better functionality of my program on NES (shooting wasn't implemented in DOS version). Yeah, it uses some runtime library, but this library - is only thin layer around accessing CPU, PPU and APU registers directly.

P.S. I actually was trying to make NES Battle City (https://leopoldbloom.files.wordpress.com/2007/07/battle-city.jpg)-like game with graphics, similar to Z game (http://zzone.lewe.com/images/screens/igv03.gif). I was a kid back then and I really loved this two games and wanted to make something similar.
Code: C  [Select][+][-]
  1. #include "neslib.h"
  2.  
  3. static unsigned char pal, pad, spr, dir, state, pressed, shell;
  4. static unsigned char tank_x;
  5. static unsigned char tank_y;
  6. static unsigned char shell_x;
  7. static unsigned char shell_y;
  8.  
  9. static unsigned char palSprites[16]={
  10.         0x0f,0x00,0x10,0x20,
  11.         0x0f,0x01,0x11,0x21,
  12.         0x0f,0x2A,0x10,0x20,
  13.         0x0f,0x2A,0x10,0x20
  14. };
  15.  
  16. const unsigned char metaTank[4][4*9+1]=
  17. {
  18.         {
  19.                 0,      0,      0x00,   0,
  20.                 8,      0,      0x01,   0,
  21.                 16,     0,      0x02,   0,
  22.                 0,      8,      0x10,   0,
  23.                 8,      8,      0x11,   0,
  24.                 16,     8,      0x12,   0,
  25.                 0,      16,     0x20,   0,
  26.                 8,      16,     0x21,   0,
  27.                 16,     16,     0x22,   0,
  28.                 128
  29.         },
  30.         {
  31.                 0,      0,      0x03,   0,
  32.                 8,      0,      0x04,   0,
  33.                 16,     0,      0x05,   0,
  34.                 0,      8,      0x13,   0,
  35.                 8,      8,      0x14,   0,
  36.                 16,     8,      0x15,   0,
  37.                 0,      16,     0x23,   0,
  38.                 8,      16,     0x24,   0,
  39.                 16,     16,     0x25,   0,
  40.                 128
  41.         },
  42.         {
  43.                 0,      0,      0x06,   0,
  44.                 8,      0,      0x07,   0,
  45.                 16,     0,      0x08,   0,
  46.                 0,      8,      0x16,   0,
  47.                 8,      8,      0x17,   0,
  48.                 16,     8,      0x18,   0,
  49.                 0,      16,     0x26,   0,
  50.                 8,      16,     0x27,   0,
  51.                 16,     16,     0x28,   0,
  52.                 128
  53.         },
  54.         {
  55.                 0,      0,      0x09,   0,
  56.                 8,      0,      0x0A,   0,
  57.                 16,     0,      0x0B,   0,
  58.                 0,      8,      0x19,   0,
  59.                 8,      8,      0x1A,   0,
  60.                 16,     8,      0x1B,   0,
  61.                 0,      16,     0x29,   0,
  62.                 8,      16,     0x2A,   0,
  63.                 16,     16,     0x2B,   0,
  64.                 128
  65.         }
  66. };
  67.  
  68. const unsigned char metaStripe[4][4*9+1]=
  69. {
  70.         {
  71.                 0,      0,      0x30,   1,
  72.                 8,      0,      0x31,   1,
  73.                 16,     0,      0x32,   1,
  74.                 0,      8,      0x40,   1,
  75.                 8,      8,      0x41,   1,
  76.                 16,     8,      0x42,   1,
  77.                 0,      16,     0x50,   1,
  78.                 8,      16,     0x51,   1,
  79.                 16,     16,     0x52,   1,
  80.                 128
  81.         },
  82.         {
  83.                 0,      0,      0x33,   1,
  84.                 8,      0,      0x34,   1,
  85.                 16,     0,      0x35,   1,
  86.                 0,      8,      0x43,   1,
  87.                 8,      8,      0x44,   1,
  88.                 16,     8,      0x45,   1,
  89.                 0,      16,     0x53,   1,
  90.                 8,      16,     0x54,   1,
  91.                 16,     16,     0x55,   1,
  92.                 128
  93.         },
  94.         {
  95.                 0,      0,      0x36,   1,
  96.                 8,      0,      0x37,   1,
  97.                 16,     0,      0x38,   1,
  98.                 0,      8,      0x46,   1,
  99.                 8,      8,      0x47,   1,
  100.                 16,     8,      0x48,   1,
  101.                 0,      16,     0x56,   1,
  102.                 8,      16,     0x57,   1,
  103.                 16,     16,     0x58,   1,
  104.                 128
  105.         },
  106.         {
  107.                 0,      0,      0x39,   1,
  108.                 8,      0,      0x3A,   1,
  109.                 16,     0,      0x3B,   1,
  110.                 0,      8,      0x49,   1,
  111.                 8,      8,      0x4A,   1,
  112.                 16,     8,      0x4B,   1,
  113.                 0,      16,     0x59,   1,
  114.                 8,      16,     0x5A,   1,
  115.                 16,     16,     0x5B,   1,
  116.                 128
  117.         }
  118. };
  119.  
  120. void update_pal(void)
  121. {
  122.         palSprites[5] = pal;
  123.         palSprites[6] = pal + 0x10;
  124.         palSprites[7] = pal + 0x20;
  125.         pal_spr(palSprites);
  126. };
  127.  
  128. void main(void)
  129. {
  130.         pal = 1;
  131.         update_pal();
  132.         pal_bg(palSprites);
  133.        
  134.         vram_adr(NAMETABLE_A);
  135.         vram_fill(0x0F, 30 * 32);
  136.         vram_fill(0xAA, 8 * 8);
  137.                
  138.         ppu_on_all();
  139.        
  140.         music_play(0);
  141.        
  142.         pressed = 0;
  143.         dir = 0;
  144.         tank_x = 122;
  145.         tank_y = 207;
  146.         shell = 4;
  147.  
  148.         while(1)
  149.         {      
  150.                 ppu_wait_frame();
  151.                
  152.                 spr = 0;
  153.                
  154.                 oam_clear();
  155.                
  156.                 if (shell < 4) {
  157.                         if ((shell == 1 && shell_x >= 232) || (shell == 3 && shell_x <= 24)) {
  158.                                 shell = 4;
  159.                         };
  160.                         if (shell < 4) {
  161.                                 spr = oam_spr(shell_x, shell_y, 0x70 + shell, 1, spr);
  162.                                 spr = oam_spr(shell_x, shell_y, 0x60 + shell, 0, spr);
  163.                         };
  164.                         switch (shell) {
  165.                                 case 0:
  166.                                         (shell_y > 7 && shell_y < 207)?shell_y-=8:shell = 4;
  167.                                         break;
  168.                                 case 1:
  169.                                         (shell_x > 0 && shell_x < 232)?shell_x-=8:shell = 4;
  170.                                         break;
  171.                                 case 2:
  172.                                         (shell_y < 223 && shell_y > 24)?shell_y+=8:shell = 4;
  173.                                         break;
  174.                                 case 3:
  175.                                         (shell_x < 248 && shell_x > 24)?shell_x+=8:shell = 4;
  176.                                         break;
  177.                         }
  178.                 };
  179.                
  180.                 spr = oam_meta_spr(tank_x, tank_y, spr, metaStripe[dir]);      
  181.                 spr = oam_meta_spr(tank_x, tank_y, spr, metaTank[dir]);
  182.                
  183.                 pad = pad_poll(0);
  184.                
  185.                 state = 4;
  186.  
  187.                 if (pad & PAD_LEFT) {
  188.             state == 4?state = 1:state = 5;                    
  189.                 }
  190.                 if (pad & PAD_RIGHT) {
  191.             state == 4?state = 3:state = 5;
  192.                 }
  193.                 if (pad & PAD_UP) {
  194.             state == 4?state = 0:state = 5;
  195.                 }
  196.                 if (pad & PAD_DOWN) {
  197.             state == 4?state = 2:state = 5;
  198.                 }
  199.                 if (state < 4 && dir != state) {
  200.                         sfx_play(3, 3);
  201.                         dir = state;
  202.                 }
  203.                 switch (state) {
  204.                         case 0:
  205.                                 tank_y > 7?tank_y--:state = 6;
  206.                                 break;
  207.                         case 1:
  208.                                 tank_x > 0?tank_x--:state = 6;
  209.                                 break;
  210.                         case 2:
  211.                                 tank_y < 207?tank_y++:state = 6;
  212.                                 break;
  213.                         case 3:
  214.                                 tank_x < 232?tank_x++:state = 6;
  215.                                 break;
  216.                 };     
  217.                 if (state == 6) sfx_play(0, 0);
  218.                 if (pad & PAD_A) {
  219.                         if (!(pressed & 0x01)) {
  220.                                 sfx_play(2, 2);
  221.                                 pal++;
  222.                                 if ((pal & 0x0F) == 0x0D) {
  223.                                         pal += 2;
  224.                                 } else {
  225.                                         pal &= 0x1F;
  226.                                 };
  227.                                 update_pal();
  228.                                 pressed |= 0x01;
  229.                         }
  230.                 } else {
  231.                         pressed &= 0xFE;
  232.                 };
  233.                 if (pad & PAD_B) {
  234.                         if (!(pressed & 0x02)) {
  235.                                 if (shell == 4) {
  236.                                         sfx_play(1, 1);
  237.                                         shell = dir;                           
  238.                                         switch (shell) {
  239.                                                 case 0:
  240.                                                         shell_x = tank_x + 8;
  241.                                                         shell_y = tank_y - 8;
  242.                                                         break;
  243.                                                 case 1:
  244.                                                         shell_x = tank_x - 8;
  245.                                                         shell_y = tank_y + 8;
  246.                                                         break;
  247.                                                 case 2:
  248.                                                         shell_x = tank_x + 8;
  249.                                                         shell_y = tank_y + 24;
  250.                                                         break;
  251.                                                 case 3:
  252.                                                         shell_x = tank_x + 24;
  253.                                                         shell_y = tank_y + 8;
  254.                                                         break;
  255.                                         };     
  256.                                 };
  257.                                 pressed |= 0x02;
  258.                         };
  259.                 } else {
  260.                         pressed &= 0xFD;
  261.                 }
  262.         }
  263. }
  264.  
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 17, 2017, 12:08:28 am
That stuff is really cool :)

What did you use for writing your code in C? I've always been interested in learning the C family, i tried C# but don't like the .Net stuff and not compiling to native, and C++ is way beyond me.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 17, 2017, 07:37:24 am
That stuff is really cool :)

What did you use for writing your code in C? I've always been interested in learning the C family, i tried C# but don't like the .Net stuff and not compiling to native, and C++ is way beyond me.
I used cc65 (http://cc65.github.io/cc65/) and runtime libraries from shiru (http://shiru.untergrund.net/articles/programming_nes_games_in_c.htm).

Yeah, NES has many restrictions, that are removed in SNES/Sega. Just 3 colors (plus one global backdrop color) per 16x16 background tile. Just 3 colors per sprite. Just 4 palettes for background and 4 palettes for sprites at the same time. Just 64 8x8 sprites. Just 8 sprites max on one scan line. But it's still just heaven in comparison with EGA programming. Yeah, there is ton of info about EGA/VGA programming in Internet (I had no access to Internet back then, but I had this amazing book (https://im0-tub-ru.yandex.net/i?id=6501f19f5ab746ea6a6d1979c308a9b2&n=13), that covered literally everything about Intel processor architecture, all PC hardware, Asm programming and DOS), but there are no examples of real games. So I had to explore everything by myself. And some things, like V-Sync and flipping pages, aren't covered anywhere.

I.e. what do you have to do to scroll your level on PC? On modern computers you can redraw everything from scratch. Back in that old days CPUs were too weak to do it. You had to shift image via copying it. And even simple memory copying was too slow on my 12Mhz Intel 80286, just because you had to use movsb, that was twice slower, than movsw, just because video adapter latch register was just 8bit wide - it was taking whole CPU time in EGA 640x350 mode (that's why all old games usually used 320x200, besides of having compatibility with VGA 8bpp mode). And what do you have to do on NES? Just 5 instructions:
Code: [Select]
bit PPUSTATUS
lda cam_position_x
sta PPUSCROLL
lda cam_position_y
sta PPUSCROLL
I.e. you can do it almost for free - no CPU time is spent on it. Yeah, that's, how true hardware acceleration works.  8-)

P.S. This idea constantly comes into my mind. May be old versions of Game Maker were using software rendering? I.e. Game Maker was rendering to memory image and using GDI just to draw it to window? It's totally possible. If I won't be busy - I'll show you an example. There was a time, when I wanted to make cross-platform game, that would have one single exe, working both on DOS and Windows. This game should have used software rendering and should have used GDI only as "video driver". I haven't made game, but I've made 24bit BMP viewer with simple dithering support instead. I'm not at home now, so I don't have it with me, otherwise I would show it to you.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 17, 2017, 02:17:48 pm
What a great insight, you're clearly very talented and knowledgeable  8-)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 17, 2017, 11:48:24 pm
I was wondering how to also delete a sprite under the mouse as I couldn't work it out? %)

Also I think I noticed a bug, if lots of sprites are added very close together, when you resize the form the main bitmap changes and then just moving your mouse over the sprites again causes them to change again?
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 18, 2017, 08:42:20 am
I was wondering how to also delete a sprite under the mouse as I couldn't work it out? %)
It's obvious - the same way, you add it. You need to delete sprite from sprite list, update bitmap and make sure you no longer have any references to it (i.e. SpriteUnderCursor and FClickedSprite). Try doing it yourself first.

Also I think I noticed a bug, if lots of sprites are added very close together, when you resize the form the main bitmap changes and then just moving your mouse over the sprites again causes them to change again?
Errr. It's due to performance optimization, I've added in last version of my program - I wanted to make resizing faster. That's, what happens, when clipping doesn't work. That's because implicit clipping works for Canvas of control itself, but not for Bitmap. I've fixed it for Windows via adding explicit clipping to Bitmap too. But unfortunately, again, this feature has to be disabled on Linux - all code, related to FUpdateCount, has to be removed.  :'( Clipping is needed and as result we have all this problems with it, because I have to use Draw method of Canvas - not CopyRect. Masking doesn't work for CopyRect.

Code: Pascal  [Select][+][-]
  1. unit SpriteControl;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses {$ifdef windows}Windows,{$endif} Classes, Controls, Graphics, Fgl;
  8.  
  9. type
  10.   TSprite = class
  11.   protected
  12.     FBitmap:TBitmap;
  13.   public
  14.     Rect:TRect;
  15.     constructor Create(ABitmap:TBitmap;ACoord:TPoint);
  16.     destructor Destroy;override;
  17.     procedure Draw(ACanvas:TCanvas;ARect:TRect);
  18.     procedure Move(APoint:TPoint);
  19.     function HitTest(APoint:TPoint):Boolean;
  20.     property Bitmap:TBitmap read FBitmap;
  21.   end;
  22.  
  23.   TSpriteList = specialize TFPGObjectList<TSprite>;
  24.  
  25.   TSpriteControl = class(TCustomControl)
  26.   protected
  27.     FBitmap:TBitmap;
  28.     FSprites:TSpriteList;
  29.     FSpriteUnderCursor:TSprite;
  30.     {$ifdef windows}
  31.     FNeedUpdate:Boolean;
  32.     {$endif}
  33.     FClickedSprite:TSprite;
  34.     FClickedPoint:TPoint;
  35.     FClickedCoord:TPoint;
  36.     FDragging:Boolean;
  37.     procedure Paint;override;
  38.     procedure MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  39.     procedure MouseMove(Shift: TShiftState; X,Y: Integer);override;
  40.     procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  41.     procedure SetSpriteUnderCursor(ASprite:TSprite);
  42.   public
  43.     BackgroundColor:TColor;
  44.     constructor Create(AOwner: TComponent);override;
  45.     destructor Destroy;override;
  46.     procedure AddSprite(ASprite:TSprite);
  47.     procedure UpdateSprite(ASprite:TSprite);
  48.     property Bitmap:TBitmap read FBitmap;
  49.     property Sprites:TSpriteList read FSprites;
  50.     property SpriteUnderCursor:TSprite read FSpriteUnderCursor write SetSpriteUnderCursor;
  51.   end;
  52.  
  53.  
  54. implementation
  55.  
  56. uses SysUtils;
  57.  
  58. constructor TSprite.Create(ABitmap:TBitmap;ACoord:TPoint);
  59. begin
  60.   inherited Create;
  61.   FBitmap := ABitmap;
  62.   Rect := TRect.Create(ACoord, Bitmap.Width, Bitmap.Height);
  63. end;
  64.  
  65. destructor TSprite.Destroy;
  66. begin
  67.   FBitmap.Free;
  68.   inherited Destroy;
  69. end;
  70.  
  71. procedure TSprite.Draw(ACanvas:TCanvas;ARect:TRect);
  72. begin
  73.   if not (ARect * Rect).IsEmpty then begin
  74.     ACanvas.Draw(Rect.Left, Rect.Top, Bitmap);
  75.   end;
  76. end;
  77.  
  78. procedure TSprite.Move(APoint:TPoint);
  79. begin
  80.   Rect := TRect.Create(APoint, Bitmap.Width, Bitmap.Height);
  81. end;
  82.  
  83. function TSprite.HitTest(APoint:TPoint):Boolean;
  84. begin
  85.   Result := Rect.Contains(APoint);
  86.   if Result and Bitmap.Transparent then begin
  87.     APoint := APoint - Rect.TopLeft;
  88.     Result := Bitmap.Canvas.Pixels[APoint.X, APoint.Y] <> Bitmap.TransparentColor;
  89.   end;
  90. end;
  91.  
  92. constructor TSpriteControl.Create(AOwner: TComponent);
  93. begin
  94.   inherited Create(AOwner);
  95.   FBitmap := TBitmap.Create;
  96.   FSprites := TSpriteList.Create;
  97.   {$ifdef windows}
  98.   FNeedUpdate := True;
  99.   {$endif}
  100. end;
  101.  
  102. destructor TSpriteControl.Destroy;
  103. begin
  104.   FBitmap.Free;
  105.   FSprites.Free;
  106.   inherited Destroy;
  107. end;
  108.  
  109. procedure TSpriteControl.AddSprite(ASprite:TSprite);
  110. begin
  111.   Sprites.Insert(0, ASprite);
  112.   UpdateSprite(ASprite);
  113. end;
  114.  
  115. procedure TSpriteControl.UpdateSprite(ASprite:TSprite);
  116. begin
  117.   {$ifdef windows}
  118.     InvalidateRect(Handle, ASprite.Rect, False);
  119.     FNeedUpdate := True;
  120.   {$else}
  121.     Invalidate;
  122.   {$endif}
  123. end;
  124.  
  125. procedure TSpriteControl.Paint;
  126.   var Rect, ClipRect, PaintRect:TRect;
  127.   I:Integer;Sprite:TSprite;
  128.   FocusSprite:TSprite;
  129.   {$ifdef windows}
  130.   ClipRgn:HRGN;
  131.   {$endif}
  132. begin
  133.   inherited Paint;
  134.   Rect := TRect.Create(TPoint.Create(0, 0), Bitmap.Width, Bitmap.Height);
  135.   ClipRect := Canvas.ClipRect;
  136.   PaintRect := Rect * ClipRect;
  137.   {$ifdef windows}
  138.   if FNeedUpdate then begin
  139.   {$endif}
  140.     with Bitmap.Canvas do begin
  141.       {$ifdef windows}
  142.       ClipRgn := CreateRectRgn(PaintRect.Left, PaintRect.Top, PaintRect.Right, PaintRect.Bottom);
  143.       SelectClipRgn(Handle, ClipRgn);
  144.       {$endif}
  145.       Brush.Color := BackgroundColor;
  146.       FillRect(PaintRect);
  147.     end;
  148.     FocusSprite := nil;
  149.     for I := Sprites.Count - 1 downto 0 do begin
  150.       Sprite := Sprites[I];
  151.       Sprite.Draw(Bitmap.Canvas, PaintRect);
  152.       if Sprite = SpriteUnderCursor then begin
  153.         FocusSprite := Sprite;
  154.       end;
  155.     end;
  156.     if Assigned(FocusSprite) then begin
  157.       with Bitmap.Canvas do begin
  158.         DrawFocusRect(FocusSprite.Rect);
  159.       end;
  160.     end;
  161.   {$ifdef windows}
  162.     with Bitmap.Canvas do begin
  163.       SelectClipRgn(Handle, 0);
  164.       DeleteObject(ClipRgn);
  165.     end;
  166.     FNeedUpdate := False;
  167.   end;
  168.   {$endif}
  169.   Canvas.CopyRect(PaintRect, Bitmap.Canvas, PaintRect);
  170.   if not PaintRect.Contains(ClipRect) then begin
  171.     Canvas.Brush.Color := Color;
  172.     Rect := ClientRect;
  173.     Rect.Left := Bitmap.Width;
  174.     Rect.Intersect(ClipRect);
  175.     Canvas.FillRect(Rect);
  176.     Rect := ClientRect;
  177.     Rect.Top := Bitmap.Height;
  178.     Rect.Width := Bitmap.Width;
  179.     Rect.Intersect(ClipRect);
  180.     Canvas.FillRect(Rect);
  181.   end;
  182. end;
  183.  
  184. procedure TSpriteControl.MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  185.   var I:Integer;Sprite, HitSprite:TSprite;
  186. begin
  187.   inherited MouseDown(Button, Shift, X, Y);
  188.   if Button = mbLeft then begin
  189.     HitSprite := nil;
  190.     for I := 0 to Sprites.Count - 1 do begin
  191.       Sprite := Sprites[I];
  192.       if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  193.         HitSprite := Sprite;
  194.         FClickedPoint := TPoint.Create(X, Y);
  195.         FClickedCoord := Sprite.Rect.TopLeft;
  196.         Break;
  197.       end;
  198.     end;
  199.     FClickedSprite := HitSprite;
  200.   end;
  201. end;
  202.  
  203. procedure TSpriteControl.MouseMove(Shift: TShiftState; X,Y: Integer);
  204.   var I:Integer;Sprite, HitSprite:TSprite;
  205. begin
  206.   inherited MouseMove(Shift, X, Y);
  207.   if Assigned(FClickedSprite) then begin
  208.     if not FDragging then begin
  209.       if (Abs(X - FClickedPoint.X) > 5) or (Abs(Y - FClickedPoint.Y) > 5) then begin
  210.         FDragging := True;
  211.       end;
  212.     end;
  213.     if FDragging then begin
  214.       UpdateSprite(FClickedSprite);
  215.       FClickedSprite.Move(FClickedCoord + TPoint.Create(X, Y) - FClickedPoint);
  216.       UpdateSprite(FClickedSprite);
  217.       SpriteUnderCursor := FClickedSprite;
  218.       Exit;
  219.     end;
  220.   end;
  221.   HitSprite := nil;
  222.   for I := 0 to Sprites.Count - 1 do begin
  223.     Sprite := Sprites[I];
  224.     if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  225.       HitSprite := Sprite;
  226.     end;
  227.   end;
  228.   SpriteUnderCursor := HitSprite;
  229. end;
  230.  
  231. procedure TSpriteControl.MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  232. begin
  233.   inherited MouseUp(Button, Shift, X, Y);
  234.   if Button = mbLeft then begin
  235.     if FDragging then begin
  236.       FDragging := False;
  237.     end
  238.     else begin
  239.       if Assigned(FClickedSprite) then begin
  240.         Sprites.Move(Sprites.IndexOf(FClickedSprite), 0);
  241.         UpdateSprite(FClickedSprite);
  242.       end;
  243.     end;
  244.     FClickedSprite := nil;
  245.   end;
  246. end;
  247.  
  248. procedure TSpriteControl.SetSpriteUnderCursor(ASprite:TSprite);
  249.   var OldSprite:TSprite;
  250. begin
  251.   if SpriteUnderCursor <> ASprite then begin
  252.     OldSprite := SpriteUnderCursor;
  253.     FSpriteUnderCursor := ASprite;
  254.     if Assigned(OldSprite) then begin
  255.       UpdateSprite(OldSprite);
  256.     end;
  257.     if Assigned(SpriteUnderCursor) then begin
  258.       UpdateSprite(SpriteUnderCursor);
  259.     end;
  260.   end;
  261. end;
  262.  
  263. end.
  264.  

P.S. I've replaced FUpdateCount with FNeedUpdate.

P.P.S. I have some crazy idea for software rendering example, but unfortunately I was very busy yesterday and will also be busy for next two days. If I will have some free time - I'll do it.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 18, 2017, 05:16:31 pm
I came up with this for the deletion:

Code: Pascal  [Select][+][-]
  1. procedure TSpriteControl.DeleteSpriteUnderMouse;
  2. var
  3.   I: Integer;
  4. begin
  5.   if SpriteUnderCursor <> nil then
  6.   begin
  7.     for I := Sprites.Count -1 downto 0 do
  8.     begin
  9.       if Sprites.Items[I] = SpriteUnderCursor then
  10.       begin
  11.         Sprites.Delete(I);
  12.         Exit;
  13.       end;
  14.     end;
  15.   end;
  16. end;

I guess that should be good enough?

EDIT: It seems there is more still to do, as your last comment suggests I need to update the bitmap still as ghost sprites are still left behind...



As for the update I haven't properly tested but it seems ok thanks.

It sounds like the current implementation has limitations as you described with the clipping of the bitmap etc. Anyway you've already done a great amount of work here so you shouldn't push yourself anymore, having said that though if you were to come back when you have some free time and post your crazy idea that would be interesting to see for sure, but again dont feel obliged that you have to I already appreciate your efforts ;)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 18, 2017, 06:22:38 pm
I came up with this for the deletion:

Code: Pascal  [Select][+][-]
  1. procedure TSpriteControl.DeleteSpriteUnderMouse;
  2. var
  3.   I: Integer;
  4. begin
  5.   if SpriteUnderCursor <> nil then
  6.   begin
  7.     for I := Sprites.Count -1 downto 0 do
  8.     begin
  9.       if Sprites.Items[I] = SpriteUnderCursor then
  10.       begin
  11.         Sprites.Delete(I);
  12.         Exit;
  13.       end;
  14.     end;
  15.   end;
  16. end;

I guess that should be good enough?

There is shorter and faster way to do it:
Code: Pascal  [Select][+][-]
  1. if Assigned(SpriteUnderCursor) then begin
  2.    Sprites.Remove(SpriteUnderCursor);
  3. end;
  4.  

EDIT: It seems there is more still to do, as your last comment suggests I need to update the bitmap still as ghost sprites are still left behind...

It's not that hard:
1) UpdateSprite(SpriteUnderCursor);
2) if FClickedSprite = SpriteUnderCursor  then begin FClickedSprite := nil; FDragging := False; end; 
3) SpriteUnderCursor := nil; 



As for the update I haven't properly tested but it seems ok thanks.

It sounds like the current implementation has limitations as you described with the clipping of the bitmap etc. Anyway you've already done a great amount of work here so you shouldn't push yourself anymore, having said that though if you were to come back when you have some free time and post your crazy idea that would be interesting to see for sure, but again dont feel obliged that you have to I already appreciate your efforts ;)
No. There is simple rule in programming: most obvious algorithm is usually the slowest one. It's just tweaks and optimizations to maximize performance via minimizing amount of redrawing, cuz redrawing - is the most expensive part. And it's iterative process. In my real program even more advanced algorithm is used. As you can see, no sprites are being redrawn at all, if none are updated - we just copy Bitmap to Canvas and that's it. Also clipping allows us to redraw only those sprites, that intersect with current update rect - other sprites stay unaffected. Plus I take old versions of Windows into account. They don't have built-in double-buffering (cuz Canvas has been drawn into temporary texture to be rendered via Direct3D since Vista only) - therefore any direct drawing on Canvas can cause flickering. Plus I try to use minimal amount of memory - just one back buffer. There are more simple, but much slower solutions. Most obvious one - just redraw all sprites every time. Another solution, that is a little bit slower, but should be cross platform - triple-buffering. You should also understand, that if you need to draw background, that is grid of sprites - more simple decomposition algorithms can be used to detect visibility and hit testing.

Of course other methods are possible, but they can't be based solely on GDI - they require either software rendering or using OpenGL/Direct3D.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 18, 2017, 06:42:32 pm
There is shorter and faster way to do it:
Code: Pascal  [Select][+][-]
  1. if Assigned(SpriteUnderCursor) then begin
  2.    Sprites.Remove(SpriteUnderCursor);
  3. end;
  4.  

......

It's not that hard:
1) UpdateSprite(SpriteUnderCursor);
2) if FClickedSprite = SpriteUnderCursor  then begin FClickedSprite := nil; FDragging := False; end; 
3) SpriteUnderCursor := nil;

Umm maybe I did something wrong :-[

This leaves ghost images behind still sometimes: %)

Code: Pascal  [Select][+][-]
  1. procedure TSpriteControl.DeleteSpriteUnderMouse;
  2. begin
  3.   if Assigned(SpriteUnderCursor) then begin
  4.     Sprites.Remove(SpriteUnderCursor);
  5.     UpdateSprite(SpriteUnderCursor);
  6.  
  7.     if FClickedSprite = SpriteUnderCursor then begin
  8.       FClickedSprite := nil;
  9.       FDragging := False;
  10.     end;
  11.  
  12.     SpriteUnderCursor := nil;
  13.   end;
  14. end;
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: argb32 on May 18, 2017, 07:37:06 pm
Yeah. BIOS functions were slow just because they were altering just one bit in video memory (4-plane 1bpp EGA for example), but had to calculate it's address, bitmask to access it and set up video adapter registers for every single pixel. My library had to overcome this problem too. But still - horizontal moving of sprites and horizontal scrolling were limited to 8-pixel blocks only. It's actually very noticeable in some old DOS games - that horizontal scrolling is quantized.

Most games still drew sprites at arbitrary coordinates. But it required some additional processing.
As of scrolling - EGA adapters had special register which controlled starting address of video memory which is visible at screen's top left corner.
Changing that register is a sort of hardware accelerated scrolling. But it scrolled with 8 pixels granularity.
But there was another one register which controlled starting bit or something like this.
Manipulating both of the registers gave pixel-precise scrolling.
I did this - it worked.
Games like Dangerous Dave also used this.

I.e. what do you have to do to scroll your level on PC? On modern computers you can redraw everything from scratch. Back in that old days CPUs were too weak to do it. You had to shift image via copying it. And even simple memory copying was too slow on my 12Mhz Intel 80286, just because you had to use movsb, that was twice slower, than movsw, just because video adapter latch register was just 8bit wide - it was taking whole CPU time in EGA 640x350 mode (that's why all old games usually used 320x200, besides of having compatibility with VGA 8bpp mode). And what do you have to do on NES? Just 5 instructions:

Games like Duke Nukem used the "total redraw" method and ran well on my 8MHz PC XT
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 18, 2017, 07:48:12 pm
Umm maybe I did something wrong :-[

This leaves ghost images behind still sometimes: %)

Code: Pascal  [Select][+][-]
  1. procedure TSpriteControl.DeleteSpriteUnderMouse;
  2. begin
  3.   if Assigned(SpriteUnderCursor) then begin
  4.     Sprites.Remove(SpriteUnderCursor);
  5.     UpdateSprite(SpriteUnderCursor);
  6.  
  7.     if FClickedSprite = SpriteUnderCursor then begin
  8.       FClickedSprite := nil;
  9.       FDragging := False;
  10.     end;
  11.  
  12.     SpriteUnderCursor := nil;
  13.   end;
  14. end;
Try this:
Code: Pascal  [Select][+][-]
  1. procedure TSpriteControl.DeleteSpriteUnderCursor;
  2.   var Temp:TSprite;
  3. begin
  4.   if Assigned(SpriteUnderCursor) then begin
  5.     Temp := SpriteUnderCursor;
  6.     if FClickedSprite = Temp then begin
  7.       FClickedSprite := nil;
  8.       FDragging := False;
  9.     end;
  10.     SpriteUnderCursor := nil;
  11.     Sprites.Remove(Temp);
  12.   end;
  13. end;
  14.  
I don't see any ghost sprites in both cases, but that's may be because target is 64bit by default for me. But initial code is still unsafe, cuz Sprites - is object list and it destroys object, when it's removed. May be that's, why you sometimes get ghost sprites. UpdateSprite is removed, cuz SpriteUnderCursor := nil; does the same.

P.S. And don't be afraid of clipping. Windows itself works this way (emm, worked in the past). In order to simulate whole window system from scratch, you need just 3 things: rects, clipping and Z-ordered lists.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 18, 2017, 07:56:14 pm
That seems to work thanks Mr.Madguy ;D

I'm gonna play around with your code some more and try and understand it better its quite interesting 8-)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 18, 2017, 08:24:39 pm
Most games still drew sprites at arbitrary coordinates. But it required some additional processing.
As of scrolling - EGA adapters had special register which controlled starting address of video memory which is visible at screen's top left corner.
Changing that register is a sort of hardware accelerated scrolling. But it scrolled with 8 pixels granularity.
But there was another one register which controlled starting bit or something like this.
Manipulating both of the registers gave pixel-precise scrolling.
I did this - it worked.
Games like Dangerous Dave also used this.
As I remember, I actually tried all this methods and something didn't work well. That's, what I'm talking about: there was ton of info about adapters themselves, but no working examples of real applications. For example IRQ 2 on retrace would be useful, but it was used as cascade one.

P.S. And yeah, shifting bits - was slow operation too. May be it was possible to store 8 copies of the same sprites, but I just decided to use 8-pixel blocks instead.  %)

P.S.S. As I remember, I even tried to copy pages of video memory via DMA memory-memory mode.  :o

And on NES everything is quite simple. You don't even need any timers, as framerate is fixed. Your app usually works this way:
Code: [Select]
Init;
while True do begin
   WaitForFrameStart;
   //PPU is now busy - we can do our stuff
   ReadJoystics;
   DoGameLogic;
   //We should prepare this in advance, cuz retrace period is short
   FillBuffersWithDataForPPU;
end;

//NMI interrupt - is thing, whole program is built around
procedure OnRetrace;
begin
   //PPU is busy during frame rendering
   //Short period, when we can send data to it
   UpdatePPU;
   //Framerate is fixed, so retrace is used for sound timing
   //Since PPU is updated, there is no reason for hurrying
   //But we shouldn't waste whole processor time
   //Some room should be left for game logic
   UpdateSound;
end;

Games like Duke Nukem used the "total redraw" method and ran well on my 8MHz PC XT
That's most amazing thing about games from that period of time. Even if you knew asm and low level optimizing, you still couldn't imagine, how such games could be made. But at the same time they existed, so it was possible and you could actually reproduce them from scratch. And this idea was just blowing your mind. Because it was amazing, how it was possible to make such games on machines, that barely could copy data from one place to another within time of one frame.
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 18, 2017, 11:02:35 pm
It really is fascinating how games could run so well on old low spec machines, the discussions in this thread are rather insightful and interesting :) 8-)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 20, 2017, 08:57:04 pm
Ok. First of all - small fix for original program:
Code: Pascal  [Select][+][-]
  1. unit TestMain;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, SpriteControl,
  9.   LCLType;
  10.  
  11. type
  12.  
  13.   { TSpriteTestForm }
  14.  
  15.   TSpriteTestForm = class(TForm)
  16.     procedure FormCreate(Sender: TObject);
  17.     procedure FormDestroy(Sender: TObject);
  18.     procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  19.     procedure SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  20.       Shift: TShiftState; X, Y: Integer);
  21.   private
  22.     { private declarations }
  23.   public
  24.     { public declarations }
  25.     SpriteControl:TSpriteControl;
  26.     SpriteBitmap:TBitmap;
  27.     function CreateSprite(AIndex:Integer):TBitmap;
  28.   end;
  29.  
  30. var
  31.   SpriteTestForm: TSpriteTestForm;
  32.  
  33. implementation
  34.  
  35. {$R *.lfm}
  36.  
  37. { TSpriteTestForm }
  38.  
  39. procedure TSpriteTestForm.FormCreate(Sender: TObject);
  40. begin
  41.   Randomize;
  42.   SpriteBitmap := TBitmap.Create;
  43.   SpriteBitmap.LoadFromFile('Sprites.bmp');
  44.   SpriteControl := TSpriteControl.Create(Self);
  45.   with SpriteControl do begin
  46.     Parent := Self;
  47.     OnMouseUp := @SpriteControlMouseUp;
  48.     Align := alClient;
  49.     Bitmap.Width := 640;
  50.     Bitmap.Height := 480;
  51.     BackgroundColor := clGreen;
  52.   end;
  53. end;
  54.  
  55. procedure TSpriteTestForm.FormDestroy(Sender: TObject);
  56. begin
  57.   SpriteBitmap.Free;
  58. end;
  59.  
  60. procedure TSpriteTestForm.FormKeyDown(Sender: TObject; var Key: Word;
  61.   Shift: TShiftState);
  62. begin
  63.   if Key = VK_DELETE then begin
  64.     SpriteControl.DeleteSpriteUnderCursor;
  65.   end;
  66. end;
  67.  
  68. procedure TSpriteTestForm.SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  69.   Shift: TShiftState; X, Y: Integer);
  70. begin
  71.   if Button = mbRight then begin
  72.     SpriteControl.AddSprite(TSprite.Create(CreateSprite(Random(12 * 8 - 1)), TPoint.Create(X - 40, Y - 40)));
  73.   end;
  74. end;
  75.  
  76. function TSpriteTestForm.CreateSprite(AIndex:Integer):TBitmap;
  77.   var Src, Dest:TRect;
  78. begin
  79.   Result := nil;
  80.   if (AIndex >= 0) and (AIndex < 12 * 8) then begin
  81.     Result := TBitmap.Create;
  82.     with Result do begin
  83.       Width := 81;
  84.       Height := 81;
  85.       Dest := TRect.Create(TPoint.Create(0, 0), 81, 81);
  86.       Src := Dest;
  87.       Src.Offset(5 + 81 * (AIndex mod 12), 5 + 81 * (AIndex div 12));
  88.       Canvas.CopyRect(Dest, SpriteBitmap.Canvas, Src);
  89.       Transparent := True;
  90.       TransparentColor := clBlack;
  91.       Mask(clBlack);
  92.     end;
  93.   end;
  94. end;
  95.  
  96. end.
  97.  
Code: Pascal  [Select][+][-]
  1. unit SpriteControl;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses {$ifdef windows}Windows,{$endif} Classes, Controls, Graphics, Fgl;
  8.  
  9. type
  10.   TSprite = class
  11.   protected
  12.     FBitmap:TBitmap;
  13.     FRect:TRect;
  14.   public
  15.     constructor Create(ABitmap:TBitmap;ACoord:TPoint);
  16.     destructor Destroy;override;
  17.     procedure Draw(ACanvas:TCanvas;ARect:TRect);
  18.     procedure Move(APoint:TPoint);
  19.     function HitTest(APoint:TPoint):Boolean;
  20.     property Bitmap:TBitmap read FBitmap;
  21.     property Rect:TRect read FRect;
  22.   end;
  23.  
  24.   TSpriteList = specialize TFPGObjectList<TSprite>;
  25.  
  26.   TSpriteControl = class(TCustomControl)
  27.   protected
  28.     FBitmap:TBitmap;
  29.     FSprites:TSpriteList;
  30.     FSpriteUnderCursor:TSprite;
  31.     {$ifdef windows}
  32.     FNeedUpdate:Boolean;
  33.     {$endif}
  34.     FClickedSprite:TSprite;
  35.     FClickedPoint:TPoint;
  36.     FClickedCoord:TPoint;
  37.     FDragging:Boolean;
  38.     procedure Paint;override;
  39.     procedure MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  40.     procedure MouseMove(Shift: TShiftState; X,Y: Integer);override;
  41.     procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  42.     procedure SetSpriteUnderCursor(ASprite:TSprite);
  43.   public
  44.     BackgroundColor:TColor;
  45.     constructor Create(AOwner: TComponent);override;
  46.     destructor Destroy;override;
  47.     procedure AddSprite(ASprite:TSprite);
  48.     procedure UpdateSprite(ASprite:TSprite);
  49.     procedure DeleteSpriteUnderCursor;
  50.     property Bitmap:TBitmap read FBitmap;
  51.     property Sprites:TSpriteList read FSprites;
  52.     property SpriteUnderCursor:TSprite read FSpriteUnderCursor write SetSpriteUnderCursor;
  53.   end;
  54.  
  55.  
  56. implementation
  57.  
  58. constructor TSprite.Create(ABitmap:TBitmap;ACoord:TPoint);
  59. begin
  60.   inherited Create;
  61.   FBitmap := ABitmap;
  62.   FRect := TRect.Create(ACoord, Bitmap.Width, Bitmap.Height);
  63. end;
  64.  
  65. destructor TSprite.Destroy;
  66. begin
  67.   FBitmap.Free;
  68.   inherited Destroy;
  69. end;
  70.  
  71. procedure TSprite.Draw(ACanvas:TCanvas;ARect:TRect);
  72. begin
  73.   if not (ARect * Rect).IsEmpty then begin
  74.     ACanvas.Draw(Rect.Left, Rect.Top, Bitmap);
  75.   end;
  76. end;
  77.  
  78. procedure TSprite.Move(APoint:TPoint);
  79. begin
  80.   FRect := TRect.Create(APoint, Bitmap.Width, Bitmap.Height);
  81. end;
  82.  
  83. function TSprite.HitTest(APoint:TPoint):Boolean;
  84. begin
  85.   Result := Rect.Contains(APoint);
  86.   if Result and Bitmap.Transparent then begin
  87.     APoint := APoint - Rect.TopLeft;
  88.     Result := Bitmap.Canvas.Pixels[APoint.X, APoint.Y] <> Bitmap.TransparentColor;
  89.   end;
  90. end;
  91.  
  92. constructor TSpriteControl.Create(AOwner: TComponent);
  93. begin
  94.   inherited Create(AOwner);
  95.   FBitmap := TBitmap.Create;
  96.   FSprites := TSpriteList.Create;
  97.   {$ifdef windows}
  98.   FNeedUpdate := True;
  99.   {$endif}
  100. end;
  101.  
  102. destructor TSpriteControl.Destroy;
  103. begin
  104.   FBitmap.Free;
  105.   FSprites.Free;
  106.   inherited Destroy;
  107. end;
  108.  
  109. procedure TSpriteControl.AddSprite(ASprite:TSprite);
  110. begin
  111.   Sprites.Insert(0, ASprite);
  112.   UpdateSprite(ASprite);
  113. end;
  114.  
  115. procedure TSpriteControl.UpdateSprite(ASprite:TSprite);
  116. begin
  117.   {$ifdef windows}
  118.     InvalidateRect(Handle, ASprite.Rect, False);
  119.     FNeedUpdate := True;
  120.   {$else}
  121.     Invalidate;
  122.   {$endif}
  123. end;
  124.  
  125. procedure TSpriteControl.DeleteSpriteUnderCursor;
  126.   var Temp:TSprite;
  127. begin
  128.   if Assigned(SpriteUnderCursor) then begin
  129.     Temp := SpriteUnderCursor;
  130.     if FClickedSprite = Temp then begin
  131.       FClickedSprite := nil;
  132.       FDragging := False;
  133.     end;
  134.     SpriteUnderCursor := nil;
  135.     Sprites.Remove(Temp);
  136.   end;
  137. end;
  138.  
  139. procedure TSpriteControl.Paint;
  140.   var Rect, ClipRect, PaintRect:TRect;
  141.   I:Integer;
  142.   {$ifdef windows}
  143.   ClipRgn:HRGN;
  144.   {$endif}
  145. begin
  146.   inherited Paint;
  147.   Rect := TRect.Create(TPoint.Create(0, 0), Bitmap.Width, Bitmap.Height);
  148.   ClipRect := Canvas.ClipRect;
  149.   PaintRect := Rect * ClipRect;
  150.   {$ifdef windows}
  151.   if FNeedUpdate then begin
  152.   {$endif}
  153.     with Bitmap.Canvas do begin
  154.       {$ifdef windows}
  155.       ClipRgn := CreateRectRgn(PaintRect.Left, PaintRect.Top, PaintRect.Right, PaintRect.Bottom);
  156.       SelectClipRgn(Handle, ClipRgn);
  157.       {$endif}
  158.       Brush.Color := BackgroundColor;
  159.       FillRect(PaintRect);
  160.     end;
  161.     for I := Sprites.Count - 1 downto 0 do begin
  162.       Sprites[I].Draw(Bitmap.Canvas, PaintRect);
  163.     end;
  164.     if Assigned(SpriteUnderCursor) and
  165.       not (SpriteUnderCursor.Rect * PaintRect).IsEmpty then
  166.     begin
  167.       with Bitmap.Canvas do begin
  168.         DrawFocusRect(SpriteUnderCursor.Rect);
  169.       end;
  170.     end;
  171.   {$ifdef windows}
  172.     with Bitmap.Canvas do begin
  173.       SelectClipRgn(Handle, 0);
  174.       DeleteObject(ClipRgn);
  175.     end;
  176.     FNeedUpdate := False;
  177.   end;
  178.   {$endif}
  179.   Canvas.CopyRect(PaintRect, Bitmap.Canvas, PaintRect);
  180.   if not PaintRect.Contains(ClipRect) then begin
  181.     Canvas.Brush.Color := Color;
  182.     Rect := ClientRect;
  183.     Rect.Left := Bitmap.Width;
  184.     Rect.Intersect(ClipRect);
  185.     Canvas.FillRect(Rect);
  186.     Rect := ClientRect;
  187.     Rect.Top := Bitmap.Height;
  188.     Rect.Width := Bitmap.Width;
  189.     Rect.Intersect(ClipRect);
  190.     Canvas.FillRect(Rect);
  191.   end;
  192. end;
  193.  
  194. procedure TSpriteControl.MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  195.   var I:Integer;Sprite, HitSprite:TSprite;
  196. begin
  197.   inherited MouseDown(Button, Shift, X, Y);
  198.   if Button = mbLeft then begin
  199.     HitSprite := nil;
  200.     for I := 0 to Sprites.Count - 1 do begin
  201.       Sprite := Sprites[I];
  202.       if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  203.         HitSprite := Sprite;
  204.         FClickedPoint := TPoint.Create(X, Y);
  205.         FClickedCoord := Sprite.Rect.TopLeft;
  206.         Break;
  207.       end;
  208.     end;
  209.     FClickedSprite := HitSprite;
  210.   end;
  211. end;
  212.  
  213. procedure TSpriteControl.MouseMove(Shift: TShiftState; X,Y: Integer);
  214.   var I:Integer;Sprite, HitSprite:TSprite;
  215. begin
  216.   inherited MouseMove(Shift, X, Y);
  217.   if Assigned(FClickedSprite) then begin
  218.     if not FDragging then begin
  219.       if (Abs(X - FClickedPoint.X) > 5) or (Abs(Y - FClickedPoint.Y) > 5) then begin
  220.         FDragging := True;
  221.       end;
  222.     end;
  223.     if FDragging then begin
  224.       UpdateSprite(FClickedSprite);
  225.       FClickedSprite.Move(FClickedCoord + TPoint.Create(X, Y) - FClickedPoint);
  226.       UpdateSprite(FClickedSprite);
  227.       SpriteUnderCursor := FClickedSprite;
  228.       Exit;
  229.     end;
  230.   end;
  231.   HitSprite := nil;
  232.   for I := 0 to Sprites.Count - 1 do begin
  233.     Sprite := Sprites[I];
  234.     if Sprite.HitTest(TPoint.Create(X, Y)) then begin
  235.       HitSprite := Sprite;
  236.     end;
  237.   end;
  238.   SpriteUnderCursor := HitSprite;
  239. end;
  240.  
  241. procedure TSpriteControl.MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  242. begin
  243.   inherited MouseUp(Button, Shift, X, Y);
  244.   if Button = mbLeft then begin
  245.     if FDragging then begin
  246.       FDragging := False;
  247.     end
  248.     else begin
  249.       if Assigned(FClickedSprite) then begin
  250.         Sprites.Move(Sprites.IndexOf(FClickedSprite), 0);
  251.         UpdateSprite(FClickedSprite);
  252.       end;
  253.     end;
  254.     FClickedSprite := nil;
  255.   end;
  256. end;
  257.  
  258. procedure TSpriteControl.SetSpriteUnderCursor(ASprite:TSprite);
  259.   var OldSprite:TSprite;
  260. begin
  261.   if SpriteUnderCursor <> ASprite then begin
  262.     OldSprite := SpriteUnderCursor;
  263.     FSpriteUnderCursor := ASprite;
  264.     if Assigned(OldSprite) then begin
  265.       UpdateSprite(OldSprite);
  266.     end;
  267.     if Assigned(SpriteUnderCursor) then begin
  268.       UpdateSprite(SpriteUnderCursor);
  269.     end;
  270.   end;
  271. end;
  272.  
  273. end.
  274.  
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 20, 2017, 08:57:21 pm
Aaaaaaaaaaaand now! Are you ready for some software rendering madness?????????? ::)

(Windows only :'()
Code: Pascal  [Select][+][-]
  1. unit TestMain;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, SpriteControl,
  9.   LCLType, SoftwareRendering;
  10.  
  11. type
  12.  
  13.   { TSpriteTestForm }
  14.  
  15.   TSpriteTestForm = class(TForm)
  16.     procedure FormCreate(Sender: TObject);
  17.     procedure FormDestroy(Sender: TObject);
  18.     procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  19.     procedure SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  20.       Shift: TShiftState; X, Y: Integer);
  21.   private
  22.     { private declarations }
  23.   public
  24.     { public declarations }
  25.     SpriteControl:TSpriteControl;
  26.     SpriteBitmap:TBitmap;
  27.     function CreateSprite(AIndex:Integer):TBitmap;
  28.   end;
  29.  
  30. var
  31.   SpriteTestForm: TSpriteTestForm;
  32.  
  33. implementation
  34.  
  35. {$R *.lfm}
  36.  
  37. { TSpriteTestForm }
  38.  
  39. procedure TSpriteTestForm.FormCreate(Sender: TObject);
  40. begin
  41.   Randomize;
  42.   SpriteBitmap := TBitmap.Create;
  43.   SpriteBitmap.LoadFromFile('Sprites.bmp');
  44.   SpriteControl := TSpriteControl.Create(Self);
  45.   with SpriteControl do begin
  46.     Parent := Self;
  47.     OnMouseUp := @SpriteControlMouseUp;
  48.     Align := alClient;
  49.     Bitmap.BackgroundColor := clGreen;
  50.   end;
  51. end;
  52.  
  53. procedure TSpriteTestForm.FormDestroy(Sender: TObject);
  54. begin
  55.   SpriteBitmap.Free;
  56. end;
  57.  
  58. procedure TSpriteTestForm.FormKeyDown(Sender: TObject; var Key: Word;
  59.   Shift: TShiftState);
  60. begin
  61.   if Key = VK_DELETE then begin
  62.     SpriteControl.DeleteSpriteUnderCursor;
  63.   end;
  64. end;
  65.  
  66. procedure TSpriteTestForm.SpriteControlMouseUp(Sender: TObject; Button: TMouseButton;
  67.   Shift: TShiftState; X, Y: Integer);
  68.   var Temp:TBitmap;Bitmap:TSoftwareBitmap;
  69. begin
  70.   if Button = mbRight then begin
  71.     Temp := CreateSprite(Random(12 * 8 - 1));
  72.     Bitmap := TSoftwareBitmap.Create(Temp, $000000);
  73.     SpriteControl.AddSprite(TSoftwareSprite.Create(Bitmap, TPoint.Create(X - 40, Y - 40)));
  74.     Temp.Free;
  75.   end;
  76. end;
  77.  
  78. function TSpriteTestForm.CreateSprite(AIndex:Integer):TBitmap;
  79.   var Src, Dest:TRect;
  80. begin
  81.   Result := nil;
  82.   if (AIndex >= 0) and (AIndex < 12 * 8) then begin
  83.     Result := TBitmap.Create;
  84.     with Result do begin
  85.       Width := 81;
  86.       Height := 81;
  87.       Dest := TRect.Create(TPoint.Create(0, 0), 81, 81);
  88.       Src := Dest;
  89.       Src.Offset(5 + 81 * (AIndex mod 12), 5 + 81 * (AIndex div 12));
  90.       Canvas.CopyRect(Dest, SpriteBitmap.Canvas, Src);
  91.       Transparent := True;
  92.       TransparentColor := clBlack;
  93.       Mask(clBlack);
  94.     end;
  95.   end;
  96. end;
  97.  
  98. end.
  99.  
Code: Pascal  [Select][+][-]
  1. unit SpriteControl;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses Windows, Classes, Controls, Graphics, Fgl, SoftwareRendering;
  8.  
  9. type
  10.   TSpriteList = specialize TFPGObjectList<TSoftwareSprite>;
  11.  
  12.   TSpriteControl = class(TCustomControl)
  13.   protected
  14.     FBitmap:TSoftwareDevice;
  15.     FSprites:TSpriteList;
  16.     FSpriteUnderCursor:TSoftwareSprite;
  17.     FNeedUpdate:Boolean;
  18.     FClickedSprite:TSoftwareSprite;
  19.     FClickedPoint:TPoint;
  20.     FClickedCoord:TPoint;
  21.     FDragging:Boolean;
  22.     procedure Paint;override;
  23.     procedure MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  24.     procedure MouseMove(Shift: TShiftState; X,Y: Integer);override;
  25.     procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);override;
  26.     function DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean;override;
  27.     function DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean;override;
  28.     procedure SetSpriteUnderCursor(ASprite:TSoftwareSprite);
  29.   public
  30.     constructor Create(AOwner: TComponent);override;
  31.     destructor Destroy;override;
  32.     procedure AddSprite(ASprite:TSoftwareSprite);
  33.     procedure UpdateSprite(ASprite:TSoftwareSprite;ADoSpriteUpdate:Boolean = True);
  34.     procedure DeleteSpriteUnderCursor;
  35.     property Bitmap:TSoftwareDevice read FBitmap;
  36.     property Sprites:TSpriteList read FSprites;
  37.     property SpriteUnderCursor:TSoftwareSprite read FSpriteUnderCursor write SetSpriteUnderCursor;
  38.   end;
  39.  
  40.  
  41. implementation
  42.  
  43. uses SysUtils;
  44.  
  45. constructor TSpriteControl.Create(AOwner: TComponent);
  46. begin
  47.   inherited Create(AOwner);
  48.   FBitmap := TSoftwareDevice.Create(640, 480);
  49.   FSprites := TSpriteList.Create;
  50.   FNeedUpdate := True;
  51. end;
  52.  
  53. destructor TSpriteControl.Destroy;
  54. begin
  55.   FBitmap.Free;
  56.   FSprites.Free;
  57.   inherited Destroy;
  58. end;
  59.  
  60. procedure TSpriteControl.AddSprite(ASprite:TSoftwareSprite);
  61. begin
  62.   Sprites.Insert(0, ASprite);
  63.   UpdateSprite(ASprite);
  64. end;
  65.  
  66. procedure TSpriteControl.UpdateSprite(ASprite:TSoftwareSprite;ADoSpriteUpdate:Boolean);
  67. begin
  68.   InvalidateRect(Handle, ASprite.Rect, False);
  69.   if ADoSpriteUpdate then begin
  70.     FNeedUpdate := True;
  71.   end;
  72. end;
  73.  
  74. procedure TSpriteControl.DeleteSpriteUnderCursor;
  75.   var Temp:TSoftwareSprite;
  76. begin
  77.   if Assigned(SpriteUnderCursor) then begin
  78.     Bitmap.ClipRect := SpriteUnderCursor.Rect;
  79.     Bitmap.Clear;
  80.     Temp := SpriteUnderCursor;
  81.     if FClickedSprite = Temp then begin
  82.       FClickedSprite := nil;
  83.       FDragging := False;
  84.     end;
  85.     SpriteUnderCursor := nil;
  86.     Sprites.Remove(Temp);
  87.     UpdateSprite(Temp);
  88.   end;
  89. end;
  90.  
  91. procedure TSpriteControl.Paint;
  92.   var Rect, ClipRect, PaintRect:TRect;
  93.   I:Integer;Sprite:TSoftwareSprite;
  94. begin
  95.   inherited Paint;
  96.   Rect := Bitmap.Rect;
  97.   ClipRect := Canvas.ClipRect;
  98.   PaintRect := Rect * ClipRect;
  99.   Bitmap.ClipRect := PaintRect;
  100.   Bitmap.FocusRect := TRect.Create(0, 0, 0, 0);
  101.   if FNeedUpdate then begin
  102.     Bitmap.Clear;
  103.     for I := Sprites.Count - 1 downto 0 do begin
  104.       Sprite := Sprites[I];
  105.       Bitmap.DrawSprite(Sprite);
  106.     end;
  107.     FNeedUpdate := False;
  108.   end;
  109.   if Assigned(SpriteUnderCursor) then begin
  110.     Bitmap.FocusRect := SpriteUnderCursor.Rect;
  111.   end;
  112.   Bitmap.DrawToCanvas(Canvas);
  113.   if not PaintRect.Contains(ClipRect) then begin
  114.     Canvas.Brush.Color := Color;
  115.     Rect := ClientRect;
  116.     Rect.Left := Bitmap.Rect.Width;
  117.     Rect.Intersect(ClipRect);
  118.     Canvas.FillRect(Rect);
  119.     Rect := ClientRect;
  120.     Rect.Top := Bitmap.Rect.Height;
  121.     Rect.Width := Bitmap.Rect.Width;
  122.     Rect.Intersect(ClipRect);
  123.     Canvas.FillRect(Rect);
  124.   end;
  125. end;
  126.  
  127. procedure TSpriteControl.MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  128. begin
  129.   inherited MouseDown(Button, Shift, X, Y);
  130.   if Button = mbLeft then begin
  131.     FClickedSprite := Bitmap.HitTest(TPoint.Create(X, Y));
  132.     if Assigned(FClickedSprite) then begin
  133.       FClickedPoint := TPoint.Create(X, Y);
  134.       FClickedCoord := FClickedSprite.Rect.TopLeft;
  135.     end;
  136.   end;
  137. end;
  138.  
  139. procedure TSpriteControl.MouseMove(Shift: TShiftState; X,Y: Integer);
  140. begin
  141.   inherited MouseMove(Shift, X, Y);
  142.   if Assigned(FClickedSprite) then begin
  143.     if not FDragging then begin
  144.       if (Abs(X - FClickedPoint.X) > 5) or (Abs(Y - FClickedPoint.Y) > 5) then begin
  145.         FDragging := True;
  146.       end;
  147.     end;
  148.     if FDragging then begin
  149.       UpdateSprite(FClickedSprite);
  150.       FClickedSprite.Move(FClickedCoord + TPoint.Create(X, Y) - FClickedPoint);
  151.       UpdateSprite(FClickedSprite);
  152.       SpriteUnderCursor := FClickedSprite;
  153.       Exit;
  154.     end;
  155.   end;
  156.   SpriteUnderCursor := Bitmap.HitTest(TPoint.Create(X, Y));
  157. end;
  158.  
  159. procedure TSpriteControl.MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
  160. begin
  161.   inherited MouseUp(Button, Shift, X, Y);
  162.   if Button = mbLeft then begin
  163.     if FDragging then begin
  164.       FDragging := False;
  165.     end
  166.     else begin
  167.       if Assigned(FClickedSprite) then begin
  168.         Sprites.Move(Sprites.IndexOf(FClickedSprite), 0);
  169.         UpdateSprite(FClickedSprite);
  170.       end;
  171.     end;
  172.     FClickedSprite := nil;
  173.   end;
  174. end;
  175.  
  176. function TSpriteControl.DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean;
  177. begin
  178.   Result := inherited DoMouseWheelDown(Shift, MousePos);
  179.   if Assigned(SpriteUnderCursor) then begin
  180.     SpriteUnderCursor.Z := SpriteUnderCursor.Z - 1;
  181.     UpdateSprite(SpriteUnderCursor);
  182.   end;
  183. end;
  184.  
  185. function TSpriteControl.DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean;
  186. begin
  187.   Result := inherited DoMouseWheelUp(Shift, MousePos);
  188.   if Assigned(SpriteUnderCursor) then begin
  189.     SpriteUnderCursor.Z := SpriteUnderCursor.Z + 1;
  190.     UpdateSprite(SpriteUnderCursor);
  191.   end;
  192. end;
  193.  
  194. procedure TSpriteControl.SetSpriteUnderCursor(ASprite:TSoftwareSprite);
  195.   var OldSprite:TSoftwareSprite;
  196. begin
  197.   if SpriteUnderCursor <> ASprite then begin
  198.     OldSprite := SpriteUnderCursor;
  199.     FSpriteUnderCursor := ASprite;
  200.     if Assigned(OldSprite) then begin
  201.       UpdateSprite(OldSprite, False);
  202.     end;
  203.     if Assigned(SpriteUnderCursor) then begin
  204.       UpdateSprite(SpriteUnderCursor, False);
  205.     end;
  206.   end;
  207. end;
  208.  
  209. end.
  210.  
Code: Pascal  [Select][+][-]
  1. unit SoftwareRendering;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses Windows, Graphics;
  8.  
  9. type
  10.   TSoftwareBitmap = class
  11.   protected
  12.     FColorKey:TColor;
  13.     FBitmapInfo:TBitmapInfo;
  14.     FRect:TRect;
  15.     FBits:PColor;
  16.   public
  17.     constructor Create(ABitmap:TBitmap;AColorKey:TColor);
  18.     destructor Destroy;override;
  19.     property Rect:TRect read FRect;
  20.     property ColorKey:TColor read FColorKey;
  21.     property Bits:PColor read FBits;
  22.   end;
  23.  
  24.   TSoftwareSprite = class
  25.   protected
  26.     FZ:Integer;
  27.     FRect:TRect;
  28.     FBitmap:TSoftwareBitmap;
  29.     procedure SetZ(AZ:Integer);
  30.   public
  31.     constructor Create(ABitmap:TSoftwareBitmap;ACoord:TPoint);
  32.     destructor Destroy;override;
  33.     procedure Move(APoint:TPoint);
  34.     procedure BringToFront;
  35.     property Z:Integer read FZ write SetZ;
  36.     property Rect:TRect read FRect;
  37.     property Bitmap:TSoftwareBitmap read FBitmap;
  38.   end;
  39.  
  40.   TSoftwareDevice = class
  41.   protected
  42.     FBitmapInfo:TBitmapInfo;
  43.     FRect:TRect;
  44.     FBits:PColor;
  45.     FSelectBuffer:PPointer;
  46.     FFocusRect:TRect;
  47.     procedure SetFocusRect(ARect:TRect);
  48.   public
  49.     BackgroundColor:TColor;
  50.     ClipRect:TRect;
  51.     constructor Create(AWidth, AHeight:Integer);
  52.     destructor Destroy;override;
  53.     procedure DrawSprite(ASprite:TSoftwareSprite);
  54.     procedure Clear;
  55.     procedure DrawFocusRect(AFocusRect:TRect);
  56.     procedure DrawToCanvas(ACanvas:TCanvas);
  57.     function HitTest(APoint:TPoint):TSoftwareSprite;
  58.     property Rect:TRect read FRect;
  59.     property FocusRect:TRect read FFocusRect write SetFocusRect;
  60.   end;
  61.  
  62. implementation
  63.  
  64. constructor TSoftwareBitmap.Create(ABitmap:TBitmap;AColorKey:TColor);
  65. begin
  66.   inherited Create;
  67.   FColorKey := AColorKey;
  68.   with FBitmapInfo.bmiHeader do begin
  69.     biSize := SizeOf(FBitmapInfo.bmiHeader);
  70.     biWidth := ABitmap.Width;
  71.     biHeight := -ABitmap.Height;
  72.     biPlanes := 1;
  73.     biBitCount := 32;
  74.     biCompression := BI_RGB;
  75.     biSizeImage := 0;
  76.     biXPelsPerMeter := 0;
  77.     biYPelsPerMeter := 0;
  78.     biClrUsed := 0;
  79.     biClrImportant := 0;
  80.   end;
  81.   FBits := GetMem(ABitmap.Width * ABitmap.Height * SizeOf(TColor));
  82.   GetDIBits(ABitmap.Canvas.Handle, ABitmap.Handle, 0, ABitmap.Height, FBits, FBitmapInfo, DIB_RGB_COLORS);
  83.   FRect := TRect.Create(TPoint.Create(0, 0), ABitmap.Width, ABitmap.Height);
  84. end;
  85.  
  86. destructor TSoftwareBitmap.Destroy;
  87. begin
  88.   FreeMem(FBits);
  89.   inherited Destroy;
  90. end;
  91.  
  92. constructor TSoftwareSprite.Create(ABitmap:TSoftwareBitmap;ACoord:TPoint);
  93. begin
  94.   inherited Create;
  95.   FBitmap := ABitmap;
  96.   FRect := TRect.Create(ACoord, ABitmap.Rect.Width, ABitmap.Rect.Height);
  97. end;
  98.  
  99. destructor TSoftwareSprite.Destroy;
  100. begin
  101.   FBitmap.Free;
  102.   inherited Destroy;
  103. end;
  104.  
  105. procedure TSoftwareSprite.SetZ(AZ:Integer);
  106. begin
  107.   if AZ >= 0 then begin
  108.     FZ := AZ;
  109.   end;
  110. end;
  111.  
  112. procedure TSoftwareSprite.Move(APoint:TPoint);
  113. begin
  114.   FRect := TRect.Create(APoint, Bitmap.Rect.Width, Bitmap.Rect.Height);
  115. end;
  116.  
  117. procedure TSoftwareSprite.BringToFront;
  118. begin
  119.   FZ := 0;
  120. end;
  121.  
  122. constructor TSoftwareDevice.Create(AWidth, AHeight:Integer);
  123. begin
  124.   inherited Create;
  125.   with FBitmapInfo.bmiHeader do begin
  126.     biSize := SizeOf(FBitmapInfo.bmiHeader);
  127.     biWidth := AWidth;
  128.     biHeight := -AHeight;
  129.     biPlanes := 1;
  130.     biBitCount := 32;
  131.     biCompression := BI_RGB;
  132.     biSizeImage := 0;
  133.     biXPelsPerMeter := 0;
  134.     biYPelsPerMeter := 0;
  135.     biClrUsed := 0;
  136.     biClrImportant := 0;
  137.   end;
  138.   FBits := GetMem(AWidth * AHeight * SizeOf(TColor));
  139.   FSelectBuffer := GetMem(AWidth * AHeight * SizeOf(Pointer));
  140.   FRect := TRect.Create(TPoint.Create(0, 0), AWidth, AHeight);
  141. end;
  142.  
  143. destructor TSoftwareDevice.Destroy;
  144. begin
  145.   FreeMem(FBits);
  146.   FreeMem(FSelectBuffer);
  147.   inherited Destroy;
  148. end;
  149.  
  150. procedure TSoftwareDevice.SetFocusRect(ARect:TRect);
  151. begin
  152.   if FFocusRect <> ARect then begin
  153.     if not FFocusRect.IsEmpty then begin
  154.       DrawFocusRect(FFocusRect);
  155.     end;
  156.     if not ARect.IsEmpty then begin
  157.       DrawFocusRect(ARect);
  158.     end;
  159.     FFocusRect := ARect;
  160.   end;
  161. end;
  162.  
  163. procedure TSoftwareDevice.DrawSprite(ASprite:TSoftwareSprite);
  164.   var Src, Dest:TRect;SrcData,DestData:PColor;SelectData:PPointer;
  165.     SrcOffset, DestOffset:Integer;ColorKey:TColor;
  166.     I, J:Integer;
  167. begin
  168.   Dest := ASprite.Rect * ClipRect * Rect;
  169.   Src := Dest;
  170.   Src.Offset(-ASprite.Rect.TopLeft.X, -ASprite.Rect.TopLeft.Y);
  171.   Src := Src * ASprite.Bitmap.Rect;
  172.   Dest := Src;
  173.   Dest.Offset(ASprite.Rect.TopLeft);
  174.   SrcData := ASprite.Bitmap.Bits + (Src.Top * ASprite.Rect.Width + Src.Left);
  175.   DestData := FBits + (Dest.Top * Rect.Width + Dest.Left);
  176.   SelectData := FSelectBuffer + (Dest.Top * Rect.Width + Dest.Left);
  177.   SrcOffset := ASprite.Rect.Width - Src.Width;
  178.   DestOffset := Rect.Width - Dest.Width;
  179.   ColorKey := ASprite.Bitmap.ColorKey;
  180.   for I := 0 to Src.Height - 1 do begin
  181.     for J := 0 to Src.Width - 1 do begin
  182.       if SrcData^ <> ColorKey  then begin
  183.         if (not Assigned(SelectData^)) or (TSoftwareSprite(SelectData^).Z >= ASprite.Z) then begin
  184.           DestData^ := SrcData^;
  185.           SelectData^ := Pointer(ASprite);
  186.         end;
  187.       end;
  188.       Inc(SrcData);
  189.       Inc(DestData);
  190.       Inc(SelectData);
  191.     end;
  192.     Inc(SrcData, SrcOffset);
  193.     Inc(DestData, DestOffset);
  194.     Inc(SelectData, DestOffset);
  195.   end;
  196. end;
  197.  
  198. procedure TSoftwareDevice.Clear;
  199.   var Dest:TRect;DestData:PColor;SelectData:PPointer;
  200.     DestOffset:Integer;I, J:Integer;
  201. begin
  202.   Dest := ClipRect * Rect;
  203.   DestData := FBits + (Dest.Top * Rect.Width + Dest.Left);
  204.   SelectData := FSelectBuffer + (Dest.Top * Rect.Width + Dest.Left);
  205.   DestOffset := Rect.Width - Dest.Width;
  206.   for I := 0 to Dest.Height - 1 do begin
  207.     for J := 0 to Dest.Width - 1 do begin
  208.       DestData^ := BackgroundColor;
  209.       SelectData^ := nil;
  210.       Inc(DestData);
  211.       Inc(SelectData);
  212.     end;
  213.     Inc(DestData, DestOffset);
  214.     Inc(SelectData, DestOffset);
  215.   end;
  216. end;
  217.  
  218. procedure TSoftwareDevice.DrawFocusRect(AFocusRect:TRect);
  219.   var Dest:TRect;DestData:PColor;
  220.   I:Integer;Fill:Boolean;
  221. begin
  222.   Dest := AFocusRect * ClipRect * Rect;
  223.   if Dest.Top = AFocusRect.Top then begin
  224.     Fill := ((Dest.Left and 1) xor (Dest.Top and 1)) = 1;
  225.     DestData := FBits + (Dest.Top * Rect.Width + Dest.Left);
  226.     for I := 0 to Dest.Width - 1 do begin
  227.       if Fill then begin
  228.         DestData^ := (not DestData^) and $ffffff;
  229.       end;
  230.       Inc(DestData);
  231.       Fill := not Fill;
  232.     end;
  233.   end;
  234.   if Dest.Left = AFocusRect.Left then begin
  235.     Fill := ((Dest.Left and 1) xor (Dest.Top and 1)) = 1;
  236.     DestData := FBits + (Dest.Top * Rect.Width + Dest.Left);
  237.     for I := 0 to Dest.Height - 1 do begin
  238.       if Fill then begin
  239.         DestData^ := (not DestData^) and $ffffff;
  240.       end;
  241.       Inc(DestData, Rect.Width);
  242.       Fill := not Fill;
  243.     end;
  244.   end;
  245.   if Dest.Bottom = AFocusRect.Bottom then begin
  246.     Fill := ((Dest.Left and 1) xor ((Dest.Bottom - 1) and 1)) = 1;
  247.     DestData := FBits + ((Dest.Bottom - 1) * Rect.Width + Dest.Left);
  248.     for I := 0 to Dest.Width - 1 do begin
  249.       if Fill then begin
  250.         DestData^ := (not DestData^) and $ffffff;
  251.       end;
  252.       Inc(DestData);
  253.       Fill := not Fill;
  254.     end;
  255.   end;
  256.   if Dest.Right = AFocusRect.Right then begin
  257.     Fill := (((Dest.Right - 1) and 1) xor (Dest.Top and 1)) = 1;
  258.     DestData := FBits + (Dest.Top * Rect.Width + (Dest.Right - 1));
  259.     for I := 0 to Dest.Height - 1 do begin
  260.       if Fill then begin
  261.         DestData^ := (not DestData^) and $ffffff;
  262.       end;
  263.       Inc(DestData, Rect.Width);
  264.       Fill := not Fill;
  265.     end;
  266.   end;
  267. end;
  268.  
  269. function TSoftwareDevice.HitTest(APoint:TPoint):TSoftwareSprite;
  270. begin
  271.   Result := nil;
  272.   if Rect.Contains(APoint) then begin
  273.     Result := TSoftwareSprite((FSelectBuffer + (APoint.Y * Rect.Width + APoint.X))^);
  274.   end;
  275. end;
  276.  
  277. procedure TSoftwareDevice.DrawToCanvas(ACanvas:TCanvas);
  278.   var PaintRect:TRect;
  279. begin
  280.   PaintRect := Rect * ClipRect;
  281.   SetDIBitsToDevice(
  282.     ACanvas.Handle,
  283.     PaintRect.Left,
  284.     PaintRect.Top,
  285.     PaintRect.Width,
  286.     PaintRect.Height,
  287.     PaintRect.Left,
  288.     Rect.Height - PaintRect.Height - PaintRect.Top,
  289.     0,
  290.     Rect.Height,
  291.     FBits,
  292.     FBitmapInfo,
  293.     DIB_RGB_COLORS
  294.   );
  295. end;
  296.  
  297. end.
  298.  

I'm not sure about it's performance - you should figure it out by yourself. I tried to optimize everything, I could. But it's obviously more CPU and RAM hungry due to obvious reasons. %)

Functionality should be exactly the same, but there is completely new cool feature. Software Z-buffer!  8-) (That is also used as software selection buffer to boost hit detection performance) You can now assign explicit Z coordinate to every sprite! It's done via mouse wheel. By default all sprites are topmost. But via scrolling mouse wheel up you can increase their Z coordinate. Mouse wheel down obviously decreases it. And since that moment order of sprites will matter only for sprites with the same Z coordinate. No matter, what order sprites will have - sprites with lower Z coordinate will always be above sprites with higher one. Left mouse click still brings sprite to the top of sprite list, as earlier - and this still will change order of sprites with the same Z coordinate.

P.S. As we have software Z-buffer now - software 3D rendering is possible via this software renderer.  >:D
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: knuckles on May 21, 2017, 08:54:09 pm
woah how cool is that 8-) thanks for sharing  ;D

I wouldn't even know where to begin trying to understand it all especially when it comes to working with the bitmap bits and stuff  %) %)

EDIT:

So do you think this is more likely how the older Game Makers achieved this especially when the really old versions were before XP days when hardware was really low spec? or would your first examples have been more likely? I guess both seem feasible and no one will ever truly know without completely reverse engineering the logic but trying to get in the mind of another developer and finding out how certain tasks may have been approached and implemented is quite interesting, some of the techniques demonstrated here I would not ever even thought of %)
Title: Re: How to handle moving and deleting of objects drawn on a bitmap?
Post by: Mr.Madguy on May 22, 2017, 07:32:18 am
woah how cool is that 8-) thanks for sharing  ;D

I wouldn't even know where to begin trying to understand it all especially when it comes to working with the bitmap bits and stuff  %) %)

EDIT:

So do you think this is more likely how the older Game Makers achieved this especially when the really old versions were before XP days when hardware was really low spec? or would your first examples have been more likely? I guess both seem feasible and no one will ever truly know without completely reverse engineering the logic but trying to get in the mind of another developer and finding out how certain tasks may have been approached and implemented is quite interesting, some of the techniques demonstrated here I would not ever even thought of %)
I don't know. According to Wiki since version 3.0 Game Maker has been using DirectDraw and since version 5.3 it has been using Direct3D. Whether it had been using GDI or software rendering prior to 3.0 - depended on whether it needed advanced rendering features or not. As you can see, GDI can be successfully used for sprite rendering, but at the same time GDI is very limited. Alpha blending isn't supported for example. And via software rendering you can simulate the way, old (even DOS) games worked - i.e. any arbitrary effects. Also hardware capabilities should be taken into account. You should know, that in the past there was some period of time, when video cards were too expensive and not all people had good ones. That's, when software rendering was more widespread. But at some moment load started to shift towards video cards. I.e. hardware acceleration was used in order to free CPU and system RAM for another tasks.
TinyPortal © 2005-2018