Search code examples
pythonkivy

Kivy PongApp ball goes through paddle at high velocity


I am new to python and kivy, and was following the Kivy tutorial on creating the PongApp line by line when I noticed that after the ball collides with a paddle 25 times, it will not register the 26th collision and thus the player's score will increase.

I assume this issue is related to the velocity moving past a certain speed along the x axis where the ball's position will never interact with the paddle's x position. However what's confusing me is when changing the speed increase per collision from 1.1 to 1.2, the maximum number of collisions before score increases varies from 11 to 18 times.

My question is, what is actually the root cause of this issue, and how can I go about ensuring the ball will always collide with the paddle regardless of the velocity?

main.py:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import (NumericProperty, ReferenceListProperty, ObjectProperty)
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint

class PongPaddle(Widget):
    score = NumericProperty(0)

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            offset = (ball.center_y - self.center_y) / (self.height / 2)
            bounced = Vector(-1 * vx, vy)
            vel = bounced * 3
            ball.velocity = vel.x, vel.y + offset


class PongBall(Widget):

    # velocity of the ball on x and y axis
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)

    # referencelist property so we can use ball.velocity as
    # a shorthand, just like e.g. w.pos for w.x and w.y
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    # ``move`` function will move the ball one step. This
    #  will be called in equal intervals to animate the ball
    def move(self):
        self.pos = Vector(*self.velocity) + self.pos

class PongGame(Widget):
    ball = ObjectProperty(None)
    player1 = ObjectProperty(None)
    player2 = ObjectProperty(None)

    def serve_ball(self, vel=(4, 0)):
        self.ball.center = self.center
        self.ball.velocity = vel

    def update(self, dt):
        self.ball.move()

        #bounce off paddles
        self.player1.bounce_ball(self.ball)
        self.player2.bounce_ball(self.ball)

        # bounce off top and bottom
        if (self.ball.y < 0) or (self.ball.top > self.height):
            self.ball.velocity_y *= -1

        # bounce off left and right to score point
        if self.ball.x < self.x:
            self.player2.score += 1
            self.serve_ball(vel=(4, 0))
        if self.ball.x > self.width:
            self.player1.score += 1
            self.serve_ball(vel=(-4, 0))

    def on_touch_move(self, touch):
        if touch.x < self.width / 3:
            self.player1.center_y = touch.y
        if touch.x > self.width - self.width / 3:
            self.player2.center_y = touch.y


class PongApp(App):
    def build(self):
        game = PongGame()
        game.serve_ball()
        Clock.schedule_interval(game.update, 1.0/60.0)
        return game

if __name__ == '__main__':
    PongApp().run()

pong.kv:

#:kivy 2.0.0
<PongBall>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size

<PongPaddle>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right

    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height

    Label:
        font_size: 70
        center_x: root.width / 4
        top: root.top - 50
        text: str(root.player1.score)

    Label:
        font_size: 70
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: str(root.player2.score)

    PongBall:
        id: pong_ball
        center: self.parent.center

    PongPaddle:
        id: player_left
        x: root.x
        center_y: root.center_y

    PongPaddle:
        id: player_right
        x: root.width - self.width
        center_y: root.center_y

Solution

  • This is a fundamental problem with simple collision algorithms: collision is only checked once each frame after moving the objects, and if they move through each other during that frame the algorithm has no way to notice. The issue is well known, for instance versions of this problem are one reason it's often possible to contrive to clip through walls in computer games (although in general games nowadays do use more complex algorithms, and are tricked in more complex ways).

    In a general sense the way to solve this is to use a better collision detection algorithm. For instance, for two spheres you can work out if they ever will have intersected during that frame by working out the point of closest approach of their centre points during the linear movement step of the frame, and comparing that distance to their radii to see if they would have collided.

    Doing generic collisions efficiently is in general a hard problem, with various trade-offs you can make depending on what you want to optimise for (e.g. if you want to handle approximate collisions of many objects efficiently you have different problems to if you want to handle precisely a small number of objects with complex shapes). Kivy's example uses a simple algorithm to avoid making the example complex, not because it's a good solution. If you really care about doing collisions efficiently I'd recommend using a physics library to handle it, there are various libraries with python interfaces.