【问题标题】:How to manage event-handler recursion in python turtle?如何在 python turtle 中管理事件处理程序递归?
【发布时间】:2019-01-20 20:32:25
【问题描述】:

我一直在使用 Python 中的 turtle 模块开发 Pong 游戏。以下是我的代码:

from turtle import Turtle, _Screen, TurtleScreen
from random import choice, randrange, randint
from tkinter import *
from tkinter import messagebox

class Field(_Screen):
    def __init__(self, width = 1024, height = 600):
        # Get __init__ from _Screen
        super().__init__()
        # Get __init__ from TurtleScreen (parent class of _Screen)
        TurtleScreen.__init__(self, self._canvas)
        if Turtle._screen is None:
            Turtle._screen = self
        self.width = width
        self.height = height
        self.setup(self.width+100,self.height+50)
        self.screensize(self.width,self.height)
        self.title("Pong")
        self.bgcolor("black")
        # Define size of score bar, above the play field
        self.score_height = self.height/10
        # Offset 0 axis line, due to score bar
        self.yzero = -(self.height/2 - (self.height-self.score_height)/2)

class Ball(Turtle):
    def __init__(self, velocity = 5, size = 1, color = "white"):
        super().__init__(shape="circle",visible=False)
        self.color(color)
        self.speed(0)
        self.penup()
        self.shapesize(size,size)
        self.setposition(0,field.yzero)
        self.st()
        self.velocity = velocity
        self.dirrection = 0

    def player_collision(self,player):
        bx,by = self.position()
        px,py = player.position()
        x_off = ball.shapesize()[1]*10 + player.shapesize()[0]*10
        y_off = ball.shapesize()[0]*10 + player.shapesize()[1]*10
        if px > 0:
            if (bx > px-x_off and by <= py+y_off and by >= py-y_off):
                return True
        elif px < 0:
            if (bx < px+x_off and by <= py+y_off and by >= py-y_off):
                return True
        return False

    def court_collision(self,court):
        if (ball.ycor() >= ((field.height/2)-
                            court.score_height-self.shapesize()[0]*10)
            or ball.ycor() <= -court.height/2+10): return True
        return False

    def out_left(self,court):
        if self.xcor() <= -court.width/2: return True
        return False

    def out_right(self,court):
        if self.xcor() >= court.width/2: return True
        return False        

class Player(Turtle):
    def __init__(self, x=0, y=0, color="white", up=None, down=None):
        super().__init__(shape="square",visible=False)
        self.color(color)
        self.speed(0)
        self.penup()
        # setup player paddle
        self.shapesize(1,10)
        # Rotate turtle, to allow the use of forward method
        self.setheading(90)
        self.setposition(x,y)
        self.st()
        self.score = 0
        self.height = self.shapesize()[1]*10
        self.velocity = 50
        self.ondrag(self.drag)
        self.upkey = up
        self.downkey = down

    def drag(self,x,y):
        self.ondrag(None) # Disable event handler to avoid recursion
        if y >= (field.height/2-field.score_height) - self.height:
            y = (field.height/2-field.score_height) - self.height
        if y <= -field.height/2+self.height:
            y = -field.height/2+self.height
        self.goto(self.xcor(),y)
        self.ondrag(self.drag) # Reactivate event handler

    def up(self):
        #field.onkeypress(None, self.upkey)
        if (self.ycor()+self.height <=
            (field.height-field.score_height)/2+field.yzero):
            self.forward(self.velocity)
        #field.onkeypress(self.up, self.upkey)

    def down(self):
        #field.onkeypress(None, self.downkey)
        if self.ycor()-self.height >= -field.height/2:
            self.forward(-self.velocity)
        #field.onkeypress(self.down, self.downkey)

class Score(Turtle):
    def __init__(self):
        super().__init__(visible=False)
        self.speed(0)
        self.color("white")
        self.pensize(3)
        # Draw lower border
        self.penup()
        self.goto(-field.width,-field.height/2)
        self.pendown()
        self.goto(field.width,-field.height/2)
        # Draw upper border
        self.penup()
        self.goto(-field.width,field.height/2-field.score_height)
        self.pendown()
        self.goto(field.width,field.height/2-field.score_height)
        self.penup()
        # Draw score
        self.goto(-100,field.height/2-field.score_height)
        self.write(player2.score,font=("Monospace",50,"bold")) 
        self.goto(100,field.height/2-field.score_height)
        self.write(player1.score,font=("Monospace",50,"bold"))

    def update(self):
        # Clear the previous score
        for i in range(3):
            self.undo()
        # And write the new one
        self.write(player2.score,font=("Monospace",50,"bold")) 
        self.goto(100,field.height/2-field.score_height)
        self.write(player1.score,font=("Monospace",50,"bold"))

class Game:
    def __init__(self,court,difficulty=0):
        # Difficulty = increase in ball speed
        self.difficulty = difficulty
        # Setup event handlers
        court.onkeypress(self.qt, "Escape")
        court.onkeypress(player1.up, player1.upkey)
        court.onkeypress(player1.down, player1.downkey)
        court.onkeypress(player2.up, player2.upkey)
        court.onkeypress(player2.down, player2.downkey)
        court.onkey(self.pause, "p")
        court.listen()
        # Try to implement game pause. Not working, for the moment
        #self.pause = False
        #self.pause_count = 0

    def reset(self):
        ball.setposition(0,field.yzero)
        player1.setposition(player1.xcor(),field.yzero)
        player2.setposition(player2.xcor(),field.yzero)
        ball.dirrection = choice([0,180])   # Left or right
        ball.setheading(ball.dirrection+randrange(-80,80))

    def restart(self):
        self.reset()
        self.player1_score = 0
        self.player2_score = 0
        self.difficulty = 0

    def qt(self):
        prompt = Tk()
        prompt.eval('tk::PlaceWindow %s center' % prompt.winfo_toplevel())
        prompt.withdraw()
        answer = messagebox.askyesno("Quit", "Are you sure you want to quit?")
        if answer == True:
            field.bye()
        return

    # Not currently working
    def pause(self):
        if self.pause_count % 2 == 0:
            self.pause == True
        else:
            self.pause = False

class Play(Turtle):
    def __init__(self):
        super().__init__(visible=False)
        self.shape("square")
        self.color("white")
        self.speed(0)
        self.penup()
        self.shapesize(2,4)
        self.goto(-field.width/2,field.height/2-field.score_height/2)
        self.write("Play",font=("Monospace",20,"bold"))
        field.onscreenclick(self.click)

    def click(self,x,y):
        print(x,y)
        if (x <= -field.width/2+field.width/2/10 and
            x >= -field.width/2 and
            y >= field.height/2-field.score_height/2 and y <= field.height/2):
            self.color("green")
            self.clear()
            self.write("Play",font=("Monospace",20,"bold"))
            self.color("white")
            self.clear()
            self.write("Play",font=("Monospace",20,"bold"))
            game.reset()
            main()

def main():
    ball.forward(ball.velocity+game.difficulty)
    # Check for paddle collision
    if ball.player_collision(player1) or ball.player_collision(player2):
        ball.setheading(180 - ball.heading())
    # Bounce from upper or lower border
    if ball.court_collision(field):
        ball.setheading(-ball.heading())
    # Check for ball out of field and update player score
    elif ball.out_right(field):
        game.reset()
        player2.score += 1
        score.update()
        game.difficulty += 0.5
    elif ball.out_left(field):
        game.reset()
        player1.score += 1
        score.update()
        game.difficulty += 0.5
    field.ontimer(main)

if __name__ == "__main__":
    field = Field(1280,720)
    ball = Ball()
    player1 = Player(field.width/2,field.yzero,up = "Up", down = "Down")
    player2 = Player(-field.width/2,field.yzero, up = "w", down = "s")
    game = Game(field)
    score = Score()
    play_button = Play()
    #field.mainloop()

有点用,但是如果你用按键玩的话,最终还是会报错:

RecursionError:调用 a 时超出了最大递归深度 Python 对象

首先看起来问题出在main() 函数上,但实际问题出在按键的事件处理程序上。如果我只用鼠标玩,游戏不会报错,只是感觉生涩。

到目前为止,我已经阅读了以下主题:

maximum recursion depth exceeded

Avoid RecursionError in turtle paint code

Turtle.onkeypress not working (Python)

并尝试实施那里找到的解决方案。唯一对我有用的是禁用ondrag() 函数的事件处理程序。如果我尝试在播放器上使用相同的解决方案(取消注释Playerup()down() 方法中的行)它只会在main() 未运行时起作用。如果我启动 main() 函数,它将只运行一次并停用。

所以我需要帮助的是:

  1. 避免最大递归错误。 (仅当main() 处于活动状态时才会发生);
  2. 使ondrag 函数正常工作,而不会影响main() 函数;
  3. 如果 main() 正在运行,来自 game 类的 qt() 方法将无法正常工作。

那么你们认为我可以改进这些方面吗?

编辑:下面是完整的回溯

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 1702, in __call__
    return self.func(*args)
  File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 746, in callit
    func(*args)
  File "C:/Users/Bogey/Desktop/asd.py", line 209, in main
    ball.forward(ball.velocity+game.difficulty)
  File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\turtle.py", line 1637, in forward
    self._go(distance)
  File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\turtle.py", line 1604, in _go
    ende = self._position + self._orient * distance
RecursionError: maximum recursion depth exceeded

【问题讨论】:

  • 请显示实际的回溯。它会告诉我们递归调用了什么函数。
  • @Bryan Oakley 我已经编辑了我的原始帖子并添加了完整的回溯。但是,正如我在对 cdlane 的回答的评论中所写的那样,问题在于没有为 ontimer 方法提供时间参数。唯一未解决的问题是鼠标拖动功能,它会减慢球的速度。

标签: python object recursion event-handling turtle-graphics


【解决方案1】:

我看到的主要问题是你在玩游戏时做了太多不必要的计算。例如,考虑在每次球运动时调用的 court_collision() 方法:

def court_collision(self,court):
        if (ball.ycor() >= ((field.height/2)-
                            court.score_height-self.shapesize()[0]*10)
            or ball.ycor() <= -court.height/2+10): return True
        return False

在所有这些值中,只有 ball.ycor() 发生了变化,其余的应该在游戏开始之前计算并存储起来,以便该方法看起来更像:

def court_collision(self):
        return not self.wall_top_offset > ball.ycor() > self.wall_bottom_offset

player_collision()drag() 等也是如此。

main() 函数应该真的是Ballmove()方法

我还有其他 nits,但它们与游戏性能没有任何关系。

【讨论】:

  • 你有一些优点。事实上,我做了一些不必要的计算,并且 main 可以作为 Ball 方法实现。我考虑了您的意见并更改了我的代码,但发现我的问题中描述的问题实际上来自 ontimer() 方法,我在没有时间参数的情况下使用该方法。我想频率太高而无法处理,或者什么,我不知道。即使是 1ms 也足以避免崩溃。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-11
  • 1970-01-01
  • 2012-03-02
  • 2020-04-06
  • 1970-01-01
相关资源
最近更新 更多