Asteroids 6: Controls
To move forward with this project, the ship needs to be able to move forward. And turn. And fire …
I’ve mentioned that I believe Codea’s keyboard handling is too weak to let me map the keyboard keys into controls. So our only course is to put some sensitive spots on the screen, and to treat them as momentary switches for left, right, go, fire (and hyperspace, perhaps).
There’s a sample program that comes with Codea, showing how to do track multiple touches:
The program looks like this:
function setup()
print("This example tracks multiple touches and colors them based on their ID")
-- keep track of our touches in this table
touches = {}
end
-- This function gets called whenever a touch
-- begins or changes state
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
-- When any touch ends, remove it from
-- our table
touches[touch.id] = nil
else
-- If the touch is in any other state
-- (such as BEGAN) we add it to our
-- table
touches[touch.id] = touch
end
end
function draw()
background(0, 0, 0, 255)
for k,touch in pairs(touches) do
-- Use the touch id as the random seed
math.randomseed(touch.id)
-- This ensures the same fill color is used for the same id
fill(math.random(255),math.random(255),math.random(255))
-- Draw ellipse at touch position
ellipse(touch.pos.x, touch.pos.y, 100, 100)
end
end
Netting this out, this code shows us how to create a table of all the active touches on the screen. For our purposes, we can check to see if any of them are in the hot zones we’re using for buttons and behave accordingly. I’ll begin by adopting the touched
function from this example into our program.
local Touches = {}
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
Declaring Touches
as local makes it visible to this entire file, and not to any other tabs that might look for it. It’s kind of a poor man’s class variable or such. I capitalize those items as a hint that they are more globally available than the local variables in functions.
An irritating thing about Lua, by the way, is that if you just refer to a variable it has never heard of, that will become a truly global variable. You might get an error if you use it before storing into it, but if you store first, well, it’s on you. All languages have their quirks, and we come up with ways to deal with them.
But I digress.
Now I’ll put a button or a few on the screen and see what I can do about recognizing that they’re touched. If I’m wise, I’ll just do one. I think I’ll put the left and right buttons on the lower left of the screen, and accelerate / fire on the lower right. My tentative plan is to angle them a bit for convenient touching. Anyway, here goes … Oh! I’d better remove that turning code before it confuses me, and then commit to “touch table”. Removing that stuff makes moveShip
entirely empty, for now.
OK, a bit of fiddling produces this:
From this:
function drawControls()
pushStyle()
ellipseMode(RADIUS)
stroke(255)
strokeWidth(1)
pushMatrix()
translate(50,200)
ellipse(0,0,50)
popMatrix()
pushMatrix()
translate(200,50)
ellipse(0,0,50)
popMatrix()
popStyle()
end
As a small note, the text console in that picture is turned off, and so are the little buttons that let you restart or stop the program. This is displayMode(FULLSCREEN_NO_BUTTONS)
and while it’ll be good for game play, it’s not convenient for debugging, running the tests, or the like. I’ll probably have to come up with some way of switching screen modes once the game starts. Should be easy enough.
Now that I know where the buttons want to be, it’s probably best to build a little table of those locations and draw them from the table. This is easy enough with these snippets:
local Buttons = {}
function setup()
print("Hello Asteroids!")
--displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function createButtons()
local dx=50
local dy=200
table.insert(Buttons, vec2(dx,dy))
table.insert(Buttons, vec2(dy,dx))
table.insert(Buttons, vec2(WIDTH-dx,dy))
table.insert(Buttons, vec2(WIDTH-dy,dx))
end
function drawControls()
pushStyle()
ellipseMode(RADIUS)
stroke(255)
strokeWidth(1)
for i,b in ipairs(Buttons) do
pushMatrix()
translate(b.x,b.y)
ellipse(0,0, 50)
popMatrix()
end
popStyle()
end
And we get four buttons, as intended. It’s a miracle!
I should (and do) rename drawControls
to drawButtons
. Commit “drawing four buttons”.
Now then, where are we? The next task, it seems to me, is to connect the buttons to actions. Right now, we just have the buttons drawn. They have no real existence in the program, certainly no connection between button and its meaning.
What might be part of the meaning of a Button? Well …
- Where is it?
- What text does it display?
- What is supposed to happen when it is touched?
- How is that operation communicated to the program?
Now, to me, this really cries out for a Button object. Each instance would know how to draw itself, in the right position, displaying any convenient text like “left” or “right”, and it would know how to decide if it is touched, and if so, how to communicate to the Ship what is to be done.
And, once we start thinking that way, the Ship should be an object as well, with behaviors like “left” and “right” and “go” and “fire”.
Now our Ship is perhaps “on the way” to becoming an object as well. It is a table containing pos
and ang
variables saying where it is and which way it is pointing. AndShip
is a known variable in our program, so that if we can figure out what to do about things like turning left or accelerating, we could add more information to it.
Another way to go would be for the ship to interrogate the touches and the button locations, and if it sees its buttons pressed, it just does whatever it should do. That’s a far more procedural way to do things, but maybe it’s closer to what a beginning Codea programmer knows how to do.
My purpose, however, isn’t just to simulate a beginning programmer, it’s also to show what I consider to be interesting, powerful, and useful ways to do things. Those ways surely include the use of classes and objects.
What is certainly the case is that our single-tab program here has quite a few separate ideas in it. It has the screen, the asteroids, the ship, and now the buttons. That’s at least four ideas, all in one program. That’s not good.
According to the people who “should”, code should have high cohesion and low coupling. What that means in people language is that each code package, be it a file, a class, a function, whatever, should hang together. It should be all about one thing. And the pieces should be coupled together as little as possible, so that each bit can be as independent of decisions about the others as possible.
That thinking is why I’ve already broken out functions like createAsteroids
and createAsteroid
, to keep single and related ideas together. Using a class is just another way to group information, and I’m feeling the need to do that.
But we’ll leave that decision for next time. Here’s the program so far, and I hope to see you next time!
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
local Ship = {}
local Touches = {}
local Buttons = {}
function setup()
print("Hello Asteroids!")
displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function createAsteroids()
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
end
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
return a
end
function createButtons()
local dx=50
local dy=200
table.insert(Buttons, vec2(dx,dy))
table.insert(Buttons, vec2(dy,dx))
table.insert(Buttons, vec2(WIDTH-dx,dy))
table.insert(Buttons, vec2(WIDTH-dy,dx))
end
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.ang = 0
end
function draw()
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawAsteroids()
popStyle()
end
function drawButtons()
pushStyle()
ellipseMode(RADIUS)
stroke(255)
strokeWidth(1)
for i,b in ipairs(Buttons) do
pushMatrix()
translate(b.x,b.y)
ellipse(0,0, 50)
popMatrix()
end
popStyle()
end
function drawShip()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(Ship.pos.x, Ship.pos.y)
rotate(Ship.ang)
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
function moveShip()
end
function drawAsteroids()
pushStyle()
stroke(255)
fill(0,0,0, 0)
strokeWidth(2)
rectMode(CENTER)
for i,asteroid in ipairs(Asteroids) do
drawAsteroid(asteroid)
moveAsteroid(asteroid)
end
popStyle()
end
function drawAsteroid(asteroid)
rect(asteroid.pos.x, asteroid.pos.y, 120)
end
function moveAsteroid(asteroid)
local step = vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function keepInBounds(value, bound)
return (value+bound)%bound
end
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end