Author Topic: 2D Animated Sprite Sheet Parser  (Read 3418 times)

0 Members and 1 Guest are viewing this topic.

Offline johannhowitzer

  • Forum Regular
  • Posts: 118
    • View Profile
2D Animated Sprite Sheet Parser
« on: October 22, 2020, 06:07:47 am »
This code takes sprite sheets that contain marker pixels, and chops it up by storing a bunch of data in an array.  The indexes of the array can then be used to call upon the data to draw and animate sprites.  The sprite sheets can be changed dramatically, and the parsing routine will do all the work of detecting where things have moved.  Multiple sheets can be loaded one after another, and the new sprites from each sheet will be added to the array in order.

If you're confused about something, please bring it up so I can clarify, and if this post needs to be edited to be easier to understand, I'll gladly make the edits.


First, this stuff goes in the header:


Code: QB64: [Select]
  1. type coordinate_int
  2.    x as long
  3.    y as long
  4.  
  5. type sprite_structure
  6.    ' Top-left position of the sprite's first animation frame in the sheet
  7.    pos     as coordinate_int
  8.  
  9.    ' Size of sprite visually
  10.    size    as coordinate_int
  11.  
  12.    ' Number of frames in animation
  13.    frames  as integer
  14.  
  15.    ' "Frames-per-frame" - that is, the number of time units that will tick in your program
  16.    ' per frame of animation.  If this has a value of 2, and your program runs at 60 FPS,
  17.    ' the sprite will animate at 30 FPS.  This is done so that animations that do not update
  18.    ' to the next frame every unit of time will not take up huge sections of the sprite sheet
  19.    ' with duplicates.
  20.    fpf     as _byte
  21.  
  22.    ' Offset from hitbox position to sprite's visual position.  Your program should be storing
  23.    ' the position of an object mechanically, and then add this to the object's position
  24.    ' to get the position that the sprite will be drawn.
  25.    offset  as coordinate_int
  26.  
  27.    ' Size of hitbox, in my program I've copied this over to the object data
  28.    hb_size as coordinate_int
  29.  
  30.    ' Image handle of the sheet this sprite is found in
  31.    image   as _unsigned long
  32.  
  33. dim shared sprite(1000) as sprite_structure
  34. dim shared sprite_count as integer
  35.  
  36. call parse_sprites([first image handle])
  37. call parse_sprites([second image handle])


The two routines below are the tools the parsing routine uses.  They take a starting position, handle of image to scan, and the color used for detection.  scan_right returns the x coordinate where the color was first found, while scan_down returns the y coordinate. These scans are noninclusive - they will not scan the pixel they start on.  To make them inclusive, move the loop condition in front of the do statement.

The preserve& handle is used to make sure these routines keep the same _source the program had going in, so you don't have to worry about that part.  I like to do that for most routines that will use _source or _dest in any way, saves headaches.


Code: QB64: [Select]
  1. function scan_right(x1, y, i&, d~&)
  2. x = x1
  3. preserve& = _source
  4. w = _width(i&)
  5.    x = x + 1
  6.    ' if x > w then do something - you can error trap moving past the edge of the image here
  7. loop until point(x, y) = d~& or x > w
  8. scan_right = x
  9. _source preserve&
  10.  
  11.  
  12. function scan_down(x, y1, i&, d~&)
  13. y = y1
  14. preserve& = _source
  15. h = _height(i&)
  16.    y = y + 1
  17.    ' if y > h then do something - you can error trap moving past the edge of the image here
  18. loop until point(x, y) = d~& or y > h
  19. scan_down = y
  20. _source preserve&


Now here's the star of the show, the parsing routine.  It uses the coordinates such as x1, y1, x2, y2, x_hb, y_hb as its scan locations - moving from x1 to x2 etc. when it needs to leave some information behind for later.  You can actually use this technique and restructure it as you see fit for your own purposes, this sprite sheet slicer is just the way I've used it, and seems like it would be the most likely use.


Code: QB64: [Select]
  1. sub parse_sprites(i&)
  2.  
  3. preserve& = _source
  4.  
  5. d~& = point(0, 0) ' Detection color
  6. s  = sprite_count + 1
  7. x1 = 1 ' Top left of first sprite in sheet
  8. y1 = 2
  9.  
  10.    sprite(s).image = i&
  11.  
  12.    ' Source position
  13.    sprite(s).pos.x = x1
  14.    sprite(s).pos.y = y1
  15.  
  16.    ' #1 Sprite size
  17.    x2 = scan_right(x1, y1, i&, d~&)
  18.    y2 =  scan_down(x1, y1, i&, d~&)
  19.    sprite(s).size.x = x2 - x1 - 1
  20.    sprite(s).size.y = y2 - y1 - 1
  21.  
  22.    ' #2 Animation frame count
  23.    x2 = scan_right(x2, y1, i&, d~&)
  24.    sprite(s).frames = int( ((x2 + 1) - x1) / (sprite(s).size.x + 2) )
  25.    if sprite(s).frames < 1 then sprite(s).frames = 1
  26.  
  27.    ' #3 Frame counter ticks per animation frame
  28.    sprite(s).fpf = scan_right(x2, y1 - 1, i&, d~&) - x2
  29.    if sprite(s).fpf < 1 then sprite(s).fpf = 1
  30.    x2 = x2 + 1
  31.  
  32.    ' #4 Sprite display position - relative to hitbox position
  33.    x_hb = scan_right(x2 - 1, y1, i&, d~&)
  34.    y_hb =  scan_down(x2, y1 - 1, i&, d~&)
  35.    sprite(s).offset.x = x2 - x_hb
  36.    sprite(s).offset.y = y1 - y_hb
  37.    ' NOTE If either offset is zero, this forces the other one to be zero as well
  38.    '      Easy fix is to move the detection pixels outside the sprite area
  39.  
  40.    ' #5 Hitbox size
  41.    sprite(s).hb_size.x = scan_right(x_hb, y1, i&, d~&) - x_hb
  42.    sprite(s).hb_size.y =  scan_down(x2, y_hb, i&, d~&) - y_hb
  43.  
  44.    y1 = y2 + 1
  45.    if point(x1 - 1, y1) = d~& then ' #6 End of column
  46.       if point(x1, 0) = d~& then exit do ' No more columns
  47.       y1 = 2
  48.       x1 = scan_right(x1, 0, i&, d~&) + 1 ' Find new column
  49.    end if
  50.  
  51.    s = s + 1
  52.  
  53. sprite_count = s
  54.  
  55. _source preserve&
  56.  


(Notice the minor issue with the flexibility of the hitbox detection pixels; I realized this quirk after fully coding this, and have had bigger fish to fry since then, so I haven't gone back and updated it.  All it really means is in some fringe cases, you will need to add a row and column of empty pixels at the top and left of a sprite.  If I do get around to updating this, I'll come back and post it here.)

Now, here's an example of using the sprite data in a _putimage statement:


Code: QB64: [Select]
  1. 's = [sprite index, set through the object data somehow]
  2. 'frame = [frame counter used by the program to mark time units]
  3.  
  4. w = sprite(s).size.x
  5. h = sprite(s).size.y
  6.  
  7. ' Animation frame
  8. fpf = sprite(s).fpf
  9. if fpf = 0 then fpf = 1 ' prevent division by zero
  10. f = int((frame - entity(n).spawn_f) / fpf) mod sprite(s).frames ' Sprite animation frame
  11.  
  12. x1 = [x of object] + sprite(s).offset.x ' Use hitbox offset to get sprite position on screen
  13. y1 = [y of object] + sprite(s).offset.y
  14. x2 = sprite(s).pos.x + (f * (w + 2)) ' Sprite position in source image, modified by animation frame
  15. y2 = sprite(s).pos.y
  16.  
  17. _putimage(x1, y1)-step(w, h), sprite(s).image, [image handle to draw to], (x2, y2)-step(w, h)


The last thing is to describe how the image is set up for the purposes of this parsing process, so you can follow how the routine analyzes the image, and if you want to use this slicer directly, you can easily make your own sprite sheets by following this structure.  The detection pixel color is freely changeable, the parsing routine finds it at point(0, 0).

Now, you'll want to download the image attachment, open it and zoom in so you can see pixels clearly.  The grey borders are really just to keep you from getting confused, the parser doesn't care about them.  You don't have to keep the borders there, but you do need one pixel between each sprite-frame on the sheet, and the borders shouldn't be the same as the detection color.  Note that while I've colored the detection pixels for the purposes of this explanation, they need to all be the same color as point(0, 0) normally.


Here's a step-by-step of the parsing process.  I've marked the parsing routine with numbers so you can follow it more easily.

1. First, from the current position, the red pixels are located to determine the sprite's size, seen at #1 in the routine.  The y2 coordinate will be used again later.

2. The orange pixel far to the right is found by #2 in the routine to determine the number of animation frames.  Note that the animation frames are expected to be the same size, if they aren't, you will see some strange things.

3. The second orange pixel immediately to the right of it is found by #3 in the routine, the further to the right, the more frames-per-frame.  So if it's only one pixel to the right of the other orange pixel, it will parse to a value of 1, and the animation will advance every frame.

4. Re-using the x2 and y1 coordinates that were left behind, that last box is scanned for the green pixels in #4 and #5 of the routine, to get the hitbox offset and size.  This is very intuitive - this little box outlined by these pixels is the hitbox, within the sprite's rectangle.  You can even draw a box for yourself as a visual aid - like that light-blue box I've drawn - since this last section is not an animation frame, it only contains the hitbox data.

5. The parser continues down the column with each new sprite, until it detects the end of the column, by finding the blue pixel you see there, detected by #6 in the code.  This section also looks for a new column along the top of the image, finding the other blue pixel you see at the top of the second column.  If it finds TWO such pixels - the blue and red - it knows this is the last column, and will terminate instead of looking for another column.


Do be careful with the detection pixels - it's easy to forget one, which will cause the parser to slip through and make a mistake.  For every sprite, you need:

- First-frame sprite height and width (red)
- Animation count, at right side of last animation frame (orange)
- Frames-per-frame, to the right of the previous pixel (orange)
- Four pixels giving hitbox offset and size (green)

And then you also need column end (blue), new column (blue), and last column (red) pixels.  And remember, all of these must be the same color as point(0, 0), which in this case is actually purple, like all the key pixels in the image that I haven't mentioned.


I've done the same scanning thing here with text, in order to implement a text tag replacement routine.  In my program, I've used this to make sure if the user changes the keybinds, the program will update the dialogue text that contains the keybinds to reflect the change.  It's a pretty flexible routine and can be easily expanded to do all kinds of things to text, I'll post it here too if people are interested.
blanksheet.png
* blanksheet.png (Filesize: 1.14 KB, Dimensions: 334x130, Views: 302)
« Last Edit: October 22, 2020, 06:43:24 am by johannhowitzer »