Basics of Gaming while making (not perfect) Breakout!
Show me the game!
Yes the graphics look bad, but you can make them better!
About me: Dan M.
- Senior Software Engineer at the Organic Box
- My past: Infrastructure Engineering jobs and Network Engineering jobs.
- Went to school for Electrical Engineering (I don't do that anymore)
- I like kitties, badminton and development.
Feel Free to reach out!
- You can reach me on linkedin!
https://www.linkedin.com/in/daniel-mouris-baa4b972/ - You can reach me on twitter!
https://twitter.com/dgmouris
(although I mainly tweet about edmontonpy meetups) - You can email me at dgmouris@gmail.com
- Slack as well (dgmouris)
Disclaimer:
You can ask questions.
I haven't made many games in my life (You'll see, I'm not an expert)
Don't be afraid to learn!
Why teal slides?
It's a curve ball.
What am I going to talk about?
-
How to dunk a basketball
-
Basics of Game Development
-
What you need to know for this talk
-
Building Breakout!
- Pygame Basics and Boiler Plate needed for our game
- Drawing Shapes for our game
- Handling Events
- Collision Detection
- Gameplay
- Programming the Game Menu (simplistically)
- End Game
How to dunk a basketball.
-
Hold a Basketball
-
Jump high
-
Profit!
The Basics of game development
What are the pieces to a game?
- Main loop
- Game Physics
- Artificial Intelligence (not going to cover in depth)
- Playing audio (not going to cover in depth)
- Lives Score and Levels (not going to cover in depth)
- Storage (not going to cover in depth)
- Networking (not going to cover in depth)
Main Loop
What does the main loop do?
- Handling events
- Updating State
- Drawing stuff
What is the main loop?
The main loop is basically the piece of the program that refreshes the screen.
Also is your framerate is normally either 30-60 frames per second.
Game Physics
You'll either have to write your own (like we will today)
Normally comes with a game engine read: Unity, UDK ...
Artificial Intelligence
Is how will enemies in your game challenge the player.
We're not going to touch on this today.
Playing Audio
Sound Effects
Music
We're not going to touch on this today.
Tracking Lives and Scores
Basically just keeping score in your game.
We're not going to touch on this today.
Storage
Just storing progress either locally or on a server of sorts.
We're not going to touch on this today.
Networking
No, Not people networking (Nope never)
This is super complicated and there is no way that I can talk about any of this.
Examples: Games that use xbox live.
We're not going to touch on this today.
What you're going to need to know.
nested functions (and their scope)
lambda functions
Basic Class Inheritance
Basic OOP
Let's Build Breakout!
Code is here:
https://github.com/dgmouris/pygame-tutorial
How are we building Breakout?
- Pygame Basics and Boiler Plate needed for our game
- Drawing Shapes and Text for our game
- Handling Events
- Gameplay
- Collision Detection
- Programming the Game Menu (simplistically)
- End Game
Pygame Basics and boiler plate code
Stuff specific to pygame and our project to make it work.
File Structure
- The ball.py, brick.py and paddle.py are going to contain their respective parts.
- config and colors just look at the repo, I stole this but don't tell anyone.
- text_object.py will be the object that displays text on the screen.
- game.py will contain the Game Object.
├── Pipfile
├── Pipfile.lock
├── README.md
├── ball.py
├── breakout.py
├── brick.py
├── button.py
├── colors.py
├── config.py
├── game.py
├── game_object.py
├── images
│ └── background.jpg
├── paddle.py
├── sound_effects
│ ├── brick_hit.wav
│ ├── effect_done.wav
│ ├── level_complete.wav
│ └── paddle_hit.wav
└── text_object.py
Notes on the structure
- The Game Object is a visual object that knows how to render itself.
- Is going to be a base for other objects.
- It has an update method which updates the objects position (based on speed)
- As well the draw() method.
from pygame.rect import Rect
class GameObject:
def __init__(self, x, y, w, h, speed=(0,0)):
self.bounds = Rect(x, y, w, h)
self.speed = speed
def draw(self, surface):
pass
def move(self, dx, dy):
self.bounds = self.bounds.move(dx,
dy)
def update(self):
if self.speed == [0, 0]:
return
self.move(*self.speed)
@property
def left(self):
return self.bounds.left
@property
def right(self):
return self.bounds.right
... it goes on.
@property
def top(self):
return self.bounds.top
@property
def bottom(self):
return self.bounds.bottom
@property
def width(self):
return self.bounds.width
@property
def height(self):
return self.bounds.height
@property
def center(self):
return self.bounds.center
@property
def centerx(self):
return self.bounds.centerx
@property
def centery(self):
return self.bounds.centery
The Game Object
- The game class is the core.
- It runs the main loop of the game.
- __init__ method initializes the game.
- update and draw objects loop over objects either draw or update in the main loop.
import pygame
import sys
from collections import defaultdict
class Game:
def __init__(self,
caption,
width,
height,
back_image_filename,
frame_rate):
self.background_image = \
pygame.image.load(back_image_filename)
self.frame_rate = frame_rate
self.game_over = False
self.objects = []
pygame.mixer.pre_init(44100, 16, 2, 4096)
pygame.init()
pygame.font.init()
self.surface = pygame.display.set_mode((width, height))
pygame.display.set_caption(caption)
self.clock = pygame.time.Clock()
self.keydown_handlers = defaultdict(list)
self.keyup_handlers = defaultdict(list)
self.mouse_handlers = []
def update(self):
for o in self.objects:
o.update()
def draw(self):
for o in self.objects:
o.draw(self.surface)
The Game Class
- handle_events method basically just takes in user input
- run method is the main loop discussed earlier.
import pygame
import sys
from collections import defaultdict
class Game:
... (on other page)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
for handler in self.keydown_handlers[event.key]:
handler(event.key)
elif event.type == pygame.KEYUP:
for handler in self.keydown_handlers[event.key]:
handler(event.key)
elif event.type in (pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP,
pygame.MOUSEMOTION):
for handler in self.mouse_handlers:
handler(event.type, event.pos)
def run(self):
while not self.game_over:
self.surface.blit(self.background_image, (0, 0))
self.handle_events()
self.update()
self.draw()
pygame.display.update()
self.clock.tick(self.frame_rate)
The Game Class, cont'd
Drawing Shapes and Text
Drawing stuff on the screen where we want it
- Shows text on the screen!
import pygame
class TextObject:
def __init__(self, x, y, text_func,
color, font_name, font_size):
self.pos = (x, y)
self.text_func = text_func
self.color = color
self.font = pygame.font.SysFont(
font_name,
font_size
)
self.bounds = self.get_surface(
text_func()
)
def draw(self, surface, centralized=False):
text_surface, self.bounds = \
self.get_surface(self.text_func())
if centralized:
pos = (
self.pos[0] - self.bounds.width // 2,
self.pos[1]
)
else:
pos = self.pos
surface.blit(text_surface, pos)
def get_surface(self, text):
text_surface = self.font.render(
text, False, self.color)
return text_surface, text_surface.get_rect()
def update(self):
pass
The Text Object
What Kind of Shapes can I draw?
- rect
- polygon
- circle
- ellipse
- arc
- line
- lines
- anti-aliased line
- anti-aliased lines
- We all know bricks are
circlesovalsrectangles
import pygame
from game_object import GameObject
class Brick(GameObject):
def __init__(self, x, y, w, h,
color, special_effect=None):
GameObject.__init__(self, x, y, w, h)
self.color = color
self.special_effect = special_effect
def draw(self, surface):
pygame.draw.rect(surface,
self.color, self.bounds)
Drawing the Brick
-
basketballsballs are very similar looking to the brick class. - inherits from gameObject
class Ball(GameObject):
def __init__(self, x, y, r, color, speed):
GameObject.__init__(self,
x - r,
y - r,
r * 2,
r * 2,
speed)
self.radius = r
self.diameter = r * 2
self.color = color
def draw(self, surface):
pygame.draw.circle(surface,
self.color,
self.center,
self.radius)
def update(self):
super().update()
Balls!
- just another rectangle, although this moves left and right.
import pygame
import config
from game_object import GameObject
class Paddle(GameObject):
def __init__(self, x, y, w, h, color, offset):
GameObject.__init__(self, x, y, w, h)
self.color = color
self.offset = offset
self.moving_left = False
self.moving_right = False
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.bounds)
Let's Draw the paddle
Handling Events
Mouse Clicks or Key Board presses
What are the Events that we're going to handle?
- Mouse Events
- Key Press Events
- Timed Events
- when a key is pressed the handle() method is called.
import pygame
import config
from game_object import GameObject
class Paddle(GameObject):
def __init__(self, x, y, w, h, color, offset):
GameObject.__init__(self, x, y, w, h)
self.color = color
self.offset = offset
self.moving_left = False
self.moving_right = False
def update(self):
if self.moving_left:
dx = -(min(self.offset, self.left))
elif self.moving_right:
dx = min(self.offset,
config.screen_width - self.right)
else:
return
self.move(dx, 0)
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.bounds)
def handle(self, key):
if key == pygame.K_LEFT:
self.moving_left = not self.moving_left
else:
self.moving_right = not self.moving_right
Key Events
- This basically just handles any mouse movement and clicks.
class Button(GameObject):
def __init__(self, x, y, w, h, text, on_click=lambda x: None, padding=0):
super().__init__(x, y, w, h)
self.state = 'normal'
self.on_click = on_click
self.text = TextObject(x + padding, y + padding, lambda: text,
config.button_text_color,
config.font_name,
config.font_size)
...
def handle_mouse_event(self, type, pos):
if type == pygame.MOUSEMOTION:
self.handle_mouse_move(pos)
elif type == pygame.MOUSEBUTTONDOWN:
self.handle_mouse_down(pos)
elif type == pygame.MOUSEBUTTONUP:
self.handle_mouse_up(pos)
def handle_mouse_move(self, pos):
if self.bounds.collidepoint(pos):
if self.state != 'pressed':
self.state = 'hover'
else:
self.state = 'normal'
def handle_mouse_down(self, pos):
if self.bounds.collidepoint(pos):
self.state = 'pressed'
def handle_mouse_up(self, pos):
if self.state == 'pressed':
self.on_click(self)
self.state = 'hover'
Mouse Events!
- Timer Events are not specified in the main loop.
- When would you use them?
- Freezing the game for example.
class Breakout(Game):
...
def show_message(self,
text,
color=colors.WHITE,
font_name='Arial',
font_size=20,
centralized=False):
message = TextObject(
config.screen_width // 2,
config.screen_height // 2,
lambda: text, color,
font_name, font_size
)
self.draw()
message.draw(self.surface, centralized)
pygame.display.update()
time.sleep(config.message_duration)
Timer Events
Game Play
Programming the rules and moving objects based on the game state
- Stops the paddle from moving off the screen!
import pygame
import config
from game_object import GameObject
class Paddle(GameObject):
def __init__(self, x, y, w, h, color, offset):
GameObject.__init__(self, x, y, w, h)
self.color = color
self.offset = offset
self.moving_left = False
self.moving_right = False
def update(self):
if self.moving_left:
dx = -(min(self.offset, self.left))
elif self.moving_right:
dx = min(self.offset,
config.screen_width - self.right)
else:
return
self.move(dx, 0)
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.bounds)
def handle(self, key):
if key == pygame.K_LEFT:
self.moving_left = not self.moving_left
else:
self.moving_right = not self.moving_right
Paddle Rules/Play
- In the break out class (the main game class) we're just going to make a method to create balls.
- Ball movement is already taken care of
class Breakout(Game):
...
def create_ball(self):
speed = (random.randint(-2, 2),
config.ball_speed)
self.ball = Ball(config.screen_width // 2,
config.screen_height // 2,
config.ball_radius,
config.ball_color,
speed)
self.objects.append(self.ball)
Creating the ball to start
Collision Detetection
Detecting when objects are colliding or "hitting" each other
- This gives info on whether or not the ball hit something.
- If it hit something it's going to respond with:
- 'left'
- 'right'
- 'top'
- 'bottom'
- Doesn't matter if it's the paddle or a brick.
class Breakout(Game):
...
def handle_ball_collisions(self):
def intersect(obj, ball):
edges = dict(
left=Rect(obj.left, obj.top,
1, obj.height),
right=Rect(obj.right, obj.top,
1, obj.height),
top=Rect(obj.left, obj.top,
obj.width, 1),
bottom=Rect(obj.left, obj.bottom,
obj.width, 1))
collisions = set(
edge for edge, rect in edges.items() if
ball.bounds.colliderect(rect))
if not collisions:
return None
if len(collisions) == 1:
return list(collisions)[0]
if 'top' in collisions:
if ball.centery >= obj.top:
return 'top'
if ball.centerx < obj.left:
return 'left'
else:
return 'right'
if 'bottom' in collisions:
if ball.centery >= obj.bottom:
return 'bottom'
if ball.centerx < obj.left:
return 'left'
else:
return 'right'
Intersection
- Ball hitting the paddle will have the same horizontal speed, but vertical speed will be inversed.
- If the paddle is moving then the horizontal speed is increased.
class Breakout(Game):
...
def handle_ball_collisions(self):
...
# Hit paddle
s = self.ball.speed
edge = intersect(self.paddle, self.ball)
if edge is not None:
self.sound_effects['paddle_hit'].play()
if edge == 'top':
speed_x = s[0]
speed_y = -s[1]
if self.paddle.moving_left:
speed_x -= 1
elif self.paddle.moving_left:
speed_x += 1
self.ball.speed = speed_x, speed_y
elif edge in ('left', 'right'):
self.ball.speed = (-s[0], s[1])
Paddle Collisions
- If the top of the ball is lower than the floor then create a new ball, unless there's no lives.
class Breakout(Game):
...
def handle_ball_collisions(self):
...
# Hit floor
if self.ball.top > config.screen_height:
self.lives -= 1
if self.lives == 0:
self.game_over = True
else:
self.create_ball()
Floor Collision
- If it hits the ceiling it just reverses the vertical speed.
- If it hits the right or left wall it just reverses the horizontal speed.
- Note: s is speed here.
class Breakout(Game):
...
def handle_ball_collisions(self):
...
# Hit ceiling
if self.ball.top < 0:
self.ball.speed = (s[0], -s[1])
# Hit wall
if (self.ball.left < 0 or
self.ball.right > config.screen_width):
self.ball.speed = (-s[0], s[1])
Ceiling & Wall
Collision
- if there is an intersection of the bricks
- we remove the bricks for the bricks list for the game and the objects of the game.
- We also add to the score.
- The ball continues to move in the opposite way of the collision.
- Note: s is speed here.
class Breakout(Game):
...
def handle_ball_collisions(self):
...
# Hit brick
for brick in self.bricks:
edge = intersect(brick, self.ball)
if not edge:
continue
self.bricks.remove(brick)
self.objects.remove(brick)
self.score += self.points_per_brick
if edge in ('top', 'bottom'):
self.ball.speed = (s[0], -s[1])
else:
self.ball.speed = (-s[0], s[1])
Brick Collisions
The (very simple) Game Menu
Basically two buttons "play" and "quit"
The (very simple) Game Menu
- As you can see in the for loop, we create two buttons.
- Then we append then to the objects list, buttons list and then attach the handlers to them
- The on_play removes the buttons from the screen.
class Breakout(Game):
...
def create_menu(self):
def on_play(button):
for b in self.menu_buttons:
self.objects.remove(b)
self.is_game_running = True
self.start_level = True
def on_quit(button):
self.game_over = True
self.is_game_running = False
self.game_over = True
for i, (text, click_handler) in
enumerate((
('PLAY', on_play),
('QUIT', on_quit))
):
b = Button(config.menu_offset_x,
(config.menu_offset_y +
(config.menu_button_h + 5) * i),
config.menu_button_w,
config.menu_button_h,
text,
click_handler,
padding=5)
self.objects.append(b)
self.menu_buttons.append(b)
self.mouse_handlers.append(
b.handle_mouse_event
)
Creating the Menu Buttons
Ending the Game
Just the end, and showing the code of the main loop.
- Game over happens under any of these conditions:
- The player clicked the QUIT button from the menu.
- The player loses their last life.
- The player cleared all the bricks.
class Game:
...
def run(self):
while not self.game_over:
self.surface.blit(self.background_image, (0, 0))
self.handle_events()
self.update()
self.draw()
pygame.display.update()
self.clock.tick(self.frame_rate)
Game is over when main loop ends.
Conclusion
Games aren't that scary to program.
Start with an easy game (not AAA).
If I can do it, you can too!
Summary
I hoped you enjoyed this long talk.
Here's my kitties!
Marshmallow
Ghost
Gambit
Questions?
Basics of Game Development and Making Breakout!
By Daniel Mouris
Basics of Game Development and Making Breakout!
This is just going to cover general game development.
- 770