Search code examples
pythonkivyclockattributeerrorevent-binding

Clock.unschedule not working when referenced with bind() in kivy


I'm making a game with kivy for android that has the player controlled by touching on certain areas on the screen. I create invisible buttons (char_controls) that when tapped down, the character moves. The moment the finger is released, I would like the character to stop.

I've bound a function to each button that calls Clock.schedule_interval on a move_up function in the character class (only working with the up button right now). When the button is released, it calls another function that should unschedule the original function (with Clock.unschedule). However, it doesn't do this, and the character keeps on moving.

Am I misusing kivy's bind() function when I use it to bind the button's on_press and on_release behaviours to functions defined in another class? I've noticed that I get an AttributeError: 'Button' object has no attribute 'move_up' when I use self to reference move_up — I must instead refer to the move_up function as character.move_up even when I am referencing it in the character class. If the issue doesn't have to do with the bind() function, how can I make the program unschedule the move_up function?

Below is my code:

from kivy.uix.widget import Widget
from kivy.graphics import Canvas, Rectangle, Color
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.properties import *
from kivy.core.window import Window
from main import *
from render import Layer


class char_controls(FloatLayout):
    '''controls where the character moves. There are 4 regions where the player can tap:
       the top third to go up, the bottom third to go down, the center left to go left
       and the center right to go right. They are buttons.'''

    def __init__(self, **kwargs):
        super(char_controls, self).__init__(**kwargs)
        self.opacity = 0
        self.size = (Window.width, Window.height)

        anchor_bc = AnchorLayout(anchor_x = 'center', anchor_y = 'bottom')
        down_btn = Button(text='', size_hint = (1, 0.3333))
        down_btn.bind(on_press=character.move_down, on_release=character.stop)
        down_btn.bind(on_press=Layer.move_down, on_release=Layer.stop)
        anchor_bc.add_widget(down_btn)
        self.add_widget(anchor_bc)

        anchor_cl = AnchorLayout(anchor_x = 'left', anchor_y = 'center')
        left_btn = Button(text='', size_hint = (0.5, 0.3333))
        left_btn.bind(on_press=character.move_left, on_release=character.stop)
        left_btn.bind(on_press=Layer.move_left, on_release=Layer.stop)
        anchor_cl.add_widget(left_btn)
        self.add_widget(anchor_cl)

        anchor_cr = AnchorLayout(anchor_x = 'right', anchor_y = 'center')
        right_btn = Button(text='', size_hint = (0.5, 0.3333))
        right_btn.bind(on_press=character.move_right, on_release=character.stop)
        right_btn.bind(on_press=Layer.move_right, on_release=Layer.stop)
        anchor_cr.add_widget(right_btn)
        self.add_widget(anchor_cr)

        #button of interest
        anchor_tc = AnchorLayout(anchor_x = 'center', anchor_y = 'top')
        up_btn = Button(text='', size_hint = (1, 0.3333))
        up_btn.bind(on_press=character.schedule_up, on_release=character.stop)
        up_btn.bind(on_press=Layer.move_up, on_release=Layer.stop)
        anchor_tc.add_widget(up_btn)
        self.add_widget(anchor_tc)



class character(Widget):
    '''The character class.'''
    x_pos = 0
    y_pos = 0
    pos = (x_pos, y_pos)

    def __init__(self, **kwargs):
        super(character, self).__init__(**kwargs)
        with self.canvas:
            Color(1., 0, 0)
            character.sprite = Rectangle(pos=self.pos, size=(32, 32))


    #is there a cleaner way to call the movement functions than this? (Eg lambda)
    def schedule_up(self):
        Clock.schedule_interval(character.move_up, 1/30.)

    def move_up(self):
        character.y_pos += 1
        character.pos = (character.x_pos, character.y_pos)
        character.sprite.pos = character.pos
        print('run')

    def move_down(self):
        print('down')

    def move_right(self):
        print('right')

    def move_left(self):
        print('left')

    def stop(self):
        Clock.unschedule(character.move_up) #this is not actually unscheduling the move_up function.
        print('stop') #prints, so the function is working

Thanks in advance!


Solution

  • Watch the console:

    from kivy.base import runTouchApp
    from kivy.uix.boxlayout import BoxLayout
    class Test(BoxLayout):
        def __init__(self, **kw):
            super(Test,self).__init__(**kw)
            from kivy.clock import Clock
            func1=Test.foo
            print func1
            Clock.schedule_interval(func1,1)
            func2=Test.foo
            print func2
            print func1 is func2 # <--------------------Here(False) ^^
            Clock.unschedule(func2)
        def foo(self):
            print 'bar'
    runTouchApp(Test())
    

    You are using two different things character.<function> which is scheduled inside the class(seems weird to me if you have self accessible) and you unschedule the self.<function> another function that you only call the same way.

    The first is unbound method Test.foo, the second one is bound to a class another unbound that is not the same function, therefore you are unscheduling incorrect function i.e. the one that haven't been scheduled. Either use the same wording everywhere, or use it properly.

    Also what you want is class communication, which is explained in some questions under this tag e.g. here - using main App class to handle connecting.

    I'm not really sure if you are misusing bind() because I do these thing inside kv, but you should be able to use different keybords at once i.e. bind(on_press=...,on_release=...)