California State University, Los Angeles

Transcription

California State University, Los Angeles
CS-332L: Logic Programming
Learning Python with 2D Games
California State University, Los Angeles
Computer Science Department
Lecture IV
Images, Sounds, Sprites,
Collision Detection, and Game Examples
NOTE ON PROPRIETARY ASSETS
❂
For this class I do not mind if you use sprites, images, graphics,
or other assets which come from copy-written material.
❂
HOWEVER, should you decide to make your game a public
project or develop it further to be sold, then you will need to
come up with your own assets or use open source items.
Otherwise lawyers may come after you with "nasty, big, pointy
teeth!" (yet another Python reference)
❂
REQUIREMENT FOR THIS CLASS: At the very least, your code
should document where your assets came from. If it is
something you did not create yourself I expect you to place
the proper acknowledgments in the comments at the beginning
of your main driver file.
Packages
Packages
❂
For this class you may want to organize your game into
packages.
❂
Lecture examples this week make use of packages to
keep code separated.
❂
A package is simply a directory with one requirement:
❂
−
this directory MUST include an __init__.py file. This file can
be empty, but it is required to tell python that the directory is
a package.
−
__init__.py can also have initialization code in it, but for this
class we probably do not need this. Google for more
information.
See the lecture examples for how to do package
imports.
Animation with Pygame
Animation
❂
Animation really boils down to a few simple steps:
−
Draw an image on a screen in one position.
−
Change the position of the image.
−
Clear the screen and redraw the image.
❂
One of the simplest animations to start with is the
"Bouncing Ball" animation.
❂
This example uses a custom sprite class to load an
image of a ball, and then bounce it around the
screen.
import pygame
from pygame.locals import *
ball.py – The Ball Class
class Ball(pygame.sprite.Sprite):
def __init__(self, x, y, vx, vy):
super().__init__();
self.image = pygame.image.load("pokeball.png").convert()
self.image.set_colorkey(pygame.Color(0, 0, 0))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vx = vx
self.vy = vy
def draw(self, SCREEN):
SCREEN.blit(self.image, (self.rect.x, self.rect.y))
def move(self, SCREEN):
r_collide = self.rect.x + self.image.get_width() +
self.vx > SCREEN.get_width()
l_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() +
self.vy > SCREEN.get_height()
# Check collision on right and left sides of screen
if l_collide or r_collide:
self.vx *= -1
# Check collision on top and bottom sides of screen
if t_collide or b_collide:
self.vy *= -1
self.rect.x += self.vx
self.rect.y += self.vy
import random
import pygame
Snow Animation – snow.py
class Snow:
def __init__(self, x, y, size, speed):
self.x = x
self.y = y
self.size = size
self.speed = speed
def fall(self):
self.y += self.speed
def draw(self, SCREEN):
pygame.draw.circle(SCREEN, pygame.Color(255, 255,
255), [self.x, self.y], self.size)
# Used when the snowflake reaches the end of the screen
def reset(self):
self.x = random.randint(0, 700)
self.y = -10
Snow Animation – generation
Function
def gen_snow_list(num):
"""Returns a list of snow objects"""
snow_list = []
for x in range(num):
rand_x = random.randint(0, 700)
rand_y = random.randint(0, 600)
rand_size = random.randint(2, 10)
rand_speed = random.randint(1, 5)
snow_list.append(Snow(rand_x, rand_y,
rand_size, rand_speed))
return snow_list
Snow Animation – Main
Game Loop
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
SCREEN.fill(BLACK)
for snowflake in snow_list:
snowflake.draw(SCREEN)
snowflake.fall()
if snowflake.y > 600:
snowflake.reset()
pygame.display.update()
FPS_CLOCK.tick(FPS)
Mouse Input
Mouse Basics
❂
Pygame allows you to have basic mouse controls.
❂
Pygame only checks for the three standard mouse buttons: left,
middle and right.
−
If you have a mouse with more buttons these will not be recognized.
−
There might be a library which adds more mouse button functionality,
but I have no researched this.
−
For this class the standard mouse buttons should be enough if you use
the mouse at all
❂
The pygame.mouse module has functions to deal with mouse
input.
❂
Documentation: https://www.pygame.org/docs/ref/mouse.html
Mouse Events
❂
When you start using the mouse you normally check
for three types of mouse events:
−
MOUSEBUTTONDOWN: generated when a mouse button is
pressed.
−
MOUSEBUTTONUP: generated when a mouse button is
released.
−
MOUSEMOTION: generated when the mouse is moved
across the surface.
Mouse Module Functions
❂
❂
pygame.mouse.get_pressed():
−
get_pressed() -> (button1, button2, button3)
−
returns a tuple of booleans representing the state of all
the mouse buttons
✦
button1: True if left button pressed otherwise False
✦
button2: True if middle button pressed otherwise False
✦
button3: True if right button pressed otherwise False
pygame.mouse.get_pos():
−
get_pos() -> (x, y)
−
returns a tuple of the x, y coordinates of the mouse (x, y)
Mouse Module Functions
❂
❂
❂
pygame.mouse.get_rel():
−
get_rel() -> (x, y)
−
Returns the amount of movement in x and y since the previous call to this
function.
pygame.mouse.set_pos():
−
set_pos([x, y]) -> None
−
Move the mouse to the given coordinates
−
Argument must be a list of x, y coordinates
pygame.mouse.set_visible():
−
set_visible(bool) -> bool
−
If the argument is True the cursor is displayed, if False, then the cursor is
hidden.
−
Returns the previous visible state of the cursor.
Drag and Drop
❂
Drag and drop is not a built in feature of Pygame, but
with a little creative thinking we can easily get this
working.
❂
General Steps:
−
Check if the mouse button has been pressed down if so:
✦
✦
✦
−
check to see if the mouse position collides with the area of the
object we want to move
if collision: set a boolean flag to be true
update the object with the new coordinates as long as the flag
we set earlier remains true every time the mouse is moved
Check if the mouse button has been released
✦
set the boolean flag to be false.
Game Controller Input
Game Controller Basics
❂
So you want to use a game controller with your game? No
problem! Pygame makes this quite easy.
−
❂
The pygame.joystick module has everything you need.
−
❂
a few functions to get things going
Also look at the pygame.joystick.Joystick class
−
❂
You can even manage multiple Joysticks at once.
this class has many functions related to using a joystick.
Documentation:
https://www.pygame.org/docs/ref/joystick.html
Game Controller Setup
❂
The first thing to do is initialize the joystick module:
pygame.joystick.init()
❂
pygame.joystick.get_count():
−
❂
You will also want to create a Joystick object for each joystick
recognized:
−
❂
Returns the number of recognized joysticks.
pygame.joystick.Joystick():
✦
Joystick(id) -> Joystick
✦
creates a Joystick with the given id
Initialize the controller object
−
joystick_object.init()
❂
❂
❂
❂
❂
pygame.joystick.Joystick.init():
Joystick Class Methods
−
init() -> None
−
must be called for each Joystick object to get the information about the Joystick
and its controls
pygame.joystick.Joystick.quit():
−
quit() -> None
−
unitializes the Joystick, Pygame event queue will no longer receive events from
this device.
pygame.joystick.Joystick.get_init():
−
get_init() -> bool
−
returns True of the controller is initialized, False otherwise
pygame.joystick.Joystick.get_id():
−
get_id() -> int
−
Returns the integer ID that you set for the device when you called the constructor.
pygame.joystick.Joystick.get_name():
−
get_name() -> string
−
Returns the name of the Joystick. This is a name assigned by the system
automatically. It should be a unique name.
Joystick Class Methods – Axis Controls
❂
❂
pygame.joystick.Joystick.get_numaxes():
−
get_numaxes() -> int
−
Returns the number of input axes are on a Joystick. There will usually be
two for the position. Controls like rudders and throttles are treated as
additional axes. The pygame.JOYAXISMOTION events will be in the range
from -1.0 to 1.0. A value of 0.0 means the axis is centered. Gamepad
devices will usually be -1, 0, or 1 with no values in between. Older analog
joystick axes will not always use the full -1 to 1 range, and the centered
value will be some area around 0. Analog joysticks usually have a bit of
noise in their axis, which will generate a lot of rapid small motion events.
pygame.joystick.Joystick.get_axis():
−
get_axis(axis_number) -> float
−
Returns the current position of a joystick axis. The value will range from -1
to 1 with a value of 0 being centered. You may want to take into account
some tolerance to handle jitter, and joystick drift may keep the joystick from
centering at 0 or using the full range of position values. The axis number
must be an integer from zero to get_numaxes()-1.
Joystick Class Methods – Button Control
❂
❂
pygame.joystick.Joystick.get_numbuttons():
−
get_numbuttons() -> int
−
Returns the number of pushable buttons on the joystick.
These buttons have a boolean (on or off) state. Buttons
generate a pygame.JOYBUTTONDOWN and
pygame.JOYBUTTONUP event when they are pressed
and released.
pygame.joystick.Joystick.get_button():
−
get_button(button) -> bool
−
Returns the current state of a joystick button.
Joystick Class Methods – Hat Controls
❂
❂
pygame.joystick.Joystick.get_numhats():
−
get_numhats() -> int
−
Returns the number of joystick hats on a Joystick. Hat devices are like
miniature digital joysticks on a joystick. Each hat has two axes of input. The
pygame.JOYHATMOTION event is generated when the hat changes
position. The position attribute for the event contains a pair of values that
are either -1, 0, or 1. A position of (0, 0) means the hat is centered.
pygame.joystick.Joystick.get_hat():
−
get_hat(hat_number) -> x, y
−
Returns the current position of a position hat. The position is given as two
values representing the X and Y position for the hat. (0, 0) means centered.
A value of -1 means left/down and a value of 1 means right/up: so (-1, 0)
means left; (1, 0) means right; (0, 1) means up; (1, 1) means upper-right;
etc. This value is digital, i.e., each coordinate can be -1, 0 or 1 but never inbetween. The hat number must be between 0 and get_numhats()-1.
Using Basic Images
Images
❂
❂
Images can be used in your game as either
backgrounds, characters, objects, etc.
−
For background images use .jpg images. JPEG
compression is good enough for background images.
−
For other things use .gif or .png (see next slide)
−
You may also need to resize an image before loading it,
make sure to do this manually in MS Paint, Photoshop
Gimp, or other related program.
NOTE: The steps in this section are mainly for
images which will not be involved in any collision
detection.
JPG and Transparency
Loading Images
❂
pygame.image.load("path_to_image"):
−
load(filename) -> Surface
−
returns a Surface object representing the image (be sure to
assign this to a variable)
❂
Images should generally be loaded before the main
game loop otherwise you might be loading the same
image hundreds of times which could slow down game
performance.
❂
Finally call the convert() function to convert your
image into a format Pygame can more easily work with.
Displaying the Image on the Main Surface
❂
Once an image has been loaded and converted, you
can set it to the background by taking your main
screen object and then calling the blit() function
−
blit() will transfer your image surface onto the main
surface.
−
blit() takes a surface (image) and an x, y tuple for the
location of where to place the image.
Alpha Channel and Transparency
❂
Getting transparency to work can be a bit tricky. You need to
consider some cases.
❂
If your image already has a background color that is transparent,
use the convert_alpha() method to allow the background to be
transparent.
❂
If your image does not have transparency set for a background
color, you can use the set_colorkey() function and specify the
background color to be transparent.
❂
If you want your entire image to be transparent, use convert()
and then the set_alpha() function
−
set_alpha() takes a value from 0 to 255, 0 fully transparent, 255 opaque
Sounds and Music
Adding Sound Effects
❂
Adding sounds / music to your game is quite simple.
−
❂
For sounds the best format to use is a .ogg format sound.
−
❂
Use the Sound class in the mixer module.
If you have a sound in another format, you may be able to convert it to .ogg format
using a program called Audacity.
Like images, sounds should be loaded BEFORE the main game loop.
laser_sound = pygame.mixer.Sound('laser.ogg')
while True: # <--- main game loop
for event in pygame.event.get():
if event.type == MOUSEBUTTONDOWN:
buttons = pygame.mouse.get_pressed()
if buttons[0]:
laser_sound.play()
Adding Music (Background Music)
❂
When adding background music, use the music module.
−
❂
Sound is for small short sounds, but music will stream a longer sound file
instead of loading it all at once.
You can set an end event to trigger when a song is done playing.
Using the USEREVENT event can be a way to keep the song playing.
pygame.mixer.music.load('tetris.ogg')
pygame.mixer.music.set_endevent(USEREVENT)
pygame.mixer.music.play()
while True: # <--- main game loop
for event in pygame.event.get():
if event.type == USEREVENT:
pygame.mixer.music.play()
Sprites and
Collision Detection
Collision Detection and Sprites
❂
Collision detection is one of the major calculations that is
performed in a video game.
❂
Collision detection can be one of the most expensive tasks
(computation wise) That your game will have to perform.
❂
In order for collision detection to work, the items you want to test
for collisions have to be Sprite objects.
❂
−
a sprite is just a two-dimensional image or animation that is integrated
into a larger scene.
−
the name is a holdover from the 8-bit game era.
The image examples we saw previously were for static images,
now we will switch to Sprites for collision detection.
Collision Detection and Sprites
❂
NOTE: That a sprite can be a single image, it could
be one of a series of images from a sprite sheet, or it
could even just be a shape drawn using one of the
pygame drawing functions.
❂
What makes a sprite a "Sprite" is that it should be an
object which is a subclass of
pygame.sprite.Sprite
❂
The following example will give you an idea of how
to use sprites and collision detection.
Sprite Class Setup
❂
Make your class a subclass of pygame.sprite.Sprite
class MySprite(pygame.sprite.Sprite):
❂
In the constructor of your class, call the parent class,
super constructor
super().__init__()
❂
Create the image that will appear on screen.
−
The "image" could be a shape, actual image, just a Surface
object with color filled in.
−
You must set this image to the self.image attribute
self.image = pygame.Surface([width, height])
self.image.fill(color)
Sprite Class Setup
❂
Finally, you need to set the self.rect property.
−
This rectangle will be the bounding box which pygame
will use to check for collisions between this box and other
bounding boxes.
−
This variable must be an instance of pygame.Rect and
represents the dimensions of the Sprite.
−
The Rect class has attributes for the x and y values of
the location of the Sprite, and these values are what you
alter if you want to move the sprite.
✦
mySpriteRef.rect.x and mySpriteRef.rect.y
self.rect = self.image.get_rect()
The Player Class – Using an Image as a Sprite
import pygame
class Player(pygame.sprite.Sprite):
def __init__(self):
# Call the parent class (Sprite) constructor
super().__init__()
# Create the image
self.image = pygame.image.load('creeper.png').convert()
# Set the bounding box
self.rect = self.image.get_rect()
The Block Class – Using a Filled Surface as a Sprite
import pygame
class Block(pygame.sprite.Sprite):
def __init__(self, color, width, height):
# Call the parent class (Sprite) constructor
super().__init__()
# Create the image
self.image = pygame.Surface([width, height])
self.image.fill(color)
# Get the bounding box for the sprite
self.rect = self.image.get_rect()
The MyCircle Class – Using a Drawn Shape as a Sprite
import pygame
class MyCircle(pygame.sprite.Sprite):
def __init__(self, color, radius):
# Call the parent class (Sprite) constructor
super().__init__()
# Create the image
self.image = pygame.Surface([radius * 2, radius * 2])
self.image.fill(pygame.Color(255, 255, 255))
# Draw the circle in the image (Surface)
pygame.draw.circle(self.image, color, radius)
# Set the bounding box
self.rect = self.image.get_rect()
Groups
❂
Groups can provide a powerful way to manage a lot
of sprites at once.
❂
A Group is a class in Pygame which is essentially a
list of Sprite objects.
❂
You can draw and move all sprites with one
command if they are in a Group and you can even
check for collisions against an entire Group.
Groups
❂
Our example will use two Group lists:
block_list = pygame.sprite.Group()
all_sprites_list = pygame.sprite.Group()
−
all_sprites_list contains every sprite in the game,
this list will be used to draw all of the sprites.
−
block_list holds each object that the player can collide
with so it holds every object except for the player. if the
player were in this list then Pygame will always say the
player is colliding with an item even when they are not.
Checking for Collisions
❂
pygame.sprite.spritecollide():
−
Returns a list of all Sprites in a Group that have collided
with another Sprite. The third parameter is a boolean
value when set to True, will remove that sprite from the
group.
blocks_hit_list =
pygame.sprite.spritecollide(player, block_list, True)
# Check the list of collisions.
for block in blocks_hit_list:
score += 1
print(score)
RenderPlain and RenderClear
❂
pygame.sprite.RenderPlain and
pygame.sprite.RenderClear are simply aliases to
pygame.sprite.Group.
−
❂
They do the same exact thing as Group and no additional
functionality, they are only another name for a Group.
pygame.sprite.GroupSingle
−
a "group" that only holds a single sprite, when you add a
new sprite the old one is removed.
pygame.sprite.RenderUpdates
❂
This is a subclass of Group that keeps track of sprites that
have changed. Really only useful with games where there is
a static (non-moving) background.
−
❂
Still is basically an unordered list of Sprites with one overridden
function from Group
pygame.sprite.RenderUpdates.draw():
−
draw(surface) → Rect_list
−
Overrides the draw() function from Group and returns a list of
pygame.Rect objects (rectangular areas which have been
changed). This also includes areas of the screen that have been
affected by previous Group.clear() calls.
−
This returned list should be passed to pygame.display.update().
✦
update() can take a list of Rect objects to update instead of just calling
update on everything.
Basics Steps for Using RenderUpdates
❂
Use the pygame.sprite.Group.clear() function to clear your
old sprites
−
clear(Surface, background) -> None
−
erases all of the sprites used in the last pygame.sprite.Group.draw()
function call. fills the areas of the drawn sprites with the background.
❂
Update your sprites.
❂
Draw the sprites by using the draw() function from
RenderUpdates.
❂
Pass the list returned by the previous step to the
pygame.display.update() function.
pygame.sprite.OrderedUpdates
❂
This is a subclass of RenderUpdates that draws the
Sprites in the order they were placed in the list.
−
❂
Adding and removing from this kind of Group will be a bit
slower than regular Groups.
Can be used to provide layering, but only really good
for simple layers (between only a few sprites)
pygame.sprite.LayeredUpdates
❂
This is another Group type which handles layers and
draws like OrderedUpdates.
❂
Sprites added to this type of group MUST have a
self._layer attribute with an integer.
−
The lower the integer value, the further back the item will
be drawn when compared to other layers.
DirtySprite
DirtySprite Basics
❂
This is a subclass of Sprite with added attributes and features. This can
be used to give you more precise control over whether or not an individual
sprite should be repainted (instead of just repainting them all)
−
❂
"dirty" is a term used to refer to a sprite that is "out of date" or needs to be
updated (redrawn / repainted).
−
❂
Better than RenderUpdates because this RenderUpdates does not know anything
about how each sprite works. It can optimize drawing areas, but it does not know
if a sprite has changed or not. This is why RenderUpdates still redraws sprites that
haven't even moved.
Honestly I don't know if this is a term used in gaming or if this was something
Pygame came up with. I couldn't find anything else related to it on Google.
If you use DirtySprite, then you have to keep track individually which
sprites in your game need to be redrawn at which points in time.
−
This can be a lot more to manage, and for simple games is unecessary
−
However it does provide some advantages...
DirtySprite Basics
❂
As discussed last week, drawing things on the screen
can be very intensive for your game.
❂
DirtySprite and LayeredDirty can be used in
place of Sprite and Group to keep track of what
parts of the screen need a refresh and what don't
making your game render more efficiently.
❂
DirtySprites also allow you to keep track of different
layers for your sprites.
−
Layers are which sprites are allowed to be drawn on
others.
Attributes in DirtySprite
❂
❂
dirty = 1
−
Specifies whether or not a sprite should be repainted.
−
A value of 0 means the the sprite is "not dirty" and will not be repainted.
−
A value of 1 means the sprite is "dirty" and will be repainted then the
value will be set back to 0.
−
A value of 2 means the sprite is always "dirty" and will always be
repainted with each frame.
visible = 1
−
❂
if set to 0, then the sprite will never be repainted (you have to set the
dirty attribute as well in order for the sprite to be erased.)
_layer = 0
−
READONLY value
−
used with the LayeredDirty data structure to render sprites with
different layers so they overlap one another.
Advanced Collision Detection
Collision Detecting with a Circle
❂
As you know Pygame tests for collisions on the
bounding rectangle of a Sprite.
−
❂
Sprites must define a self.rect attribute.
You can also have a bounding circle to check for
collisions.
−
Sprites still need to define a self.rect attribute.
−
Sprites can optionally define a self.radius attribute.
This is the radius of the bounding circle.
✦
If this attribute is missing then the size of the circle is exactly
large enough to surround the pygame.Rect object defined in
self.rect (rectangle is inscribed in the Circle)
Circle Collision (Single Sprites)
❂
pygame.sprite.collide_circle()
−
collide_circle(sprite1, sprite2) -> bool
−
Tests for collision between two individual sprites. Uses
the bounding circle specified by self.radius or
self.rect (if self.radius is missing).
Circle Collision (using Groups)
❂
❂
pygame.sprite.spritecollide() can take an optional
fourth parameter which is a callback function.
−
This function must have two parameters and these two
parameters are sprites.
−
This function is used to calculated whether or not two sprites
have collided.
−
This function can be a custom collision function or one from
Pygame. (In this case for circles, the function will be
pygame.sprint.collide_circle() )
Example:
−
collision_list =
pygame.sprite.spritecollide(sprite1, group,
False, pygame.sprite.collide_circle)
Pixel Perfect Collision Using a Mask
❂
❂
The pygame.mask module is used for pixel perfect
collision. This module has the pygame.mask.Mask
class to represent a 2D bitmask for a Sprite.
pygame.mask.from_surface():
−
from_surface(Surface, threshold = 127) -> Mask
−
Returns a Mask from the given surface. Makes the
transparent parts of the Surface not set, and the opaque
parts set. The alpha of each pixel is checked to see if it is
greater than the given threshold. If the Surface uses the
set_colorkey(), then threshold is not used.
Pixel Perfect Collision Using a Mask
❂
pygame.sprite.collide_mask():
−
collide_mask(SpriteLeft, SpriteRight) -> point
−
Returns the first point on the mask where the masks
collided, or None if there was no collision. Tests for
collision by testing if the bitmasks overlap. If the sprites
have a "mask" attribute this is used. If there is no mask
attribute one is created from the sprite image.
−
You should consider creating a mask for your sprite at
load time if you are going to check collisions many times.
This will increase the performance, otherwise this can be
an expensive function because it will create the masks
each time you check for collisions.
Collision Detection and Image Transformations
❂
❂
❂
The pygame.transform module has functions which can transform an
image:
−
flip vertically or horizontially
−
resize the image
−
rotate
−
and others.
The module is pretty self explanatory, however the documentation does
not really mention that after you transform an image, you also need to
update its collision objects:
−
update the self.rect or self.mask based on the new image.
−
this can be very computationally expensive if you transform your items a lot.
Also note that it is always good to save a copy of the original image. If
you keep transforming subsequent resulting images, you may see a
decrease in the detail of your images.
Other Examples
Other Examples
❂
Shooting Example:
−
❂
Walls Example:
−
❂
Adds to the previous platformer example but adds a scrollable functionality as well as multiple
levels.
Platformer 3:
−
❂
Example of how to have a platformer type game. Highlights how to make a player "jump" (not as
simple as you think) and how to handle related collision detection issues.
Platformer 2:
−
❂
Demonstrates a more advanced wall example and also shows how to have multiple levels in a
game.
Platformer 1:
−
❂
Shows how to create walls and use collision detection so that they do not allow the player to
pass through.
Maze Runner:
−
❂
Shows how to create bullets and use collision detection to shoot at and "kill" things.
Adds to the previous but this time with movable platforms.
Sprite Sheet Platformer:
−
Advanced example which uses all the concepts from the previous platformers, but incorporates
sprite sheets and animating your character.