Space Invaders 11: An interesting problem.
In the real game, the invaders’ missiles eat away at the shields. How can we duplicate this effect?
As I’ve mentioned, the real Space Invaders game displays a bitmap from a section of memory. if there’s something on the screen, the bits are 1, and if not, 0, in that position. This makes the shields “interesting”.
The shields are copied into memory in their locations, and when an invader missile hits a shield, bits are erased from it. I’ve not read the code in detail, but observing the game, it looks as if what is erased is roughly the shape of the missile.
In the two player game, when a turn is over, that player’s current shields, with whatever damage they have, are copied to temporary memory, and the other player’s shields are copied into display memory. That means that every shield’s damage is unique to that shield, and damage is remembered from turn to turn.
Normally in Codea, we just create a sprite image and go with it. We could perhaps approximate the Space Invaders effect by making lots of damaged versions. Maybe if we drew a screen-colored sprite on top of a shield, it would make a suitable hole in it, so we could stack up damage with multiple sprites on top of the shields.
Our mission today, should we choose to accept it, is to do some spiking with the shields, and figure out a reasonable way to do shield damage. My initial plan is to try intersecting missiles and shields, erasing bits.
We’ll do this in a new spike. I’ll call it shields.
Shields
I’ll lift the basic draw
from our invaders spike, since it handles a bit of screen organizing for us, and add in the shield:
-- Shields
function setup()
shield = readImage(asset.documents.Dropbox.shield)
end
function draw()
background(40, 40, 50)
rectMode(CENTER)
noFill()
stroke(255)
strokeWidth(2)
rect(WIDTH/2, HEIGHT/2, 224*4, 256*4)
spriteMode(CORNER)
text(DeltaTime, 100,100)
translate(WIDTH/2-224*2, 0)
scale(4)
sprite(shield, 50, 20 )
end
That gives us this picture:
You can see the console panel over to the left. I usually let that open up when I’m working. You can close it under program control or manually. When things go wrong, as they frequently do, messages display there, so I leave it open.
Now for a quick check, I’ll reset some bits in the sprite’s image. I think that should work just fine, but I’ve never tried it. I’ll just bang it a bit in setup.
function setup()
shield = readImage(asset.documents.Dropbox.shield)
shieldSize = vec2(22,16)
black = color(255,0,0)
for row = 13,16 do
for col = 10,15 do
shield:set(col,row, black)
end
end
end
That definition of black
is actually red, and it gives me this picture:
So that seems to work. I don’t imagine it’s terribly slow, but as a quick check, I’ll move those lines into a touch event, and see what happens.
function touched(touch)
black = color(255,0,0)
for row = 13,16 do
for col = 10,15 do
shield:set(col,row, black)
end
end
end
That works essentially instantly, as one would hope. So my first experiment says to me that we can probably create a unique shield for each screen position, and decide what bits of it to destroy on the fly, and it should work just fine.
I have this rough plan in mind at this point:
- detect in pure code when a missile might be hitting a shield
- check the shield area where the missile hits for solid bits
- use the missile outline to clear bits from the shield
- probably allow the missile to “penetrate” a step or two
We’ll continue to spike in roughly that direction, get a sense of what we can do.
Next Step
I think I’d like a function to clear the bits in one image that are on (1) in another image. The function should do proper clipping. I reckon it should take the image being cleared, the image doing the clearing, and an image-relative offset for the clearing image. Does this seem like something that could use a little TDD? Let’s try it and find out.
It took a bit too long to get CodeaUnit hooked up with my existing code. I think I need to improve the template a bit. And I definitely need to remember always to start with the CodeaUnit base template, even if I don’t plan to write any tests.
Anyway, we’re ready to go. What shall we test first? I’m not sure, so I’ll try to write a test.
After a bit of fiddling, I came up with this:
_:test("Clear a rectangle", function()
local baseImage = createWhiteImage()
for x = 5,9 do
for y = 10,15 do
baseImage:set(x,y,black)
end
end
_:expect(isImageClear(baseImage, 5,10, 9,15)).is(true)
end)
end)
end
function isImageClear(image, x1,y1, x2, y2)
for x = x1,x2 do
for y = y1,y2 do
local r,g,b,w = image:get(x,y)
local col = color(r,g,b,w)
if col ~= black then return false end
end
end
return true
end
function createWhiteImage()
local baseImage = image(22,16)
for x = 1,22 do
for y = 1,16 do
baseImage:set(x,y,white)
end
end
return baseImage
end
This is a bit of a mess but I’m just working things out as I go here. Basically we create a white image, then set part of it black, then check that range to see if it is in fact black.
We were not aided in our quest by the fact that image.get
returns four values, r,g,b,w, while image.set
takes a color (and, in fairness, optionally r,g,b,w).
Thinking about what we have here, I believe we’ll want to clear bits by setting them to all zeros, including the transparency field. If we set the transparency full trans, that should make the pixel disappear no matter what color it was, or what color was behind it.
So I’ll write a function that checks for fully transparent, though I think we’ll always be setting to all zeros. And I’ll write an image-clearing function or two to cover the color info. Here’s what I’ve got:
-- Shield Tests
-- RJ 20200722
local erase = color(0,0,0,0)
local white = color(255)
function testShield()
CodeaUnit.detailed = true
_:describe("Shield Test Suite", function()
_:before(function()
-- Some setup
end)
_:after(function()
-- Some teardown
end)
_:test("Equality test", function()
_:expect("Foo").is("Foo")
end)
_:test("Clear a rectangle", function()
local baseImage = createWhiteImage()
clearImage(baseImage, 5,10, 9,15)
_:expect(isImageClear(baseImage, 5,10, 9,15)).is(true)
end)
end)
end
function clearImage(img, x1,y1, x2,y2)
for x = x1,x2 or x1 do
for y = y1,y2 or y1 do
img:set(x,y,0,0,0,0)
end
end
end
function isImageClear(img, x1,y1, x2, y2)
for x = x1,x2 or x1 do
for y = y1,y2 or y1 do
local r,g,b,a = img:get(x,y)
if a ~= 0 then return false end
end
end
return true
end
function createWhiteImage()
local baseImage = image(22,16)
for x = 1,22 do
for y = 1,16 do
baseImage:set(x,y,white)
end
end
return baseImage
end
For my sins, I’ll clear a bit of the shield that is displayed, and I think I’ll set the background to a different color to check the transparency effect.
function touched(touch)
clearImage(shield, 5,1, 10,16)
end
That works as expected:
I’ve noticed in using the clearing and checking functions, that I tend to get the coordinates confused, often entering x1,x2 y1,y2 instead of x1,y1, x2,y2 as intended. I’m not sure at this moment what to do about that. Since the functions will probably only be used to set and clear single pixels at a time, it may not matter.
I think it’s time to …
Sum Up
This quick experiment has generated a fair amount of learning:
- clearing bits to transparent is probably best for us
- clearing rectangles has a tricky syntax
- we may not need to clear rectangles at all
- clearing seems to be fast enough visually
We may wish to write a timing test to get more info, but at the moment I think the risk is low.
I am slightly inclined to write an object wrapper for the Codea image
, so that I can attach some decent methods to it. It might be possible to hack the image’s metatable but that is a rather esoteric technique that I’m reluctant to use. We’ll see.
For now, we’ve learned a bit and that’s a good thing. I’ll include all the code, just for the few who may look at it. See you next time!
The Code
-- CUBase
local Console = ""
function setup()
shield = readImage(asset.documents.Dropbox.shield)
shieldSize = vec2(shield.width, shield.height)
print(shieldSize)
runTests()
end
function draw()
pushMatrix()
pushStyle()
background(128,0,0)
pushMatrix()
pushStyle()
fontSize(50)
textAlign(CENTER)
text(Console, WIDTH/2, HEIGHT-200)
popStyle()
popMatrix()
rectMode(CENTER)
noFill()
stroke(255)
strokeWidth(2)
rect(WIDTH/2, HEIGHT/2, 224*4, 256*4)
spriteMode(CORNER)
text(DeltaTime, 100,100)
translate(WIDTH/2-224*2, 0)
scale(4)
sprite(shield, 50, 20 )
popStyle()
popMatrix()
end
function touched(touch)
clearImage(shield, 5,1, 10,16)
end
function runTests()
assert(CodeaUnit, "Please add CodeaUnit as a dependency")
local det = CodeaUnit.detailed
CodeaUnit.detailed = false
Console = _.execute()
CodeaUnit.detailed = det
end
-- Shield Tests
-- RJ 20200722
local erase = color(0,0,0,0)
local white = color(255)
function testShield()
CodeaUnit.detailed = true
_:describe("Shield Test Suite", function()
_:before(function()
-- Some setup
end)
_:after(function()
-- Some teardown
end)
_:test("Equality test", function()
_:expect("Foo").is("Foo")
end)
_:test("Clear a rectangle", function()
local baseImage = createWhiteImage()
clearImage(baseImage, 5,10, 9,15)
_:expect(isImageClear(baseImage, 5,10, 9,15)).is(true)
end)
end)
end
function clearImage(img, x1,y1, x2,y2)
for x = x1,x2 or x1 do
for y = y1,y2 or y1 do
img:set(x,y,0,0,0,0)
end
end
end
function isImageClear(img, x1,y1, x2, y2)
for x = x1,x2 or x1 do
for y = y1,y2 or y1 do
local r,g,b,a = img:get(x,y)
if a ~= 0 then return false end
end
end
return true
end
function createWhiteImage()
local baseImage = image(22,16)
for x = 1,22 do
for y = 1,16 do
baseImage:set(x,y,white)
end
end
return baseImage
end