5

I a made pong game by following this tutorial https://youtu.be/C6jJg9Zan7w The problem I'm having is that speed of the ball (turtle object) is not the same on different computers. For example, on the tutorial instructor's computer the value of ball.dx ball.dy is 2 and the ball speed is normal but on my computer is really fast so I had to set it to 0.1. I think this problem is because different computers can put out more or less frame. I know there is a method in popular game engines (like unity-unreal) that use time instead of frames so variables are global in different computers. Another problem I have is that when I move the paddles speed of the ball slightly changes. I don't know if this is the same issue as above

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
ggorlen
  • 44,755
  • 7
  • 76
  • 106
Saadat
  • 163
  • 4
  • 15
  • The code I posted imports some files so may not work for you. just comment out the lines that use "winsound" – Saadat Apr 03 '19 at 12:49

2 Answers2

1

Your while True: has no place in an event-driven environment like turtle. The first level replacement would be to put the body of your loop into a function that you invoke with ontimer(). This will call your routine at fixed intervals (make sure to include the ontimer() call as the last step as it's a one-shot.) Something like:

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()

However, the timer only controls the time between the end of one call to move_balls() and the start of another. It doesn't account for the time spent in the method itself. If you need more precise control, then something like this high precision frames per second code might be what you're looking for.

cdlane
  • 40,441
  • 5
  • 32
  • 81
1

When animating, especially if you want control the speed of animation, you need stick to the principle of model-view separation. This means you have one part of program that controls the state of objects (model), and the other part that draws the objects (view). State of objects may include position, heading, color, and other properties. This part of program that changes objects’ states is not concerned with drawing of the objects. The drawing is done by separate part of program that checks the states of the objects and render them on the screen.

cdlane's answer mixed model and view. Within one single timer function, objects' states are changed and also rendered (which could take lots of CPU time). This will cause delay to the next time this function will be called.

My solution is to make move_balls() functions change the positions of the balls only (without drawing it), which will return with virtually no delay keeping the time interval consistent. Then you put code for drawing the objects into a timer handler with minimal possible delay. To avoid unnecessary drawing make a should_draw global variable and returns immediately when it is false, without wasting CPU time. After move_balls() function executes the objects will need drawing. So make sure set should_draw to true before returning from move_balls(). Also, after finished drawing is done, make sure set should_draw to False. (this part edited after discussion with cdlane)

This should allow you draw a lot of objects with smooth animation. Check out this Youtube video that draws hundreds of fireflies lighting up every (almost precisely) 5'th second. There is also a tutorial in the video description.

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

Here is tutorial with complete code: https://pythonturtle.academy/tutorial-fireflies/

Jinsheng
  • 21
  • 3
  • 2
    I've no argument with the separation of model-view being an improvement. However, the fireflies example is flawed due to it's use of a`while True:` loop. By not turning over control to the tkinter event loop (via `screen.mainloop()` or similar method) events are out of synch. You can see this when you close the fireflies window and are presented with 14 lines of error trace. In a properly set up turtle event environment, this should work cleanly. – cdlane Apr 04 '19 at 02:01
  • Don't the fireflies violate separation of model-view when they include the *view* frame rate in the *model* position calculation: `current_xpos[i] += SPEED/FPS*math.cos(angle_to_target)` Or do fireflies know when they're being filmed? – cdlane Apr 04 '19 at 02:15
  • There is a dilemma here: If you invoke the drawing function from the timer event, what timer value should you use? Since drawing is the most CPU intensive part and different system have different drawing speed, and drawing time may even vary on the same system, it is hard to choose the right value. I also notice that drawing in the main thread finishes faster than drawing in the event handler (I didn't spend on verifying this). I don't worry much about the graceful termination of the program as long as it works well during execution. – Jinsheng Apr 04 '19 at 13:20
  • Since there is no need to do more frequent model update than the frequency of smooth animation in this case, we can use 1/FPS (30 frames/sec so 1/30 of a second) number as the timer value. Since 1/FPS amount of time has passed, you can use this number to update the model's position. – Jinsheng Apr 04 '19 at 13:25
  • 1
    Graceful termination of the program was merely an example of what happens when you block events with a `while True:` loop. The OP's code has key press events to move the paddles, if those fail to work also will you not worry much about that? Do *you* know which events will work and which won't? – cdlane Apr 04 '19 at 16:03
  • You are right. When having keyboard and mouse events, while True will not work. So, the solution is to move drawing code to a timer handler with very short delay (1 millisecond eg.). Still use should_draw boolean variable to return immediately when no drawing is needed. – Jinsheng Apr 05 '19 at 01:08