Forum Archive

More Scene Help

Cubbarooney104

Hit boxes. The bane of my existence (at the moment at least).

I'm referring to hit boxes as in a button. If you press the button, something happens. If you press anywhere other else on the screen however, nothing happens.

The way I figured out how to do this is to create a rectangle with many if statements.
(If this didn't make sense I'll provide an example).

This method, however, requires a lot of code. And I'm going to have five buttons at least. There has to be an easier way!

That is where you (yes, you on the other side of the screen) come in. If you know how, then I'd like to know.

Thanks in advance,
Cubbarooney

omz

There is an easier way. The scene module has Rect and Point classes that can be used with the in operator. You can see an example of this in the included "Cascade" example. Have a look at the hit_test method in the Tile class.

If you're working with layers, a typical usage would be to check whether a touch is inside a layer's frame. This can be done very easily like this:

def touch_began(self, touch):
    if touch.location in self.some_layer.frame:
        # do something...
Cubbarooney104

Thanks for your helpful (and speedy) reply! That being said, I'm still having some "slight" trouble... As I'm not using layers, I'm trying to use the "Cascade" method.

I think the root of my trouble stems from the inputs into the "Tile" class and then the "hit_test" function.

I'll walk through the following code so you can (hopefully) understand what is going on in my brain:

   def hit_test(self, touch):
        frame = Rect(self.x * tile_size + self.offset.x,
                     self.y * tile_size + self.offset.y,
                     tile_size, tile_size)
        return touch.location in frame

So let me see...
Line 1) Basic enough. creates the function and has the inputs self and touch. Touch consists of touch.x and touch.y.
Line 2) creates the variable frame. QUESTION: is frame just the name you used, or does it have to be frame?
Frame represents a rectangle. the x value is (ignoring tile_size and self.offset.x/self.offset.y) self.x (which was defined in the initial setup. I'll go through that later), the y value is self.y, and then the width and height is tile_size (defined earlier).
Line 3) return (as a value) "touch.location in frame". So, in the variable that we created above is somewhere touch.location. But isn't it one of the inputs?

Now to the initial setup (which I probably should have done first):

   def __init__(self, image, x, y):
        self.offset = Point() # used for falling animation
        self.selected = False
        self.image = image
        self.x, self.y = x, y

So now we have four inputs. self, image, x, and y. Only problem is, where do they come from? Also, it appears to me that "self.selected = False" and "self. image = image" aren't used in the "Tile" class (are they, perhaps, carried out of the class into the "Game" class?). Nonetheless, where does x and y come from?

Well, it seems to me that the input from the "main" code is as follows:

for i in xrange(cols * rows):
tile = Tile(images[randint(0, len(images)-1)], i % cols, i / cols)

I have no clue what that means... I take that back, I think I understand it a little. The "images[randint(0, len(images)-1)]" is the image input, "i%cols" is x, and "i/cols" is the y. I don't fully get the meaning of each though.

If I didn't make sense (or left something out) let me know and I'll clarify/modify the post. Thanks for dealing with rookies like me.

Cubbarooney

Sebastian

From the documentation:
Points support the in operator to test whether they lie within a given Rect.

Because touch.location is a Point, and some_layer.frame is a Rect, you can use it like omz described. i.e:

if touch.location in self.some_layer.frame:
        # do something...

That means you can also do this:

if some_point in some_rect:
        # do something...

Here's how I would check if a button was touched:

from scene import *

class Test(Scene):
    def setup(self):
        self.button = Rect(self.size.w/2-100, self.size.h/2-100, 200, 200)

        self.button_colour = Color(1,0,0)

    def draw(self):
        background(0,0,0)

        fill(*self.button_colour)
        rect(*self.button)

    def touch_began(self, touch):
        # if touch.location is inside button
        if touch.location in self.button:
            self.button_colour = Color(0,1,0)
        # if touch.location is anywhere else
        else: 
            self.button_colour = Color(1,0,0)

run(Test())
omz

"Line 2) creates the variable frame. QUESTION: is frame just the name you used, or does it have to be frame?"

Doesn't matter, it's just a variable name.

"Line 3) return (as a value) "touch.location in frame". So, in the variable that we created above is somewhere touch.location. But isn't it one of the inputs?"

It returns True if the location of the touch is within the bounds of the rectangle (frame) created earlier, and False otherwise. This works because the Rect class overloads the in operator. Semantically, you could think of it as frame.contains(touch.location) (which wouldn't work, this is just to illustrate what it means).

"So now we have four inputs. self, image, x, and y. Only problem is, where do they come from?"

They are passed as arguments when creating a Tile object. You can see these arguments in the new_game method.

"Also, it appears to me that "self.selected = False" and "self. image = image" aren't used in the "Tile" class (are they, perhaps, carried out of the class into the "Game" class?)."

You're right that the selected and image attributes aren't used directly in the Tile class. They're used in the draw method of the Game class.

"I have no clue what that means... I take that back, I think I understand it a little. The "images[randint(0, len(images)-1)]" is the image input, "i%cols" is x, and "i/cols" is the y. I don't fully get the meaning of each though."

Well, you understood most of it. ;) The rest is just simple math. % is the modulo operator, in case you didn't know that, it returns the rest of a division. The for-loop basically just sets up the x and y positions of the tiles based on the counter variable i. You can think of it as going through a grid with numbered tiles, like this:

0  1  2  3
4  5  6  7
8  9 10 11

The tile labelled as 6 would have an x value of 2 (counted from zero) and a y value of 1, which leads us to y = 6 / 4 = 1 (rounded down because this is an integer division) and x = 6 % 4 = 2 (the rest of the division).

To be honest, this would probably be easier to understand when written as two nested loops which would give the same result:

for x in xrange(cols):
    for y in xrange(rows):
        tile = Tile(image, x, y)

(don't get confused by the name of the xrange function, the x here has nothing to do with the coordinates, it's just a slightly more efficient variant of the range function that is often used for loops.)

jose3f23

My two cents:

from scene import *

class Label():
    def __init__(self):
        self.text = ''

    def draw(self):
        text(self.text, font_size = 16, x = 200, y = 200, alignment = 9)

    #  -------------------   end class label ---------------

class Buttons(Layer):
    def __init__(self):
        self.bstr=['BUT1','BUT2','BUT3','BUT4']

    def draw(self):
        tint(0.1,0.1,0.1)
        for i in range(4):
            rect(10+80*i,10,70,50)
            text(self.bstr[i], font_size = 16, x=40+80*i, y = 35, alignment = 5)

    def pressed(self,x,y):
        n='NONE'
        if y>80: return 'NONE'
        for i in range(4):
            if ((x > 80*i) and (x < 80+80*i)): n = i
        return self.bstr[n]  
#  -------------------   end class buttons ---------------

class MyScene (Scene):
    def setup(self):
        self.but='NONE'
        self.buttons=Buttons()
        self.label = Label()

    def draw(self):
        background(0.9, 0.9, 0.9)
        self.buttons.draw()
        self.label.draw()

    def touch_began(self, touch):
        pass

    def touch_moved(self, touch):
        pass

    def touch_ended(self, touch):
        self.but=self.buttons.pressed(touch.location.x,touch.location.y)
        self.label.text=self.but


run(MyScene())
Cubbarooney104

@Sebastian
Thanks for the simple code! That (combined with omz) really explained the hit box.
@omz
Thanks for the very detailed explanations! Cascade makes more sense now, as does hit boxes. I learned quite a lot today :)
@jose3f
I haven't looked at yours yet (only just finished supper), but I'll look at it right away! Then I'll comment again.

Cubbarooney

eliskan175

Nice, I learned something new too! Didn't know xrange was better than range for iterating through loops. Apparently, the only time we should use range is when we actually need the list.

Gotta love lurking other peoples threads haha

Now for my contribution...

@Cubbarooney - Hit tests are easier than they appear, depending on what you need them to do. If you look at some of my projects, you will see they all contain a hit test function (the best one imo is the one I'm using in my Space Shooter and RTS demo, which checks a single point to see if it's hitting a box). I use these functions because often times the object I'm checking to be hit isn't technically a Rect object, they are images. Either way it's identical to testing a rectangle.

Hit testing circles is a bit trickier especially if you're like me and didn't pay much attention in geometry. Here's a few working examples of hit tests that I did: http://omz-software.com/pythonista/forums/discussion/138/crude-hittest#Item_2