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.