Dungeon 127
Images, free stuff, ideas, random thoughts. It’s Sunday, cut me a break!
I found some free resources yesterday on opengameart.org, mostly in the form of “sprite sheets”, which are typically graphics files containing an array of items all on one sheet.
Hm. I think I just learned something. My iPad Downloads folder seems to be shared between both my iPads. I thought I’d have to re-DL the stuff I found yesterday, or move it, but the files seem already to be here. Nice.
Anyway, the files tend to look like this:
As you can see, there are images of different items in the first file, separated (unfortunately) by lines, and several images of the same item, a fog creature I think, in the second. Those images are intended to be used sequentially, to animate the creature.
I could certainly cut those files into smaller files with Procreate or a similar graphics program. That would multiply the number of files by 8 or 10 on the average, resulting in even more chaos in my single flat project folder. (Subfolders may be coming, I’m told, but for now, we get the one.)
So I was wondering, as I debated whether it was time to get up, whether there’s a sensible way to allow a Loot or Entity to refer to one of these sprite sheets, and to know how to extract the bits it wants from it.
This is surely a matter for research and experimentation.
In general, we draw images with the Codea sprite
function, which just takes image, x, y, width, height, and z. It always draws the whole image. The image can be the name of an asset, which will be drawn in toto (not the little dog), or an object of type image
, which is a rectangle of pixels that you can draw into and then display later.
The image
objects have a few functions, get
, set
, and copy
. The get
and set
access a single pixel of the image. (Whee!). The copy
function is more interesting:
newImage = oldImage:copy(x,y,w,h)
This will copy the pixels starting at x,y, a rectangle of width and height w,h. It will even deal politely with values outside the bounds of the source image.
It would seem that we could use this capability to slice chunks out of our sprite sheets, and tuck them away somehow.
Looking forward, I envision a class, SpriteSheet, SS for short, that owns an asset and produces slices of it as needed. At this early moment, I don’t know quite how I’d want to use the thing, nor do I know quite how to code it, though of course the basics are becoming clear.
In our present scheme, an Entity has a few sprites that it selects among to animate or die or whatever it does. A Loot has a single sprite. They are each drawn with a call to the Codea sprite
function, 60 or 120 times a second, so we probably don’t want to be slicing and dicing images on the fly. Our SS object will either do some kind of caching, or will be used to return an image that some other object saves away.
The latter form would be easier to use in our present setup, where the objects know their sprite. For example:
function Monster:drawMonster()
if not self.tile.currentlyVisible then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
noTint()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self:isDead() then
tint(0,128,0,175)
sprite(self.dead)
elseif self.showDamage then
self:selectDamageTint()
sprite(self.hit,0,0)
else
sprite(self.moving[self.movingIndex], 0,0)
end
popStyle()
popMatrix()
end
This code is typical: we often refer directly to the sprite. But the last instance shows something we might do, which is call a function to fetch the image.
So, as we peruse the code, we begin to get a sense of what our new thing needs to do. Let’s now turn to slicing and dicing.
Slicing and Dicing
There are a few examples of extracting images from sprite sheets on the Codea forum, and some of them are likely pretty good. However, I’m not going to use them, and I’ll tell you why.
I don’t want to. Expanding on that a bit, I’m more interested in understanding how this should really be done, and not very interested at all in figuring out someone else’s code that does something that surely isn’t quite what I want. I will, however, refer to other people’s code for small examples and hints as to where I’m going wrong, if I need to.
But we’re here to program, and to learn how to shape code to our liking, and that’s what we’re gonna do.
Begin with a new project with CodeaUnit attached. I’m not sure what I’ll test yet, but it’ll be something.
It seems that we have to move any assets from the iPadOS Files into the Codea space, I guess because the OS won’t let an app outside its own area. I’ll move a couple.
After a bit of research and experimentation, I have this running test:
_:test("Get a sheet", function()
local sheet = asset.documents.Dropbox.gear_staffs_2
local img = readImage(sheet)
_:expect(img.width).is(576)
_:expect(img.height).is(64)
end)
That’s the info for this sheet:
There are 12 images there and sure enough 576/12 is 48, so these thingies are approximately) 48 wide. I say approximately because of those black lines around them.
I think I’ve got to inspect this file in Procreate to see what the pixels are really like.
A bit of zooming and snapping, and it looks to me as if we want to ignore the leftmost pixel and topmost pixel of each slice. So I’ll write a tiny display program to check this out.
This is what works:
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
local img = readImage(asset.documents.Dropbox.gear_staffs_2)
local n = 3
slice = img:copy(n*48+2,1,47,63)
end
function draw()
if CodeaUnit then showCodeaUnitTests() end
background(256)
sprite(asset.documents.Dropbox.gear_staffs_2,800,200)
sprite(slice, 800, 400, 48,64)
end
The resulting display is this:
It turns out that the origin of an image is at 1,1, adhering to the Codea convention of starting indexes at 1. So we need to start at x + 2 to skip the first line and take 47 pixels in X and start at y = 1 and take 63 pixels in Y.
Well, this is going to be delightfully ad-hoc, isn’t it? What is the general case? I guess there are a lot of “general” cases:
- A sprite sheet with no lines, W wide and H high, N images, just take 1 through W/N, W/N+1, etc.
- A sprite sheet with lines at the left edge, skip 1, take one fewer in W
- A sheet with lines at the top, start Y at one, take one fewer in Y.
- A sheet with lines at the bottom, start Y at 2, take one fewer, and consider whether there’s a line at the top …
- A sheet with lines on the left and right … etc
I think what one needs in the truly general case is a slicer that knows whether to skip a pixel on the low end, and to skip a pixel on the high end, in both X and Y. I suppose it might even need to skip more than one.
I don’t think we should try to implement such a thing. We should implement the thing we need, and if and when we need more, improve it.
So let’s see about TDDing this object, at least a bit.
_:test("Slice from a sheet", function()
local ss = SS(sheet, 12, 1,0,0,1)
local img = ss:slice(3)
end)
I don’t quite know how to test the slice yet. This is enough to drive some code:
SpriteSheet = class()
SS = SpriteSheet
function SS:init(sheet, numberX, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
end
What I intend here, “obviously”, is that numberX
is the number of images in the X dimension (only 1 in Y, though I may someday regret not supplying the Y), and the number of pixels to skip at the low and high ends in each dimension.
We’ll save that away and then slice out our slice.
At this point in the proceedings, I realize that I should probably start my slice numbers at 1, not zero, to stick with the Lua conventions.
I get to this:
SpriteSheet = class()
SS = SpriteSheet
function SS:init(sheet, numberX, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
self.sheet = sheet
self.numberX = numberX
self.xSkipLow = xSkipLow
self.xSkipHigh = xSkipHigh
self.ySkipLow = ySkipLow
self.ySkipHigh = ySkipHigh
self.img = readImage(sheet)
self.sliceWidth = self.img.width/numberX
self.sliceHeight = self.img.height
end
function SS:slice(sliceNumber)
local sliceStartX = (sliceNumber-1)*self.sliceWidth + self.xSkipLow + 1
local sliceStartY = self.ySkipLow + 1
local sliceW = self.sliceWidth - self.xSkipLow - self.xSkipHigh
local sliceH = self.sliceHeight - self.ySkipLow - self.ySkipHigh
return self.img:copy(sliceStartX, sliceStartY, sliceW, sliceH)
end
That’s kind of intricate, and some of these numbers are constants. I don’t really know how to test whether the slice is right other than display it, so I’ll do that.
Our slicer is slicing correctly. Now let’s see what we can do about cleaning it up. I think we should not cache the base image, as it is “large”, but instead should cache the specific slices asked for. I believe that in many if not most uses of the sprite sheet, we’ll only use a few of the slices.
So some refactoring … First a little test for caching:
_:test("Slice from a sheet", function()
local ss = SS(sheet, 12, 1,0,0,1)
local img = ss:slice(3)
_:expect(ss.slices[2]).is(nil)
_:expect(ss.slices[3]).is(img)
end)
The initial version that make this run:
function SS:init(sheet, numberX, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
self.sheet = sheet
self.numberX = numberX
self.xSkipLow = xSkipLow
self.xSkipHigh = xSkipHigh
self.ySkipLow = ySkipLow
self.ySkipHigh = ySkipHigh
self.img = readImage(sheet)
self.sliceWidth = self.img.width/numberX
self.sliceHeight = self.img.height
self.slices = {}
end
function SS:slice(sliceNumber)
if not self.slices[sliceNumber] then
local sliceStartX = (sliceNumber-1)*self.sliceWidth + self.xSkipLow + 1
local sliceStartY = self.ySkipLow + 1
local sliceW = self.sliceWidth - self.xSkipLow - self.xSkipHigh
local sliceH = self.sliceHeight - self.ySkipLow - self.ySkipHigh
self.slices[sliceNumber] = self.img:copy(sliceStartX, sliceStartY, sliceW, sliceH)
end
return self.slices[sliceNumber]
end
But we don’t want to keep the main image. (This is arguably premature optimization, or even premature pessimization, but it seems better to me.)
So we do this:
function SS:init(sheet, numberX, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
self.sheet = sheet
self.numberX = numberX
self.xSkipLow = xSkipLow
self.xSkipHigh = xSkipHigh
self.ySkipLow = ySkipLow
self.ySkipHigh = ySkipHigh
self.slices = {}
end
function SS:slice(sliceNumber)
if not self.slices[sliceNumber] then
local img = readImage(self.sheet)
local sliceWidth = img.width/self.numberX
local sliceHeight = img.height
local sliceStartX = (sliceNumber-1)*sliceWidth + self.xSkipLow + 1
local sliceStartY = self.ySkipLow + 1
local sliceW = sliceWidth - self.xSkipLow - self.xSkipHigh
local sliceH = sliceHeight - self.ySkipLow - self.ySkipHigh
self.slices[sliceNumber] = img:copy(sliceStartX, sliceStartY, sliceW, sliceH)
end
return self.slices[sliceNumber]
end
Now we read the main image every time we slice, but we only slice once per slice chosen.
Let’s get a little fancy here. Suppose we do want all of the slices in some cases. Let’s cater to that with a new function, sliceAll
, that sets them all up. Then we’ll want to read the image only once. We prepare for that by extracting:
function SS:slice(sliceNumber)
if not self.slices[sliceNumber] then
local img = readImage(self.sheet)
self:sliceFromImage(img,sliceNumber)
end
return self.slices[sliceNumber]
end
function SS:sliceFromImage(img,sliceNumber)
local sliceWidth = img.width/self.numberX
local sliceHeight = img.height
local sliceStartX = (sliceNumber-1)*sliceWidth + self.xSkipLow + 1
local sliceStartY = self.ySkipLow + 1
local sliceW = sliceWidth - self.xSkipLow - self.xSkipHigh
local sliceH = sliceHeight - self.ySkipLow - self.ySkipHigh
self.slices[sliceNumber] = img:copy(sliceStartX, sliceStartY, sliceW, sliceH)
end
The tests all run. No surprise. Let’s now test for sliceAll:
_:test("Slice all", function()
local ss = SS(sheet, 12, 1,0, 0,1)
ss:sliceAll()
_:expect(ss.slices[4]).is(ss:slice(4))
end)
This demands the function:
function SS:sliceAll()
local img = readImage(self.sheet)
for i = 1,self.numberX do
self:sliceFromImage(img, i)
end
end
I expect my test to run, and it does. Now, for fun, I’ll display all the slices:
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
local img = readImage(asset.documents.Dropbox.gear_staffs_2)
local n = 3
slice = img:copy(n*48+2,1,47,63)
ss = SpriteSheet(asset.documents.Dropbox.gear_staffs_2, 12, 1,0, 0,1)
ss:sliceAll()
end
function draw()
if CodeaUnit then showCodeaUnitTests() end
background(256)
sprite(asset.documents.Dropbox.gear_staffs_2,800,200)
sprite(slice, 800, 400, 48,64)
for i = 1,12 do
sprite(ss:slice(i), 500 + i*50, 400, 48,64)
end
end
So that seems to work as intended. We’ll see about applying it in the real program real soon. Maybe even tomorrow.
See you then!