【问题标题】:How to fix inconsistent frame rate (speed) in python turtle如何修复 python turtle 中不一致的帧速率(速度)
【发布时间】:2019-04-03 12:47:13
【问题描述】:

我按照本教程制作了一个乒乓球游戏https://youtu.be/C6jJg9Zan7w 我遇到的问题是球(乌龟物体)的速度在不同的计算机上是不一样的。例如,在辅导老师的电脑上,ball.dx ball.dy 的值为 2,球速正常,但在我的电脑上非常快,所以我不得不将其设置为 0.1。我认为这个问题是因为不同的计算机可以输出或多或少的帧。 我知道流行的游戏引擎(如统一虚幻引擎)中有一种方法使用时间而不是帧,因此变量在不同的计算机中是全局的。 我遇到的另一个问题是,当我移动球拍时,球的速度会略有变化。不知道是不是和上面一样的问题

import turtle
import winsound

wn = turtle.Screen()
wn.title('Pong')
wn.bgcolor('black')
wn.setup(width=800, height=600)
wn.tracer(0)

# Paddle A
paddle_a = turtle.Turtle()
paddle_a.speed(0)
paddle_a.shape('square')
paddle_a.color('white')
paddle_a.penup()
paddle_a.goto(-350, 0)
paddle_a.shapesize(5, 1)

# Paddle B
paddle_b = turtle.Turtle()
paddle_b.speed(0)
paddle_b.shape('square')
paddle_b.color('white')
paddle_b.penup()
paddle_b.goto(350, 0)
paddle_b.shapesize(5, 1)

# Ball
ball = turtle.Turtle()
ball.speed(0)
ball.shape('square')
ball.color('white')
ball.penup()
ball.dx = 0.15
ball.dy = 0.15

# Pen
pen = turtle.Turtle()
pen.speed(0)
pen.color('white')
pen.penup()
pen.goto(0, 260)
pen.write("Player A: 0  Player B: 0", align='center', font=('Courier', 24, 'bold'))
pen.hideturtle()

# Score
score_a = 0
score_b = 0

def paddle_a_up():
    y = paddle_a.ycor()
    y += 20
    paddle_a.sety(y)

def paddle_b_up():
    y = paddle_b.ycor()
    y += 20
    paddle_b.sety(y)

def paddle_a_down():
    y = paddle_a.ycor()
    y += -20
    paddle_a.sety(y)

def paddle_b_down():
    y = paddle_b.ycor()
    y += -20
    paddle_b.sety(y)

# Keyboard binding
wn.listen()
wn.onkeypress(paddle_a_up, 'w')
wn.onkeypress(paddle_a_down, 's')
wn.onkeypress(paddle_b_up, 'Up')
wn.onkeypress(paddle_b_down, 'Down')


# Main game loop
while True:
    wn.update()

    # Moving Ball
    ball.setx(ball.xcor() + ball.dx)
    ball.sety(ball.ycor() + ball.dy)

    # Border checking
    if ball.ycor() > 290 or ball.ycor() < -290:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.dy *= -1

    if ball.xcor() > 390:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.goto(0, 0)
        ball.dx *= -1
        score_a += 1
        pen.clear()
        pen.write("Player A: {}  Player B: {}".format(score_a, score_b), align='center', font=('Courier', 24, 'bold'))

    if ball.xcor() < -390:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.goto(0, 0)

        ball.dx *= -1
        score_b += 1
        pen.clear()
        pen.write("Player A: {}  Player B: {}".format(score_a, score_b), align='center', font=('Courier', 24, 'bold'))

    # Paddle and ball collisions
    if (ball.xcor() > 340 and ball.xcor() < 350) and (ball.ycor() < paddle_b.ycor() + 60 and ball.ycor() > paddle_b.ycor() -60):
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.setx(340)
        ball.dx *= -1

    if (ball.xcor() < -340 and ball.xcor() > -350) and (ball.ycor() < paddle_a.ycor() + 60 and ball.ycor() > paddle_a.ycor() -60):
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.setx(-340)
        ball.dx *= -1

【问题讨论】:

  • 我发布的代码会导入一些文件,因此可能对您不起作用。只需注释掉使用“winsound”的行
  • 嗨!你找到解决问题的方法了吗?我看了同样的教程,遇到了同样的问题。

标签: python turtle-graphics


【解决方案1】:

您的while True: 在像turtle 这样的事件驱动环境中没有位置。第一级替换是将循环体放入您使用ontimer() 调用的函数中。这将以固定的时间间隔调用您的例程(确保将ontimer() 调用作为最后一步,因为它是一次性的。)类似于:

def move_balls():
    global score_a, score_b

    wn.update()

    # Moving Ball
    ball.setx(ball.xcor() + ball.dx)
    ball.sety(ball.ycor() + ball.dy)

    # Border checking
    if ball.ycor() > 290 or ball.ycor() < -290:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.dy *= -1

    if ball.xcor() > 390:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.goto(0, 0)
        ball.dx *= -1
        score_a += 1
        pen.clear()
        pen.write("Player A: {}  Player B: {}".format(score_a, score_b), align='center', font=('Courier', 24, 'bold'))

    elif ball.xcor() < -390:
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.goto(0, 0)
        ball.dx *= -1
        score_b += 1
        pen.clear()
        pen.write("Player A: {}  Player B: {}".format(score_a, score_b), align='center', font=('Courier', 24, 'bold'))

    # Paddle and ball collisions
    if (340 < ball.xcor() < 350) and (paddle_b.ycor() - 60 < ball.ycor() < paddle_b.ycor() + 60):
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.setx(340)
        ball.dx *= -1

    elif (-350 < ball.xcor() < -340) and (paddle_a.ycor() - 60 < ball.ycor() < paddle_a.ycor() + 60):
        winsound.PlaySound('bounce.wav', winsound.SND_ASYNC)
        ball.setx(-340)
        ball.dx *= -1

    wn.ontimer(move_balls, 100)

move_balls()

但是,计时器仅控制对move_balls() 的一次调用结束与另一次调用开始之间的时间。它不考虑方法本身所花费的时间。如果您需要更精确的控制,那么high precision frames per second code 之类的内容可能就是您想要的。

【讨论】:

    【解决方案2】:

    在制作动画的时候,尤其是想要控制动画的速度,需要坚持模型-视图分离的原则。这意味着您有一部分程序控制对象的状态(模型),而另一部分则绘制对象(视图)。对象的状态可能包括位置、航向、颜色和其他属性。这部分改变对象状态的程序与对象的绘制无关。绘图是由程序的单独部分完成的,该部分检查对象的状态并将它们呈现在屏幕上。

    cdlane's answer混合模型和视图。在一个定时器函数中,对象的状态被改变并被渲染(这可能需要大量的 CPU 时间)。这将导致下一次调用此函数的时间延迟。

    我的解决方案是让move_balls() 函数仅更改球的位置(不绘制它),这将几乎没有延迟地返回,保持时间间隔一致。然后,您将用于绘制对象的代码以尽可能小的延迟放入计时器处理程序中。为避免不必要的绘图,请创建一个 should_draw 全局变量,并在其为 false 时立即返回,而不会浪费 CPU 时间。 move_balls() 函数执行后,对象将需要绘制。因此,在从move_balls() 返回之前,请确保将 should_draw 设置为 true。另外,绘制完成后,确保将should_draw 设置为False。 (此部分在与 cdlane 讨论后编辑)

    这应该允许您绘制许多具有流畅动画的对象。看看这个 Youtube 视频,它每(几乎精确地)第 5 秒就会有数百只萤火虫发光。视频说明中还有教程。

    https://www.youtube.com/watch?v=K9D-wO4w_k0

    这里是完整代码的教程: https://pythonturtle.academy/tutorial-fireflies/

    【讨论】:

    • 我不认为模型视图的分离是一种改进。但是,萤火虫示例存在缺陷,因为它使用了while True: 循环。通过不将控制权移交给 tkinter 事件循环(通过screen.mainloop() 或类似方法),事件不同步。当您关闭萤火虫窗口并显示 14 行错误跟踪时,您可以看到这一点。在正确设置的海龟事件环境中,这应该可以正常工作。
    • 当萤火虫在 model 位置计算中包含 view 帧率时,不要违反 model-view 分离:current_xpos[i] += SPEED/FPS*math.cos(angle_to_target) 或萤火虫知道它们何时被拍摄吗?
    • 这里有一个难题:如果从定时器事件中调用绘图函数,应该使用什么定时器值?由于绘图是 CPU 最密集的部分,而且不同系统的绘图速度不同,甚至同一系统上的绘图时间也可能不同,因此很难选择合适的值。我还注意到,在主线程中绘制比在事件处理程序中绘制更快(我没有花在验证这一点上)。我不太担心程序的优雅终止,只要它在执行期间运行良好。
    • 由于在这种情况下不需要比平滑动画的频率更频繁的模型更新,我们可以使用 1/FPS(30 帧/秒所以 1/30 秒)的数字作为计时器值。由于 1/FPS 的时间已经过去,您可以使用此数字来更新模型的位置。
    • 程序的优雅终止只是当您使用while True: 循环阻止事件时发生的情况的一个示例。 OP 的代码有按键事件来移动拨片,如果这些不能工作,你也不会太担心吗? 是否知道哪些活动有效,哪些无效?
    猜你喜欢
    • 2015-12-25
    • 1970-01-01
    • 2015-01-04
    • 2023-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多