8
\$\begingroup\$

I have made a clone of the mobile game Ballz, and it works quite well. However, there are a few problems:

  • Performance. When I have more than 50 balls in the game, the FPS dips from 60. I am unsure how to (A) improve the performance of the game and (B) adapt to a low frame rate such that the game physics continues to work properly and the user sees the objects moving at the same speed.
  • Collisions: Most of the time, collisions work fine. However, when I up the speed of the balls they can sometimes go through boxes. This is not the desired result, but I am again unsure of how to fix it.
  • I am new to programming with Python, though I am fairly experienced in other languages. I would like to know if there are any small things I can improve with my style. Specifically, to create the singleton class board I have simply created it as a class with only static methods and variables. Is there a better way to define a singleton?
  • I would like to know how I have done with my OOP, i.e. do any objects "know" about each other that shouldn't?
  • Lastly, if there are any glitches or obvious errors I've missed please point them out!

My code is divided into several different class. Here is an image showing the flow of my program:

Pickle rick Here is a brief summary of each class (Note that I use import pygame as pyg):

  • the measures module contains variables that I want to have standardized throughout the code. This includes things like the width and height of the window and borders.
  • The ball class is derived from pyg.sprite.Sprite. It is first launch()ed when the user hits the mouse button, and, if it falls to the ground first, sets a new "center" from which the balls are launched in the next round (ball.terminus.x)
  • The item class, derived from pyg.sprite.Sprite contains all of the functions used by both ball_adder and box, such as moving according to the step given, and has overridable functions draw() and handleCollision()
  • The ball_adder class, derived from item, disappears when hit by a ball and drops a new ball at the base (ball addition is handled by board)
  • The box class, derived from item, has a number representing its "strength". Every time it is hit by a ball, number is decreased. After hitting 0, the box instance disappears.
  • The board class contains a pyg.sprite.Group of balls(board.balls) and has a list of groups for each row of the board (board_row) which contains every box and ball_adder, and each row of balls (ball_row, used to optimize collisions)

Here is my code (I apologize if it takes a long time to read through it all):
measures.py

window = (375, 585)
radius = 7
dimension = 45
step = 5 #space between each item in the grid

#border width (for the left and side borders) and height (for the top and bottom borders)
side_width = 10
top_height = 65

#grid positions for the boxes and ball_adders 
xs = [side_width + step + (dimension + step)*x for x in range(0,7)]
ys = [top_height + dimension + 2*step + (dimension + step) * x for x in range(0,8)]

item.py

import pygame as pyg
import measures

class item(pyg.sprite.Sprite):
    """Base class for the members of board_row"""
    def __init__(self, x_pos, y_pos = -1):
        pyg.sprite.Sprite.__init__(self)

        #Check if the item is a new instance or loaded from the json file. If new, place it at the initial y value.
        y = 0
        if y_pos == -1:
            y = measures.top_height + measures.step
        else:
            y = measures.ys[y_pos]

        self.rect = pyg.Rect(measures.xs[x_pos], y, measures.dimension, measures.dimension)
        self.stepper = 0.00

        self.moving = True #always move the item when it is created

    def draw(self):
        """Handled by the child classes"""
        pass

    def initiateMove(self):
        """Start moving the item"""
        self.moving = True
        self.stepper = 0

    def updatePosition(self, step):
        if self.moving:
            #Move object
            self.stepper += step
            if self.stepper >= step: #Once a full step has ocurred, move the object internally. This ugly workaround is necessary because pyg.Rect.top is an int
               self.stepper -= 1
               self.rect.top += 1

            #Check if the item has hit the next y_pos
            try:
                measures.ys.index(self.rect.top)
                self.moving = False
            except:
                pass


    def update(self, step):
        """Updates the position and draws the item"""
        self.updatePosition(step)
        self.draw()

    def handle_collision():
        """Handled by the child classes"""
        pass

Box.py

import pygame as pyg
import random
from item import item
import measures
pyg.font.init() #Throws an error when declaring box.box_font if it is not called

class box(item):
    box_font = pyg.font.Font("Fonts/Roboto-Light.ttf", 20)

    def __init__(self, game_level, x_pos, y_pos=-1, number = 0):
        item.__init__(self, x_pos, y_pos)

        #Check if this is a new box instance or loaded from a json file. If new, assign it with a 3/4 of being game_level, 
        #and a 1/4 chance of being 2*game_level 
        if number == 0:
            self.number = random.randint(1, 4)
            if self.number == 1:
                self.number = 2*game_level 
            else:
                self.number = game_level
        else:
            self.number = number

        self.number_text = box.box_font.render(str(self.number), True, (0,0,0))

        #Pick the color
        self.color = pyg.Color(0,0,0)
        if self.number <= 5:
            self.color = pyg.Color(245, 181, 46)
        elif 5 < self.number <= 12:
            self.color = pyg.Color(129, 197, 64)
        elif 12 < self.number <= 31:
            self.color = pyg.Color(234, 34, 94)
        elif 31 < self.number < 50:
            self.color = pyg.Color(196, 34, 132)
        elif 50 <= self.number:
            self.color = pyg.Color(22, 116, 188)

    def handle_collision(self):
        """decrements the number and updates the text to match it. If number <= 0, kill()"""
        self.number -= 1
        self.number_text = box.box_font.render(str(self.number), True, (0,0,0))
        self.color.r = min(self.color.r + 10, 255)

        if self.number <= 0:
            self.kill()

    def draw(self):
        """Draws the rect and the text centered on the rect"""
        number_text_rect = self.number_text.get_rect()
        number_text_rect.center = self.rect.center

        display_surface = pyg.display.get_surface()

        pyg.draw.rect(display_surface, self.color, self.rect)
        display_surface.blit(self.number_text, number_text_rect)

Ball_Adder.py

import pygame as pyg
from pygame import gfxdraw
from item import item
import measures

class ball_adder(item):
    outer_width = 2
    outer_radius = measures.radius + 8

    def __init__(self, x_pos, y_pos = -1):
        item.__init__(self, x_pos, y_pos)

    def draw(self):
        """Draws the inner circle and the outer ring"""
        display_surface = pyg.display.get_surface()
        rect_center = self.rect.center

        gfxdraw.aacircle(display_surface, rect_center[0], rect_center[1], ball_adder.outer_radius, (255,255,255))

        #create the outline width since pygame doesn't implement it that.
        for i in range(1, ball_adder.outer_width):
           gfxdraw.circle(display_surface, rect_center[0], rect_center[1], ball_adder.outer_radius - i, (255,255,255))

        gfxdraw.aacircle(display_surface, rect_center[0], rect_center[1], ball_adder.outer_radius - ball_adder.outer_width, (255,255,255))

        gfxdraw.aacircle(display_surface, rect_center[0], rect_center[1], measures.radius, (255,255,255))
        gfxdraw.filled_circle(display_surface, rect_center[0], rect_center[1], measures.radius, (255, 255, 255))

    def handle_collision(self):
        """If there is a collision, disappear"""
        self.kill()

ball.py

import pygame as pyg
from pygame import gfxdraw
import math
import measures

class ball(pyg.sprite.Sprite):
    radius = 7
    terminus = pyg.math.Vector2(measures.window[0] // 2, measures.window[1] - measures.top_height - measures.radius - 1) #launching point
    new_terminus_x = 0
    first = False #Flag that determines whether or not to update new_terminus_x upon hitting the ground
    speed = 10 

    def __init__(self, x=0):
        pyg.sprite.Sprite.__init__(self)

        self.vector = pyg.math.Vector2(0,0)
        self.moving = False 
        self.launching = False #Flag that says whether or not the ball is launching (updated in board and the launch thread)

        #Set new_terminus_x when starting up the class (avoids the ball flying off the screen when no board.json exists)
        if ball.new_terminus_x == 0:
            ball.new_terminus_x = ball.terminus.x

        #If it is 0, it is "dropped" from a ball_adder onto the correct x value, instead of at terminus.x
        if x == 0:
            x = ball.terminus.x

        self.center = pyg.math.Vector2(x, ball.terminus.y)

    def draw(self):
        """Draw"""
        display_surface = pyg.display.get_surface()

        gfxdraw.aacircle(display_surface, int(self.center.x), int(self.center.y), measures.radius, (255,255,255))
        gfxdraw.filled_circle(display_surface, int(self.center.x), int(self.center.y), measures.radius, (255,255,255))


    def launch(self):
        self.moving = True
        self.launching = False #Not launching if you have already launched

    def update(self, step):
        from Board import board

        self.draw()
        if self.moving:
            #Move along the vector
            self.center -= step*ball.speed*self.vector

            #Detects collision with the border
            #right side
            if self.center.x + 7 >= board.borders[3].left:
                self.center.x = board.borders[3].left - measures.radius
                self.vector.x *= -1
            #left side
            elif self.center.x - 7 <= board.borders[2].left:
                self.center.x = board.borders[2].left + board.borders[2].width + measures.radius
                self.vector.x *= -1
            #top
            elif self.center.y - 7 <= board.borders[0].top + board.borders[0].height:
                self.center.y = board.borders[0].top + board.borders[0].height + measures.radius
                self.vector.y *= -1
            #bottom
            elif self.center.y + 7 >= board.borders[1].top:
                self.moving = False
                #If the first variable is false, then that means no other ball has set it to be true. Set it true and update new_terminus_x
                if not ball.first:
                    ball.first = True
                    ball.new_terminus_x = self.center.x
        elif not self.launching:
            #Once the ball hits the ground, slide it over to new_terminus_x
            if abs(self.center.x - ball.new_terminus_x) < ball.speed/2:
                self.center = pyg.math.Vector2(ball.new_terminus_x, ball.terminus.y)
            if self.center.x < ball.new_terminus_x:
                self.center = pyg.math.Vector2(self.center.x + ball.speed, ball.terminus.y)
            elif self.center.x > ball.new_terminus_x:
                self.center = pyg.math.Vector2(self.center.x - ball.speed, ball.terminus.y) 

            #update the direction vector according to the angle between mouse and terminus
            angle = 0
            mouse_pos = pyg.mouse.get_pos()
            if mouse_pos[1] < self.center.y - measures.radius: 
                try:
                    angle = math.atan((mouse_pos[1] - self.center.y)/(mouse_pos[0] - self.center.x))          
                except:
                    angle = math.pi /2
            else:
                angle = 3*math.pi / 180
                if pyg.mouse.get_pos()[0] > ball.terminus.x:
                    angle *= -1

            if pyg.mouse.get_pos()[0] > ball.terminus.x:
               angle += math.pi


            self.change_vector(angle)

    def change_vector(self, angle):
        """Takes input angle and updates the direction vector"""
        self.vector = pyg.math.Vector2(math.cos(angle), math.sin(angle))

    def prepare_launch():
        """Resets the speed and first value (used as a flag to determine whether to change new_terminus_x)"""
        ball.first = False
        ball.terminus.x = ball.new_terminus_x
        ball.speed = 10

Board.py

import pygame as pyg
from pygame import gfxdraw
import random
import Box
import Ball_Adder
import ball
import button
import math
import os
import json
import measures
pyg.font.init()

def pointOfIntersect(r_center, r_size, c_center):
        """Determines the closest point of a rectangle to a circle"""
        v2_c_center = pyg.math.Vector2(c_center)
        v2_r_center = pyg.math.Vector2(r_center)

        offset = v2_c_center - v2_r_center
        if offset.x == 0 and offset.y == 0:
            return [v2_c_center, v2_r_center]

        if offset.x == 0:   
            ratio = r_size[1] / abs(offset.y)
        elif offset.y == 0: 
            ratio = r_size[0] / abs(offset.x)
        else:
            ratio  = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
        ratio *= 0.5

        return v2_r_center + (offset * ratio)

class board:
    balls = pyg.sprite.Group() 
    ball_row  = [pyg.sprite.Group() for x in range(0, 7)]
    board_row = [pyg.sprite.Group() for x in range(0, 8)] #The last row is for when they hit they bottom and the entire board is cleared.

    array_moving = False #Flag that is used to make sure that the balls aren't launched while the board is moving

    #game_level and its associated text
    game_level = 1
    game_level_font = pyg.font.Font("Fonts/Roboto-Regular.ttf", 30)
    game_level_rect = pyg.Rect(0, 0, 0, 0)
    game_level_text = None

    #Not the actual ball_count, just what is displayed before and while you are launching the balls (goes down each time one is launched)
    ball_count = 1
    gbc_font = pyg.font.Font("Fonts/Roboto-Regular.ttf", 15)
    gbc_rect = pyg.Rect(0,0,0,0)
    gbc_text = None

    #angle between mouse and ball.terminus and the associated text (uses game_level_font)
    mouse_angle = 0
    ma_rect = pyg.Rect(0,0,0,0)
    ma_text = None

    #button that, when pressed, speeds up the balls
    speed_button = button.button_image(0, 0, "Images/Lightning.png", "Images/LightningPressed.png", pyg.Color(255,255,255))

    #window borders and color
    border_color = (56, 56, 56)
    #order: top, bottom, left, right
    borders  = []

    def init():
        #set up the borders
        board.borders = [ pyg.Rect(0, 0, measures.window[0], measures.top_height),
    pyg.Rect(0, measures.window[1] - measures.top_height, measures.window[0], measures.top_height),
    pyg.Rect(0, 0, measures.side_width, measures.window[1]),
    pyg.Rect(measures.window[0] - measures.side_width, 0, measures.side_width, measures.window[1]) ]

        #Reads from the file. If it doesn't find a board.json, it simply adds a new row.
        board.read_from_file()
        #Add the necessary number of balls
        for i in range(0, max(board.ball_count, 1)):
                    board.balls.add(ball.ball())
        board.update_text(True) #updates the text otherwise the ball_count text is wonky

        #Set the position of speed_button
        board.speed_button.left = measures.window[0] - board.speed_button.regular.get_rect().width - 5
        board.speed_button.top  = (board.borders[0].height - board.speed_button.regular.get_rect().height) / 2

    def add_row():
        row = []

        #3/4 chance of being a box, 1/4 chance of being 0, 0 chance of being cool
        for i in range(0, 7):
            if random.randint(1, 4) == 1:
                row.append(0)
            else:
                row.append(Box.box(board.game_level, i))

        #add the ball adder to the list at a random spot
        ball_adder_pos = random.randint(0, 6)
        del row[ball_adder_pos] 
        row.insert(ball_adder_pos, Ball_Adder.ball_adder(ball_adder_pos))

        #initiate move animation for existing rows
        game_end = False
        for i in range(len(board.board_row) - 2, -1, -1):
            for sprite in board.board_row[i].sprites():
                sprite.initiateMove()
                if i == 6: #Checks if there is a sprite in the last row 
                    game_end = True
                    break
                #Move the sprite to the next board_row
                sprite.add(board.board_row[i+1])
                sprite.remove(board.board_row[i])
            if game_end:
                break

        board.array_moving = True #Set the flag to be true as the items are moving down

        #Add the new row to the first row
        for member in row:
            if member != 0:
                board.board_row[0].add(member) 

        #check if the game has ended
        if game_end:
            for row in board.board_row:
                row.empty()
            board.game_level = 1
            board.add_row()
            for ball_ in board.balls:
                ball_.kill()
            board.ball_count = 1
            board.update_text(False)
            board.balls.add(ball.ball())

    def loop(step):
        #Set it to be false
        board.array_moving = False

        sorted_balls = sorted(board.balls.sprites(), key = lambda x: x.center[1]) 
        #sort to avoid branch prediction failure (I'm not sure this has a huge impact on performance one way or another)

        group_collision = board.collision
        for i in range(0, len(board.board_row)):
            board.board_row[i].update(step) #update each box and ball adder

            if board.board_row[i] and board.board_row[i].sprites()[0].moving: #Check if the row has any items and if the first sprite is moving
                board.array_moving = True

            if i != 7: #If i == 7 then there are boxes in the last row, at which point the game is already over.
                pyg.sprite.groupcollide(board.ball_row[i], board.board_row[i], False, False, group_collision)

        balls_grounded = True #Flag that tells whether or not the balls are on the ground

        #reassign the ball to a new row 
        for ball_ in sorted_balls:
            ball_.update(step)
            if ball_.moving == True:
                balls_grounded = False
            for i in range(0, len(board.ball_row)):
                lower = measures.ys[i]   - ball.ball.speed - measures.radius - measures.step
                upper = measures.ys[i+1] + ball.ball.speed + measures.radius + measures.step
                if lower <= ball_.center.y <= upper: #adds range to make sure that all collisions can be caught in time
                    board.ball_row[i].add(ball_)
                else:
                    board.ball_row[i].remove(ball_)

        display_surface = pyg.display.get_surface()

        #Displays the path of the ball before the user launches them
        if balls_grounded:
            board.display_ball_path();


        #If they are on the ground and the first flag has been set, then all balls have hit the ground just after being launched
        if  balls_grounded and ball.ball.first:
            board.speed_button.clear_state()
            ball.ball.prepare_launch() #resets first
            board.game_level += 1 
            board.ball_count = len(board.balls) #reset ball_count
            board.add_row() #add another row

        #if thread is true, the function returns true and the game loop starts a new thread using initiaite_launch which launches 
        #the balls at a set interval
        thread = False
        if pyg.mouse.get_pressed()[0] and not board.array_moving and balls_grounded:
                ball.ball.prepare_launch()
                for ball_ in board.balls.sprites():
                    ball_.launching = True
                thread = True

        #display the borders
        for border in board.borders:
            pyg.draw.rect(display_surface, board.border_color, border)

        #update each piece of text
        board.update_text(balls_grounded)

        #check if speed_button has been pressed
        if not balls_grounded and board.speed_button.update():
            ball.ball.speed = 20

        if thread:
            return True
        else:
            return False

    def collision(ball_, item):
        """Just handles the collisions"""
        if type(item) == Box.box:
            #convenience
            left = item.rect.left
            right = left + item.rect.width
            top = item.rect.top
            bottom = top + item.rect.height
            center = ball_.center

            #move foward 1 iteration (done so that the collisions are better)
            center -= ball.ball.speed*ball_.vector

            #rule out impossible collisions
            if center.x + measures.radius <= left or center.x - measures.radius >= right or center.y + measures.radius <= top or center.y - measures.radius >= bottom:
                center += ball.ball.speed*ball_.vector #move back one iteration
                return None # exit the function

            #find the closest point
            closest = pointOfIntersect(item.rect.center, (item.rect.width, item.rect.height), center)
            difference = center - closest

            #move back 1 iteration
            center += ball.ball.speed*ball_.vector

            #handle the collsion
            if difference.x**2 + difference.y**2 <= measures.radius**2:
                item.handle_collision()
                #find the closest point again because otherwise weird stuff happe
                closest = pointOfIntersect(item.rect.center, (item.rect.width, item.rect.height), center)

                #top/bottom
                if top - 1 <= closest.y <= top + 1 or bottom - 1 <= closest.y <= bottom + 1:
                    ball_.vector.y *= -1
                #left/right
                elif left - 1 <= closest.x <= left + 1 or right - 1 <= closest.x <= right + 1:
                    ball_.vector.x *= -1
        elif type(item) == Ball_Adder.ball_adder:
            if math.hypot(ball_.center.x - item.rect.center[0], ball_.center.y - item.rect.center[1]) < measures.radius + Ball_Adder.ball_adder.outer_radius:
                board.balls.add(ball.ball(item.rect.center[0]))
                item.handle_collision()

    def update_text(balls_grounded):
        display_surface = pyg.display.get_surface()
        #game level
        board.game_level_text = board.game_level_font.render(str(board.game_level), True, (255, 255, 255))

        board.game_level_rect = board.game_level_text.get_rect()
        board.game_level_rect.center = board.borders[0].center 
        display_surface.blit(board.game_level_text, board.game_level_rect)

        #ball count
        if board.ball_count != 0:
            board.gbc_text = board.gbc_font.render("x{}".format(str(board.ball_count)), True, (255,255,255))
            board.gbc_rect = board.gbc_text.get_rect()
            board.gbc_rect.center = (ball.ball.terminus.x, ball.ball.terminus.y - 3*measures.radius)
            display_surface.blit(board.gbc_text, board.gbc_rect)

        #mouse angle
        if balls_grounded:
            board.mouse_angle = math.acos(board.balls.sprites()[0].vector.x)
            board.ma_text = board.game_level_font.render(str(round(180 - (board.mouse_angle * 180)/math.pi, 1)) + "°", True, (255, 255, 255))

            board.ma_rect = board.ma_text.get_rect()
            board.ma_rect.center = board.borders[1].center
            display_surface.blit(board.ma_text, board.ma_rect)

    def initiate_launch(event):
        """Launches balls at timed intervals"""
        vector = tuple(board.balls.sprites()[0].vector) #save the vector so that the user can't change the trajectory mid-launch
        for ball_ in board.balls.sprites():
            event.wait(timeout=0.08)
            ball_.center = pyg.math.Vector2(ball.ball.terminus.x, ball.ball.terminus.y)
            ball_.launch()
            ball_.vector = pyg.math.Vector2(vector)
            board.ball_count -= 1
            event.clear()

    def display_ball_path():
        """Displays the path of the balls"""
        display_surface = pyg.display.get_surface()

        spacing = 40
        start = pyg.math.Vector2(ball.ball.terminus.x, ball.ball.terminus.y)
        end = pyg.math.Vector2(pyg.mouse.get_pos())
        length = (start - end).length()
        unit_vector = pyg.math.Vector2(start - end)
        unit_vector.normalize_ip()
        initial_ball = start - spacing*unit_vector

        while (initial_ball - start).length() <= length:
            gfxdraw.aacircle(display_surface, int(initial_ball.x), int(initial_ball.y), measures.radius, (255,255,255))
            gfxdraw.filled_circle(display_surface, int(initial_ball.x), int(initial_ball.y), measures.radius, (255,255,255))
            initial_ball -= spacing*unit_vector

    def read_from_file():
        """Read from the saved json file or, if it does not exist, then just add a row"""
        if os.path.isfile("Saves/board.json") and os.path.getsize("Saves/board.json") != 0:
            with open("Saves/board.json", 'r') as f:
                data = json.load(f)
                board.game_level = data["game level"]
                board.ball_count = data["ball count"]
                ball.ball.terminus.x   = data  ["terminus"]
                ball.ball.new_terminus_x = ball.ball.terminus.x
                board.balls.empty()
                for i in range(0, len(board.board_row)):
                    for member in data[str(i)]:
                        x = measures.xs.index(member[0])
                        if member[1] != 0:
                            board.board_row[i].add(Box.box(board.game_level, x, i-1, member[1]))
                        else:
                            board.board_row[i].add(Ball_Adder.ball_adder(x, i-1))
                f.close()
        else:
            board.add_row()


    def write_to_file():
        """Write to the json file"""
        with open("Saves/board.json", 'w') as f:
            data = {"game level": board.game_level, "ball count" : len(board.balls), "terminus": ball.ball.terminus.x}

            balls_grounded = False
            while board.array_moving or not balls_grounded:
                board.loop(0.5)
                balls_grounded = True
                for ball_ in board.balls.sprites():
                    if ball_.moving == True:
                        balls_grounded = False

                pyg.display.flip()

            for i in range(0, len(board.board_row)):
                subdata = []
                for sprite in board.board_row[i]:
                    if type(sprite) == Box.box:
                        subdata.append((sprite.rect.left, sprite.number))
                    else:
                        subdata.append((sprite.rect.left, 0))
                data[i] = subdata

            json.dump(data, f, indent = 4)
            f.close()

button.py

import pygame as pyg

class button_image:
    def __init__(self, left, top, image_path, pressed_image_path, color_key):
        self.regular = pyg.image.load(image_path)
        self.pressed = pyg.image.load(pressed_image_path)
        #self.regular.convert_alpha()
        #self.presssed.convert_alpha()
        self.regular.set_colorkey(color_key)
        self.pressed.set_colorkey(color_key)

        self.left = left
        self.top = left
        self.state = False

    def check_hover(self):
        rectangle = self.regular.get_rect()
        rectangle.left = self.left
        rectangle.top = self.top

        if rectangle.collidepoint(pyg.mouse.get_pos()):
            return True
        else:
            return False
    def check_click(self):
        if self.check_hover() and pyg.mouse.get_pressed()[0]:
            self.state = True
            return True
        else:
            return False

    def draw(self):
        display_surface = pyg.display.get_surface()
        if self.state:
            display_surface.blit(self.pressed, (self.left, self.top))  
        else:
            display_surface.blit(self.regular, (self.left, self.top))

    def update(self):
        self.draw()
        return self.check_click()

    def clear_state(self):
        self.state = False

And, finally, the main file: Ballz.py

import pygame as pyg
import Board
import threading
import measures

#main function
def main():
    pyg.init()

    #initialize and set the icon
    pyg.display.set_mode(measures.window)
    pyg.display.set_icon(pyg.image.load("Images/Icon.png"))
    pyg.display.set_caption("Ballz")

    display_surface = pyg.display.get_surface()

    #create game clock for keeping the framerate constant
    game_clock = pyg.time.Clock()

    Board.board.init()

    running = True
    while running:
        game_clock.tick(60) #keeps framerate at a maximum of 60

        #Handle events
        for event in pyg.event.get():
            if event.type == pyg.QUIT:
                running = False
                break

        #Draw
        try: #In place because sometimes, when I exit the program during debug, this particular section triggers an error.
            display_surface.fill((33, 33, 33))
        except:
            break

        #print("FPS: ",  int(game_clock.get_fps()))

        thread = False
        event = threading.Event()
        for i in range(0,2): #Call it twice so collisions have a finer granularity
            if Board.board.loop(0.5):
                thread = True #makes sure that it is not called twice
        if thread:    
            ball_launch = threading.Thread(target=Board.board.initiate_launch, args=(event,))
            ball_launch.start()

        event.set()

        pyg.display.flip()

    Board.board.write_to_file() #Write to the file once the game is over 

    pyg.quit()

if __name__ == "__main__":
    main()

While it is not necessary to have the exact same font and images, here are the ones I used

  • Roboto and Roboto light can be found here, and must be saved in the Fonts/ directory of the project file
  • The lightning images are right here: enter image description here enter image description here (These are to be saved in Images/)
  • Finally, the icon image: enter image description here

Also, here is a GIF of what the program looks like. As you can see, the game is somewhat laggy starting around game level 50:
enter image description here

\$\endgroup\$
1
  • 4
    \$\begingroup\$ This is nothing like Ballz, where's the bajillion of adverts?! In all seriousness, this is so cool nice job! I hope this gets answered. \$\endgroup\$
    – Peilonrayz
    Commented May 17, 2020 at 22:59

1 Answer 1

1
\$\begingroup\$

I'm more of a Java guy, but I'll add my 2 cents on the collision detection, because you said it was not always working properly; I think you might solve this by creating a 'vecor' object, or ray. The idea is simple; instead of checking the static shapes, check if the vector intersects a block; and take the first intersection.

In 1D:

 t   ---O--------|BLOCK|-----------------
 t+1 ------------|BLOCK|-O---------------

If the ball is moving quickly it will miss the shape of the block.

Now transform the ball to a 'ray'. If you follow the ray, you'll see it hits the left of the block.

 t   ---O--------|BLOCK|-----------------
 ray ---O>>>>>>>>>>>>>>>>>>--------------

You now know it will collide with the left of the block and can calculate the bounce:

 t   ---O--------|BLOCK|-----------------
 ray ---O>>>>>>>>------------------------
       <<<<<<<<<<
 t+1 --O---------|BLOCK|-----------------

So the ball should be at the new position.

\$\endgroup\$
4
  • \$\begingroup\$ What I do currently is divide the board into rows, and update the row of every ball each loop and check for circle-rectangle collisions normally. Do you think this would be a performance improvement, or end up slowing the game down further? From what I understand, the collisions of 3 different lines must be calculated (otherwise some collisions would be missed). Additionally, should I have to check the lines against every single other box, or should I keep what I have now where ball-box collision is only checked within rows? \$\endgroup\$
    – xXCoolinXx
    Commented May 21, 2020 at 3:02
  • \$\begingroup\$ @Erdogan yes it will take more processing power if you need to calculate more boxes. However, if you partition your space (for example using quadtrees) you can quick eliminate a whole lot of boxes to check \$\endgroup\$ Commented May 23, 2020 at 9:45
  • \$\begingroup\$ I looked at the Wikipedia page for quadtrees and I (kind of) understand them, but I'm not really sure how a quadtree would be implemented with collision detection. Could you point me to an example or explain roughly how one would work? \$\endgroup\$
    – xXCoolinXx
    Commented May 23, 2020 at 13:44
  • \$\begingroup\$ @Erdogan Here's one. \$\endgroup\$
    – Linny
    Commented May 25, 2020 at 2:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.