Wiki

Clone wiki

Core / 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:

VariableUseInitial Value
xx coordinate of circlemiddle of screen
yy coordinate of circlemiddle of screen
bccolour of ellipse when not touchedsomething vaguely yellow
tccolour of ellipse when touchedsomething a bit redish
inTouchare we currently being touched?false
dxamount to change x by between frames0
dyamount to change y by between frames0
rradius of circle30

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 deltaXs and deltaYs 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