# Clone of mobile game Ballz

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:

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


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

outer_width = 2

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

#create the outline width since pygame doesn't implement it that.

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

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.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
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.
#Add the necessary number of balls
for i in range(0, max(board.ball_count, 1)):
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

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

#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.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:

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

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

#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
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

#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

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
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:
initial_ball -= spacing*unit_vector

"""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:
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:
else:
f.close()
else:

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.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 measures

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

#initialize and set the icon
pyg.display.set_mode(measures.window)
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()))

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
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: (These are to be saved in Images/)
• Finally, the icon image:

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:

• 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. – Peilonrayz May 17 at 22:59

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.

• 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? – Erdogan May 21 at 3:02
• @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 – RobAu May 23 at 9:45
• 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? – Erdogan May 23 at 13:44
• @Erdogan Here's one. – Linny May 25 at 2:54