Wiki
Clone wikiCore / TouchTutorial
Interacting With the User
An obvious thing to do after having written a simple program is to make it so that the user can interact with it. The most obvious way that a user interacts with an iPad is by touching the screen. In this tutorial we're going to write a program which allows the user to change what's on the screen.
We'll start with our basic program from the Animation Tutorial (Getting Things Moving). This was the following:
-- Use this function to perform your initial setup
function setup()
print("Hello World!")
x = WIDTH/2
y = HEIGHT/2
end
-- This function gets called once every frame
function draw()
background(0,0,0,255)
x = x + 1
ellipse(x,y,100)
end
Recall that this draws a circle which starts at the centre of the screen and moves off to the left. What we'd like to do is make it so that the ellipse appears where the user touches. This means that we need to know the coordinates of where the user is touching and feed them back to the program to set the coordinates for the ellipse.
Interpreting Touches
Before we begin, we need to know a little bit about touches and how Codea interprets them. From the user's point of view, a basic touch consists of putting a finger on the screen, moving it around, and then lifting it off again. From the program's point of view, therefore, it needs to detect when a user puts their finger on the screen, then track it as it moves, and finally record when it leaves. Thus a touch has three "states": BEGAN
, MOVING
, and ENDED
. The BEGAN
state signifies that the user has put their finger on the screen. The ENDED
state signifies that the user has lifted their finger off the screen. In between, the touch is MOVING
(even if, technically, it isn't).
Now we need to know how Codea tells a program about touches and their states. The basic cycle of a Codea program is a loop. The setup
function gets executed at the start, and then the draw
function gets executed repeatedly. In between each call of the draw
function, Codea calls another function, called touched
, to handle the touches. This function gets called once for each "active touch" (the user can touch the screen with more than one finger simultaneously). The touch that it is called for is held in a special variable called touch
. So within the touched
function, we can find out about the touch by looking at the touch
variable.
So let us imagine the situation. The user touches the screen. At the next opportunity (ie after the current draw
iteration is finished), the touched
function is called with this touch. As this is the first time for this touch, it is in the BEGAN
state. The user continues to touch the screen. Whilst the user has their finger on the screen, every time around the loop the touched
function gets called with this touch, but now in the MOVING
state. Finally, the user lifts their finger from the screen. On the next time around, the touched
function is called once more and now with the touch in state ENDED
.
Phew. It's probably a lot easier to understand in practice.
A Simple Program
Let's change our program to make it so that the ellipse is located at the point where the user is touching the screen. So when the user is touching the screen, we need to update the coordinates of the ellipse to match the coordinates of the touch. The coordinates of the touch are stored as touch.x
and touch.y
(remember that in the touched
function then the variable touch
refers to the "touch under consideration", it is possible to get this information in other places in the program but you have to use a slightly different syntax).
A very simple touched
function would simply set x
and y
accordingly:
function touched(touch)
x = touch.x
y = touch.y
end
Note that where the draw
and setup
were defines as function draw()
, this one has a word between the parentheses. This is because when this is called, it will be given the information about the touch. Within the function we need to have a variable to refer to that. The (touch)
is what says that we're going to use touch
to refer to the touch under consideration. So actually I lied above when I implied that it is Codea that decides to refer to the touch under consideration as touch
: actually, we do. But it's a good habit to stick with.
Now, the above routine will get called every time there is a touch in play. So that will update the coordinates of the ellipse whenever the screen is touched. Add that to the code that we already have.
When you run this program, the ellipse will drift off to the right. Then when you touch the screen, the ellipse appears under your finger. As you move your finger, it should follow. Then when you lift your finger off the screen, it will resume its drift to the right.
More Advanced Use
Since we went to all that trouble to introduce the state of a touch, let's use it. We can query the state and change something accordingly. For something simple, let's change the colour of the ellipse.
Setting the colour of an object like an ellipse is done by the fill
command. It is called as fill(colour)
where colour
can be either a "color" object or the specification of a colour using Red-Green-Blue-Alpha syntax. As we'll want to change it, it's easier to use the special "color" object. This is a special variable that refers to a colour. We need an initial value, so in the setup
function we add the line:
c = color(255,255,0,255)
This specifies a colour with maximum red and green and no blue (so it's yellow). The last number specifies the transparency of the colour: 255 means that it is opaque.
In the draw
function, we need to use this colour. So before the ellipse
line, we add:
fill(c)
(If you want to stop the ellipse drifting, you can remove the x = x + 1
line.)
Running the program now, we should see that the ellipse is now yellow.
We want to change this according to the state of the touch. So in the touched
function we need to find out what the state actually is. We do this with a conditional. The syntax is simple: if something then do-something end
. The first "something" is the condition that we want to test, in this case: is the touch in the BEGAN
state? The state of the touch is stored as touch.state
so we can write if touch.state == BEGAN then ...
. The ==
tests for equality (it is doubled to distinguish it from the assignment operator which is =
). The "do-something" is the action that should be taken. What we want to do is to set the colour of the ellipse. As the colour is set using the variable c
, we just need to update that. So in the touched
function we can write:
if touch.state == BEGAN then
c = color(255,0,0,255)
end
If we run the program now, we see that the ellipse turns red when we touch it. However, it stays red which perhaps isn't what we want. We can fix that by testing for the other states:
if touch.state == MOVING then
c = color(0,255,0,255)
end
if touch.state == ENDED then
c = color(255,255,0,255)
end
When this runs, then you see the ellipse change colours as it goes through the states. It briefly turns red, then to green while you keep your finger on it, and turns back to yellow when you lift your finger off.
Here's the full code, minus the drift (and it's time we ditched that "hello world" as well).
function setup()
x = WIDTH/2
y = HEIGHT/2
c = color(255,255,0,255)
end
function draw()
background(0,0,0,255)
fill(c)
ellipse(x,y,100)
end
function touched(touch)
x = touch.x
y = touch.y
if touch.state == BEGAN then
c = color(255,0,0,255)
end
if touch.state == MOVING then
c = color(0,255,0,255)
end
if touch.state == ENDED then
c = color(255,255,0,255)
end
end
Even More Advanced
The touch
variable holds more than the coordinates and the state. It also holds the previous coordinates and the change in coordinates. It's also possible to find out if the touch was actually a tap. Lastly, each touch is assigned a unique id which makes it easier to ensure that you follow a specific touch when there are multiple touches to consider.
Here's a program that makes use of some more of that information. I'll give the code first and then talk you through it.
supportedOrientations(LANDSCAPE_ANY)
function setup()
x, y = WIDTH / 2, HEIGHT / 2 -- Initialise the ball in the centre of the Viewer pane
bc = color(248, 250, 13) -- The (default) ball colour
tc = color(195, 17, 17) -- The ball-if-touched colour
inTouch = false -- Flag for Viewer pane being touched
dx, dy = 0, 0 -- Initialse the velocity of the ball per frame, to zero
r = 30 -- Initialise the radius of the ball
end
function draw()
background(0) -- Clear the background to black
local c -- Will hold the colour of the ball
if inTouch then -- Is the Viewer pane being touched?
x, y = tx, ty -- Yes: Set the position of the ball to that of the touch
c = tc -- Set the current colour for the ball
else -- Otherwise, if not touched:
x, y = x + dx, y + dy -- Change the position of the ball by the velocity
c = bc -- Set the current colour
-- Check to see if ball has passed through boundaries and, if so, bounce
-- Boundary at x = r (allow for ball radius):
if x < r then
dx = - dx -- Reverse the velocity
x = 2 * r - x -- Reposition the ball:
-- r + (r - x)
end
-- Boundary at X = WIDTH - r (allow for ball radius):
if x > WIDTH - r then
dx = - dx
x = 2 * (WIDTH - r) - x
end
if y < r then
dy = - dy
y = 2 * r - y
end
if y > HEIGHT - r then
dy = - dy -- Reverse the velocity
y = 2 * (HEIGHT - r) - y -- Reposition the ball
end
end
fill(c) -- Set the fill colour
ellipse(x, y, 2 * r) -- Draw the ball
end
function touched(touch)
tx, ty = touch.x, touch.y -- Record the position of the touch
if touch.state == BEGAN then -- Touch is beginning?
inTouch = true -- Yes: Set flag
velocity = {} -- Clear table of velocities
dx, dy = 0, 0 -- Set velocity of the ball to zero
end
if touch.state == MOVING then -- Moving?
-- Save movement as a vec2
local newVelocity = vec2(touch.deltaX, touch.deltaY)
-- Add to front of table of velocities
table.insert(velocity, 1, newVelocity)
end
if touch.state == ENDED then -- Touch has ended?
local n = 0 -- Yes: Count velocities in table
for i = 1, 10 do -- Get up to the last 10 entries
if velocity[i] then -- If it exists ...
n = n + 1 -- Increase the count
dx = dx + velocity[i].x -- Add the velocity ...
dy = dy + velocity[i].y -- ... to the total.
end
end
if n > 0 then -- Positive count?
dx, dy = dx / n, dy / n -- Yes: Calculate the average velocity
end
inTouch = false -- Clear the flag
end
end
(The code may not be the most elegant!)
It will be useful to get this code into a program and run it before the explanation. When you start it, there's just a yellow circle in the middle. Now touch the screen, and drag your finger around a bit before letting go. Fun?
Okay, let's go through it step by step. The setup
function is fairly simple: it just initialises all of the variables. Let's list them and what they'll be used for:
Variable | Use | Initial Value |
---|---|---|
x | x coordinate of circle | middle of screen |
y | y coordinate of circle | middle of screen |
bc | colour of ellipse when not touched | something vaguely yellow |
tc | colour of ellipse when touched | something a bit redish |
inTouch | are we currently being touched? | false |
dx | amount to change x by between frames | 0 |
dy | amount to change y by between frames | 0 |
r | radius of circle | 30 |
The draw
function is a little more complicated than we've seen so far, but not too much. We draw everything at the end (the final fill
, and ellipse
lines) and first update our data for the current frame (otherwise our picture would be of the previous values; you can alter this to see if you notice the difference by putting the drawing commands at the start of this function).
Exactly how the data is updated depends on whether we are currently being touched or not. If we are then we set the coordinates of the ellipse to those of the touch and set the colour to the tc
colour. If we are not currently being touched then we change our coordinates by dx
and dy
(recall that these were initially 0) and set our colour to bc
.
There is then a further set of things to check. If our increments, dx
and dy
, are non-zero then eventually the circle will leave the screen. These last checks, basically, stop that happening. If we get too close to one of the edges of the screen then the relevant increment is reversed, taking us back in to the centre again.
The interesting stuff happens in the touched
function. The first bits are simple to understand: we set the coordinates tx
and ty
to the coordinates of the touch so that the draw
function can use them to position the circle. If the state is BEGAN
then we set inTouch
to true
, note that when we check whether the state is ENDED
then we reset inTouch
to false
.
That's the simple stuff. What's all the rest? If you've played with the program, you'll have an idea: it's to set the velocity of the circle when it is released. The result of the rest of the function is to set dx
and dy
so that in the draw
routine then the circle position is changed each frame to give the appearance of having been "thrown" at the end of the touch.
To do this, we want to know the change at the end. This is stored as touch.deltaX
and touch.deltaY
. These contain the final increments in the touch position. The problem is that these might not be the best values to use. You can try this out for yourself: at the end of the if touch.state === ENDED then ... end
part (ie before the second last end
in the program) insert the lines dx = touch.deltaX
and dy = touch.deltaY
. Then try the program. Sometimes it works, and the circle launches as you'd expect, but sometimes it appears to stumble and not go as fast as it should.
The explanation is simple: when a touch ends, the touched
routine is called. But it isn't called at the instant that the touch ends. It is called when the draw
function is finished. By setting dx = touch.deltaX
(and likewise for dy
) we are saying "In each subsequent frame, move as much as the touch did in the last frame." But in that last frame, it didn't move very far because it ended midway. This leads to a low estimate of the final velocity.
To correct for this, we average the increment over the last ten frames. An alternative would be simply to take the one-before-last (if you want to try this, remove the whole if touch.state == ENDED then ... end
bit and change the MOVING
bit to dx = touch.deltaX dy = touch.deltaY
). So each time we are in the MOVING
state, we record our deltas. Then at the end, we average the last ten, or the last n
if we haven't managed to get to 10.
It is perhaps worth noting how we do this recording. When we BEGAN
the touch, we cleared a "table" for these deltas. This is the velocity={}
line. Then each time we are in the MOVING
state, we save the deltas to this table. We save them as a 2-vector, so that we save just one thing, and we put it at the front of the table. This is the table.insert
line. Then when we get to the ENDED
state, we try to read the first ten things in this table (which, because we added at the front, are the last ten deltas). To allow for there not being ten of them, we keep track of how many are actually defined. Then we add up all the deltaX
s and deltaY
s and divide by the number that there are (with a bit of error-checking to ensure we don't do anything stupid like dividing by 0).
Updated