Looking for feedback on my first game, Endless Breakout (pygame)

Started by
4 comments, last by Andrew_ 7 years, 8 months ago

Hi, I’m pretty new to game development, and I’ve spent about a couple weeks on the following game:

https://github.com/Andrew-RK/Endless-Breakout

Endless Breakout is a breakout game where you have to survive as long as possible, by keeping an ever increasing number of balls from going out of bounds. I’ve been coding it in python using the pygame library. I’ve also got some additional features I'd like to add in the future, such as targets to aim the balls at for survival and scoring benefits. Feedback from both a gameplay perspective and a programming perspective would be helpful. Also added older versions of my game for comparison sake.

I'd also like to add that I'm not just making a breakout game because it's easy to make. I'm also a big fan of breakout games and used to play them quite a bit, such as Ricochet Infinity, Shatter, and Alphabounce. There's actually quite a few ideas related to breakout that I'd like to prototype, so this won't be the last time I make a game with a ball and paddle. Despite all the breakout games that currently exist, I believe that there's still potential for interesting mechanics in the genre, so it's definitely something I want to come back to further down the road, when I have more experience under my belt.

My best score in v0.3.1 of this game is 6628.

Advertisement

For reference, I looked at
https://github.com/Andrew-RK/Endless-Breakout/blob/a06551fd03a44684f2ca4591beb65f3edcce0a10/ebalphav031.py

I just picked some things that I spotted, hopefully these are useful.


if int(ElapsedTime % 1000 < 10):
    displayMilliseconds = '00'+str(int(ElapsedTime % 1000))
elif int(ElapsedTime % 1000 < 100):
    displayMilliseconds = '0'+str(int(ElapsedTime % 1000))
else:
    displayMilliseconds = str(int(ElapsedTime % 1000))

This looks a bit bulky. First of all you're computing "int(ElapsedTime % 1000)" several times here (except in the first 'if' condition, but that may be an error).
You may want to compute it once, and store the value in a variable.

More importantly, Python has string-formatting, which can do the above for you, ie


displayMilliseconds = "{:03d}".format(int(ElapsedTime % 1000))

Your main loop starts at around line 150, and ends at around line 450. If you break that up in a few functions, things get more local, more modular, so it's easier to take out some part, and replace it with something else.
Also, the loop itself becomes much shorter then, so it's easier to understand. (Stuff that can be read from one screen or one page of paper are way easier to understand than when you have to scroll 5 pages down. This especially holds for ending loops. You jump back indenting, but to what point 4 pages ago do you now jump?)


fontSlot = 0
drawText('Highscore:', statsSurface, fontMargin, defaultFontSize * fontSlot + fontMargin, BLACK)
drawText(str(highscore), statsSurface, fontValuesPositionX, defaultFontSize * fontSlot + fontMargin, BLACK)
fontSlot = 1
drawText('Score:', statsSurface, fontMargin, defaultFontSize * fontSlot + fontMargin, BLACK)
drawText(str(score), statsSurface, fontValuesPositionX, defaultFontSize * fontSlot + fontMargin, BLACK)
fontSlot = 2
...

looks very same-ish too. If you align vertically, it becomes easier to read


drawText('Highscore:',   statsSurface, fontMargin,          defaultFontSize * fontSlot + fontMargin, BLACK)
drawText(str(highscore), statsSurface, fontValuesPositionX, defaultFontSize * fontSlot + fontMargin, BLACK)

Now you can clearly see that only the 1st and the 3rd argument are different.

One way to simplify this is to make a function that combines two of these 'draw' calls into one, but that will break the improvement further down with lines 422 and further.

Instead, why not make a list of values that you want to print:


lines = [('Highscore:',   fontMargin,          0),
         (str(highscore), fontValuesPositionX, 0),
          ...
        ]

for text, xPos, fontSlot in lines:
    drawText(text, statsSurface, xPos, defaultFontSize * fontSlot + fontMargin, BLACK)

This is very compact, but understanding what it now actually does is complicated. It's fine to use more lines, and use intermediate variables


balls.append({'rect':pygame.Rect(random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE),
random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE), BALLSIZE, BALLSIZE),
'localspeed':1.0, 'direction':DOWNRIGHT, 'angle':45.0, 'Xdecimal':0, 'Ydecimal':0, 'freshlyspawned':False})

ball ={'rect':pygame.Rect(random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE),
                          random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE),
                          BALLSIZE,
                          BALLSIZE),
       'localspeed':1.0,
       'direction':DOWNRIGHT,
       'angle':45.0,
       'Xdecimal':0,
       'Ydecimal':0,
       'freshlyspawned':False,   # Adding a "," at the end and putting the } at the next line makes it easier to add new fields.
      }

balls.append(ball)

Not too happy with this, let's do another step:


ball_xpos = random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE)
ball_xpos = random.randint(BALLSIZE, PLAYFIELDSIZE-BALLSIZE)

ball ={'rect':pygame.Rect(ball_xpos, ball_ypos, BALLSIZE, BALLSIZE),
       'localspeed':1.0,
       'direction':DOWNRIGHT,
       'angle':45.0,
       'Xdecimal':0,
       'Ydecimal':0,
       'freshlyspawned':False,
}   # Some people prefer the } at the left margin, below "ball" instead of below "{"

balls.append(ball)

You can make code look much better by adding intermediate variables and intermediate assignments. Since Python uses references, additional variables do not take extra time.

I don't know if you have read about classes yet, but your ball screams for one. I'll make it, and you can have a look at it after reading about classes.



class Ball(object):    # In Python 3, it's just "class Ball:"
    def __init__(self, rect, direction, angle, spawned):
        self.rect = rect
        self.localspeed = 1.0
        self.direction = direction
        self.angle = angle
        self.Xdecimal = 0
        self.Ydecimal = 0
        self.freshlyspawned = spawned

ball = Ball(pygame.Rect(ball_xpos, ball_ypos, BALLSIZE, BALLSIZE), DOWNRIGHT, 45.0, False)

You can access eg "direction" with ball.direction, which is a lot better than ball["direction"] :)

I hard-coded values for some variables (self.localspeed = 1.0), and added others to the "Ball" creation call at the last line. This is a matter of preference, change it if you want.

Ball change direction can also be coded differently.


if ball['rect'].top < 0:
    if ball['direction'] == UPLEFT:
        ball['direction'] = DOWNLEFT
    if ball['direction'] == UPRIGHT:
        ball['direction'] = DOWNRIGHT

A minor point is that both inner 'if's test for separate conditions, and at most one of them holds, so the second 'if' (4th line) can be an "elif" instead.

You can code this simpler with a dictionary. Say we have


d = {'a': 1, 'b': 2}

# Normal indexing fails for unknown keys:
d['a']  returns 1
d['q']  error

# 'get', returns a default
d.get('a')  returns 1
d.get('q')  returns None (indicating 'no value found')
d.get('a', 21) returns 1 (it's still available in d)
d.get('q', 21) returns 21 (21 became a new value indicating 'no value found in the dictionary')

So what about (warning some magic ahead)


top_changes = {UPLEFT:  DOWNLEFT,
               UPRIGHT: DOWNRIGHT}
if ball['rect'].top < 0:
    ball['direction'] = top_changes.get(ball['direction'], ball['direction'])

The first "ball['direction']" in the 'get' is the key we are looking for, if it's in the dictionary, you get its value. If it's not in the dictionary, you get the value of the second "ball['direction']", which means nothing changes.

One for pondering about: What is the relation between "direction" and "angle", in particular if angle runs from 0 to 360 degrees?

Finally, the duplicate-ish lines for updating the print score, see if you can improve that:
Lines 422 to around 460 look a lot like the initial text draw sequence. Can you make this a function, and use that function at both places?
If you add or move entries later, you have to change only that function, instead of at two places that you have to do now.

Thanks for the advice. I've read about OOP and classes but have never used them before. Looks like now's a good time for me to start. I'll look into your other suggestions, and I also got a suggestion elsewhere to split my code up into multiple files, so I'll look into that as well.

Technically, I use a class, but I only use it as a container of values, sort of a tuple with names instead of numbers.

OOP is a lot more, where you have functions inside your classes, and you extend classes from existing ones. Takes a while to get the hang of it, but definitely worth trying.

Splitting up into several files is useful too, for larger projects.

Hi!

I played the game and I noticed a couple things from a gameplay perspecitive: if you move the paddle toward the ball fast enough, the ball will actually move though the paddle before bouncing. Most times the ball will still reverse direction, but sometimes the ball will go through the paddle completely and not bounce. A few other times I accidentally hit the ball downward (hit it with the bottom edge of the paddle), but the ball still bounced upward.

Hi.

Yeah, physics are definitely something I'm going to work on for the next update. I've been on a hiatus from programming for awhile but I'm about to start working on this game again now. Hopefully I'll be able to get those things fixed.

This topic is closed to new replies.

Advertisement