A different approach to buttons

I am a complete newbie to writing code, having only started with codea six or seven months ago. And having never even dabbled in programming before that. So if it seems presumptuous of me to attempt a tutorial type post I sincerely apologize, but a friend of mine urged me to do this, so here goes.
Early on I found some really great tutorials on buttons and basically it came down to

If touch.x > than button.x - size/2 and touch.x < button.x + size/2
Then repeat that for y.

Not really difficult but when you start to have multiple it can get cumbersome.

I did find that if you were dealing with round or square buttons you could do this;

Make your touch a vec2 so, tvec =vec2(touch.x,touch.y)

Then use that to get the distance between the touch and the button center, so;

tDist = tvec:dist(button)
If tDist < size/2 then do whatever your button does.

But you still have to write this out for every button.

So I came up with this and I guess I am asking if this makes sense and seems practical?

Full code here

https://gist.github.com/Circussmith/7450391

For the purposes of this type of button we will think of the screen as a grid. Ignatz does a great tutorial on tables were he describes how to set up a grid and then place stuff in it. For our purposes we won’t need to actually create a grid table, just think of the screen that way for now.

First I set up a table for all of the buttons. Then I set up the Boolean that determine if we do something or not. This is what the button will change in this simple example. Then I set up another one that we will use later to make sure the action doesn’t take place until after the button has been released.

Now we create a vec2 that we will use to calculate both the size and position of the button. This is where the grid idea comes in. Thinking of the screen as a grid of so many positions (buttons) wide and so many high we decide how big we want our button. In this case I decided about 100 pixels wide by 75 high. Or in other words a 10 x 10 grid. I divide the screen width and height by my grid numbers, 10 and 10 and I get, rounded out, 102 and 77. We put that into btnSize, and it will, in fact be our buttons size but we will also use it for other calculations.

Lastly, we call the function that actually creates our buttons.


Button = class()

function Button:init()
    btnTab = {}
    txt = false
    chBtn = false
    self.btnSize = vec2(102,77)
    self:makeButton()
end

Now we make our buttons. First we define the image we want for each button. We then insert into our buttons table all of the info that button will need. The first thing you see there is x and y. These are the grid coordinates based on our 10 x 10 grid.

IMPORTANT NOTE - the grid starts at 0,0 in the lower left corner, I will explain later why.

IN this case the first button is going to show up at 2,2 on our grid. So pretty low and to the left.

In the table we say size = btnSize.

Next we do what I think is a really cool thing. As I said, I have only ever used codea so I don’t know if it is a standard thing or not. We insert an orphan function into our table. We’re gonna go all “please sir, I’d like some more” on this tables ass. We say btnDoes and then add a function that has no name and only exists in the table and gets called when the this particular button is touched. This function does whatever we want this button to do. In this case we are setting txt to true or false but it could be any number of things, including changing parameters or changing the state our app is in. We can actually use this concept to determine what buttons we are drawing. I plan on getting to that later but if I don’t please feel free to ask me.


function Button:makeButton()

    local fImg = readImage("Tyrian Remastered:Arrow Left")
    local bImg = readImage("Tyrian Remastered:Arrow Right")
    table.insert(btnTab,{x = 2,y = 2,img = fImg,size = self.btnSize,btnDoes = function() txt=false end})
    table.insert(btnTab,{x = 6, y = 2,img = bImg,size = self.btnSize,btnDoes = function() txt = true end})
end

This is where we draw the buttons.

NOTE FOR BEGINNERS LIKE ME - Creating a button as a “thing” and drawing it are two very different things. The buttons we created in the makeButton function are real in terms of the code, but they are invisible in terms of seeing them on the screen. In other words we could make them and interact with them but never actually see them on the screen. Drawing them doesn’t really affect what they do, it just gives us a visual reference to know where to touch the screen.

I know that’s a little esoteric but when you are writing the code it becomes important. In this case everything they actually do happens under the checkButton function and not the draw function. It can seem like a trivial distinction but it can become really important as your code gets more complicated.

ok, let’s look at the draw function.

first we run through the btnTab and draw our buttons. It is important to set the spriteMode to CORNER here for reasons I will explain in a bit. We take local x ( use local whenever possible to speed up your code as global variables take longer to process) and multiply it by our btnSize variable, same with y. This gives us the actual pixel positions for the sprite. We have already defined our image as img in our buttons table as well as our size and position.

Well the position is a little different. We defined that by a grid position (2,2). Now we are using our btnSize variable to convert it back into pixel coordinates. So multiplying our grid coordinates by our btnSize we get a corner position of 204,154. But honestly, we don’t need to worry about that as long as we are thinking in terms of a 10,10 grid.

Now we say if txt is true type “It Worked!” In the center of the screen. For simplicity’s sake I am putting this here but it could be in another function that is called from the main draw function.


function Button:draw()
    for i,v in ipairs (btnTab) do
        pushStyle()
        spriteMode(CORNER)
        local x = v.x * self.btnSize.x
        local y = v.y * self.btnSize.y
        sprite(v.img,x,y,v.size.x,v.size.y)
    end
    if txt == true then
        fill(255, 255, 255, 255)
        fontSize(40)
        text("It Worked!",WIDTH/2,HEIGHT/2)
    end
end

And this is where the rubber meets the road. Here we cycle through the buttons table and see if we are actually touching an active button. We are calling this from the button:touched function. Sending our x and y grid coordinates through in the (). This is where we send through the 2,2 numbers. In the next section I explain how we convert the actual touch.x and touch,y to those simple grid numbers. Once we determine we are touching the right coordinate we then call the orphan function from that button table to do what it needs to do. In this case change txt to true or false, but again, it could be anything.


function Button:checkButton(x,y)
    for i,v in ipairs(btnTab) do
        if v.x == x and v.y == y then
            v.btnDoes()
        end
    end
end

We can see the button and we have touched it, so now what?

The first thing we do is make sure nothing happens until we have released the touch. This is a bit of a personal preference but I like things to happen after I release the button, not the instant I touch it. When we touch it it sets the chBtn variable to true, then when we let go it checks to see if chBtn is true. We do this because the normal state is ENDED so we have to something else in there to help it make that decision to activate the button.

so this is kind of cool. first we take touch x and y and divide them by btnSize. However there is a problem.

If we divide touch x 422 and y 572 by btnSize we get x = 4.13 y = 7.4, not an exact grid coord for comparisons sake.

Now, if we run that number through math.random it returns the number with no decimal values. I feel like there is a better command for this and would be open to suggestions, but this does work.

So we put in 4.13 and 7.4 and we get back 4 and 7, which we can use as a coord.

If you notice, these numbers are the lowest possible for that grid coord. Basically x is farthest left possible since 4.1 would be a bit right of that and 7.4 is a bit higher than 7. When we convert these back to pixels what we get is the bottom left corner of each grid position. Which is why we set spriteMode to corner and why our grid starts at 0,0 (our first possible touches, once divided by btnSize are less than 1 so come back 0)

We then send the coords through to checkButton to do their thing.


function Button:touched(touch)

    if touch.state == BEGAN then
        chBtn = true
    end
    if chBtn == true and touch.state == ENDED then
        local bx = touch.x/self.btnSize.x
        local by = touch.y/self.btnSize.y
        local z = math.random(bx,bx)
        local d = math.random(by,by)
        chBtn = false
        self:checkButton(z,d)
    end    
end

call the button class from main and draw and add touched(touch) function to your main tab and then call button touched(touch) from there.
That’s it. The cool thing about this is to add as many buttons as you want you simply have add on line of code under the makeButton function. There is also a way to determine which buttons you want to show using a state machine. Under draw and checkButton you put in a line like if v.state == state then… And in the table for each button you put in the state you want them to show up in.

I hope this is helpful and please let me know any suggestions you might have.

@Cirussmith Great tutorial!

Thanks! I really like this approach but honestly I am so new at this that I could be missing something incredibly obvious.

Compacting an if statement:

if vec2(touch.x, touch.y):dist(buttonx, buttony) <= buttonsize then

I don’t think this is very good for square buttons, though, as if you use a circle bounding box then, if, say, you touched one of the corners it wouldn’t work, and it wouldn’t work for rectangles or ellipses.

A more compact way for rectangles is:

if math.abs(touch.x - buttonx) <= buttonsize and math.abs(touch.y - buttony) <= buttonsize then

@SkyTheCoder You are right, on square buttons you do miss the corners. Generally though if the buttons are not overly large it works. But I really like the way you compacted it. I hadn’t thought of that. Thanks.

@Circussmith - it’s nice to see someone having a go at a beginner tutorial, and I know from my own experience that you learn a lot yourself from doing it. Also, trying something different really makes you think for yourself.

I have some comments and suggestions, but don’t take them as being negative. I often find when I suggest something in the forum, someone else comes back with a better idea, so I learn from it. (And someone is sure to improve on anything I say here!).

Design

First, you imply you have to write touch testing code separately for every button, as a reason for going to a grid instead. Actually, you don’t. You can write one testing function that is given a button, and tests for a touch based on that button’s width and height. Then it is easy to loop through all the buttons and test (exactly as you have done for the buttons in your grid). This is actually considerably simpler and more flexible than a grid based system (because the buttons can be put anywhere, and different sizes, if you like).

So, IMHO, I don’t think a grid system is better. That doesn’t mean your effort is wasted, though. Grid based systems can be used for many things, especially tile based games, so getting practice in it is important.

I also don’t think a class is necessary in this case. A table would be enough, because the class doesn’t really contain much “personal information” about each button, other than x,y position and the function attached to it. That’s not really a criticism, just that a table is simpler.

Code comments

It would add flexibility if the user can make the button grid any size they want. The width and height could be set initially. This could either be done outside the button class, using global values, or perhaps more neatly if you include a class variable, eg

Button=class()

Button.size=vec2(0,0)   --NEW

function Button:init()
etc

Then in your setup function, you can set this variable with

Button.size=vec2(100,75) 

and this can be applied to all buttons created with the class (you will reference it inside the class as Button.size.x and Button.size.y, ie with dots, not colons).

The main reason for using locals is to avoid name clashes in your code, eg using the variable “b” in three separate places to mean different things. Using locals limits the “scope” of variables to a function (or even to a loop or if statement within a function). Faster processing is also a benefit, but most of the time you won’t notice the difference, so it is generally much less important.

An important reason for basing touch on the ENDED state is that if you don’t, you are going to get at least two touches registered, one for BEGAN and one for ENDED, and maybe some additional MOVING touches too. You only want one.

math.floor will give you the integer value of your fractional square position, and you should google fmod and modf as well.

I always find it useful to include some test code so people can run it to see how it all works. For example, you mention a state machine in passing at the end, but this may not be at all clear to people who have never heard of it or used it before. A simple example would be nice.

Finally, I’d like to say again that it’s great to see you having a go at a tutorial like this and taking the risk of sharing it. I would much rather respond to a post like this than to someone who says “I need X. Please send me the codes”.

So please don’t let me put you off doing more!

=D>

@Circussmith - one extra point. Something I learned from an article on side scrolling games, which suggested that when creating things that the user touches/clicks on, make the touching area bigger than the object, because people are not very accurate with their touches. So with an iPad, test for a touch that is slightly bigger than your button.

Now you can do this easily with your grid by making the grid a bit larger than your button, which also helps in spacing the buttons out so you don’t hit the wrong one accidentally.

If you were just using the normal distance check (as SkyTheCoder suggested above) then you could multiply it by (say) 1.2 when testing, to give the user more chance of hitting the button.

This also means you shouldn’t be worried if your buttons are rounded but you are testing using a rectangle. We have fat fingers, and our touches are very approximate.

@ Ignatz thanks for responding! I am going to be brief here as I am rushing off so I will probably be back with some specific questions for you. First, I have to tell you I both, really hoped, and was really afraid, that you would reply. As soon as I started reading your reply I immediately thought, I am not a smart man, why can’t you run the touch test once and then cycle through the buttons.

I did know that the main reason you activated the button ENDED was to avoid multiple touches but for some reason forgot to put that point in there.

I did find this to be a very useful exercise and will probably do more, most likely with the same results. In fact once I actually figure out all the points you made I may rewrite it using those improvements, if you don’t mind. I do feel like there is a need for more info on buttons, but it could be that I just haven’t come across it.

Thanks again, and I will be back with some specific questions.

@Circussmith thanks for the tutorial found it helpful, been altering your code to try new things. Comment out the two table inserts and replaced with below.

for xx = 1,6 do
        for yy = 1,6 do
            table.insert(btnTab,{x = xx,y = yy,img = fImg,size = self.btnSize,btnDoes = function() txt=false end})
        end
    end
    ax=math.random(1,6)
    ay=math.random(1,6)
    table.insert(btnTab,{x = ax,y = ay,img = fImg,size = self.btnSize,btnDoes = function() txt = true end})

Any comments would be very grateful.

Sorry about the code layout still have not got it right

@Jazmal You have the 3~'s before and after your code, but they have to be on a seperate line. If you do a “preview” before you post the code, you’ll see what the code looks like before actually posting it. If you want to try it, you can select “edit” and fix the above code and save it again.

I fixed the layout, Jazmal, press edit on your post and see where to put the three ~

Thank guys when I did preview it , it was on separate lines until I posted. But will try to remember the 3 ~ b4 and after

@Jazmal so you made it a sort of guess where the button is game. At least that’s what it did when I tried it. That’s pretty cool.

I don’t know where you were going with this but it gave me this idea. What if you did this where you replaced the table insert part.

for xx = 1,6 do
        for yy = 1,6 do
            table.insert(btnTab,{x = xx,y = yy,img = fImg,size = self.btnSize,btnDoes = function() txt=false  btnGone = true end})
        end
    end
    ax=math.random(1,6)
    ay=math.random(1,6)
    table.insert(btnTab,{x = ax,y = ay,img = fImg,size = self.btnSize,btnDoes = function() txt = true 
        btnGone = false end})

All I did there was add another variable, the btnGone Boolean. You need to set that to false in your init.

But now we can check that variable in the function Button:check() like this. I only added the if then part to this function but now it looks like this;

function Button:checkButton(w,h)
    
    for i,v in ipairs(btnTab) do
        if v.x == w and v.y == h then
            v.btnDoes()
            if btnGone then
                table.remove(btnTab,i)
                btnGone = false
            end
        end
    end
end

So now when you touch a button that doesn’t trigger the “it worked!” It removes that button from the screen. Then I thought, what else can we do that’s pretty simple. So try this and tell me what you think. Insert this at the bottom of your Button:draw() function

if #btnTab < 30 then
        pushStyle()
        spriteMode(CENTER)
        sprite("Tyrian Remastered:Explosion Huge",WIDTH/2,HEIGHT/2,math.random(480,520))
        popStyle()
    end

Also, I am going to go back in and fix some of the things @Ignatz recommended. He made some great points