Creating and Moving our player
This is a post in the
Worm series.
Other posts in this series:
- May 02, 2023 - Basic Pygame game structure
- May 03, 2023 - Add grid lines and Kinematic objects
- May 21, 2023 - Creating and Moving our player
- May 22, 2023 - Worm eats food
- May 24, 2023 - Grid Movement
- May 30, 2023 - Screen wrap & Detect edges
Artwork
- first get some images for our worm and food
Head over to Kenney’s shape characters package and get the following images to follow along. The files we want are in the png/default folder. The larger images in that folder are 80x80. If you are adventurous try other images.
I’m going to use individual 2D PNG files for each item to start. Note I change all file names to lowercase as that works with Linux, Mac, and Windows.
Create an “assets” folder and then copy the default folder into that. Create a player folder directly in the assets folder and move or copy blue_body_circle, blue_body_squircle.png, and face_a.png from the default folder into that folder. Also copy tile_coin into a food folder. We can delete default later.
We didn't add assets to our gitignore till later so the code that you cloned should contain the necessary files.
You can manage the files and folders how you see fit, there is no ‘ONE’ way. Others coming from other ’engines’ or ’libraries’ may find this easy to understand.
Key Input
see the git commit “Add key input direction and movement”
In python and many other languages you can use classes to group things together. i.e. keyboard input or a certain game object like the player.
You can place the class inside the game.py file or you can create a separate file and then import that file to use its code. We will use the second as it reduces the code in each file.
Let’s move known/common things out the way as we start to make them.
Create a file called keyinput.py and place the following in it. You will notice the code is (almost) identical to the code in the previous post, see if you can do this ’extraction’ yourself, don’t worry about the extra code.
# the pygame imports to make them usable in our game
import pygame
from pygame.locals import *
class KeyInput():
def __init__(self) -> None:
# these variables are new
self.key_queue = []
self.last_key_pressed = "none"
self.last_key_released = "none"
def getEvents(self):
for event in pygame.event.get():
if event.type == QUIT or (event.type==pygame.KEYDOWN and event.key in [K_ESCAPE]):
self.key_queue.clear()
return False
return True
It uses the same imports as our “game.py” file
A class called “KeyInput” we construct using our code from the game loop
I added three variables. We will use two of them soon. This will keep track of keys pressed, and in what order, plus the last key pressed or released.
The third variable is not in the git repository. I have placed it here so you can use ‘released’ instead of ‘pressed’.
“game.py” file changes
- we import the new file. Here we place it at the end of the imports list.
import pygame
from pygame.locals import *
from keyinput import *
# use class
held_keys = KeyInput()
# GAME LOOP
while game_running:
# get key presses
# return false if QUIT is requested
# else return true
game_running = held_keys.getEvents()
pygame.display.update()
The rest of the code remains the same. The behavior should not have changed, except you can quit by using the ESCAPE key too.
Add player character
Player will be a moving game object unaffected by external forces, so will end up being a kinematic character. Food will be a stationary game object.
Let’s start with player and split off code when necessary to keep our code ’light weight’
Our player’s image is loaded and resized and placed in the center of the screen. To do this we change the code in the middle once again
here is a snippet, notice all the changes in this section
#define white
white = (255,255,255)
#clear the screen
screen.fill(white)
#Get the player image and a rectangle for size/position
player = pygame.image.load("assets/player/blue_body_squircle.png")
player_rect = player.get_rect()
player_rect.center = width/2, height/2
# GAME LOOP
while game_running:
game_running = held_keys.getEvents()
screen.fill(white)
screen.blit(player, player_rect)
pygame.display.update()
This code:
- clears the screen by painting it white every loop
- draws the player
- updates the display to show our player on the screen
Game Object The player code is using some code that could be common to all game objects.
RE-USING the players code.
This can found found in the git repository “GameObject class for Player”
we extract code that can be used for any game object and place it in a class near the top of game.py
class GameObject():
def __init__(self, img_name, initial_pos):
self.img = pygame.image.load(img_name)
self.rect = self.img.get_rect()
self.rect.center = initial_pos
def draw(self):
screen.blit(self.img, self.rect)
Remove the original code we used for the player. The player can be created with this code
#Get the player image and a rectangle for size/position
player = GameObject("assets/player/blue_body_squircle.png", (width/2, height/2))
And drawn and updated
# GAME LOOP
while game_running:
game_running = held_keys.getEvents()
screen.fill(white) # this has the effect of clearing the screen
# draw the player
player.draw()
pygame.display.update()
When you clean code like this and carefully choose the names of your functions then you end up with something you can just look at and know what it is doing.
Sometimes, we create a game object but don’t know where to place it. We can move that “initial_pos” code to its own method to allow for that. Currently it is in the Constructor (init).
Animation
I don’t often use animation sequences to move my character to the next square. Sometimes its very useful, but I’d like to use the timed based movement for animation.
Above the game loop we set up the pygame ‘clock’
clock = pygame.time.Clock()
FPS = 60
and at the top of the loop we check the time taken from one loop to the next
# clock tick will return the delta time
# clock tick with a value sets the frames per second MAX value
dt = clock.tick(FPS)
Two things are accomplished with the above code
- get time between frames
- set our MAX frame rate
Kinematic object and Input
The code below is ideal for eight directions, and it is fairly simple and straight forward.
for event in pygame.event.get():
if event.type == QUIT or (event.type==pygame.KEYDOWN and event.key in [K_ESCAPE]):
# collapse the app
game_running = False
dir = [0,0]
keys=pygame.key.get_pressed()
# top left is (0,0)
if keys[K_LEFT]:
dir[0] += -1
if keys[K_RIGHT]:
dir[0] += 1
if keys[K_UP]:
dir[1] += -1
if keys[K_DOWN]:
dir[1] += 1
if dir != [0,0]:
player_rect = player_rect.move([dir[0] * dt/4, dir[1] * dt/4])
Our needs however require 4 directions. Possibly the following
dir = [0,0]
keys=pygame.key.get_pressed()
# top left is (0,0)
if keys[K_LEFT] and not keys[K_UP] and not keys[K_DOWN]:
dir[0] += -1
if keys[K_RIGHT] and not keys[K_UP] and not keys[K_DOWN]:
dir[0] += 1
if keys[K_UP] and not keys[K_LEFT] and not keys[K_RIGHT]:
dir[1] += -1
if keys[K_DOWN] and not keys[K_LEFT] and not keys[K_RIGHT]:
dir[1] += 1
BUT…
I prefer to take the keys in the order they are pressed i.e. in a list. And then clear them out the list when released.
I also want to keep moving in the last direction “pressed” not “moved” so add an extra variable that simply records every time a key is pressed i.e. the last key pressed.
Here is my code - which should be placed in the “keyinput.py” file
def getEvents(self):
for event in pygame.event.get():
if event.type == QUIT or (event.type==pygame.KEYDOWN and event.key in [K_ESCAPE]):
self.key_queue.clear()
return False
if event.type==pygame.KEYDOWN:
if event.key in [K_DOWN]:
self.last_key_pressed = "D"
self.key_queue.append("D")
if event.key in [K_UP]:
self.last_key_pressed = "U"
self.key_queue.append("U")
if event.key in [K_RIGHT]:
self.last_key_pressed = "R"
self.key_queue.append("R")
if event.key in [K_LEFT]:
self.last_key_pressed = "L"
self.key_queue.append("L")
if event.type==pygame.KEYUP:
if event.key in [K_DOWN]:
self.key_queue.remove("D")
self.last_key_released = "D"
if event.key in [K_UP]:
self.key_queue.remove("U")
self.last_key_released = "U"
if event.key in [K_RIGHT]:
self.key_queue.remove("R")
self.last_key_released = "R"
if event.key in [K_LEFT]:
self.key_queue.remove("L")
self.last_key_released = "L"
return True
def get_direction(self):
if len(self.key_queue) > 0:
return self.get_direction_vector(self.key_queue[0])
else:
return self.get_direction_vector(self.last_key_pressed)
#return self.get_direction_vector(self.last_key_released)
def get_direction_vector(self, key_eval):
match (key_eval):
case "U":
return [0,-1]
case "D":
return [0,1]
case "L":
return [-1,0]
case "R":
return [1,0]
return [0,0]
Self simply means the class and it is used it to reference the class and its methods and variables.
Here is a snippet of how to use it
# I'm not showing the code above...
player = GameObject("assets/player/blue_body_squircle.png", (width/2, height/2))
speed = 0.4
# GAME LOOP
while game_running:
# clock tick with a value will return the delta time
# as well as prevent clock speed being higher than FPS
dt = clock.tick(FPS)
game_running = held_keys.getEvents()
dir = held_keys.get_direction()
velocity = (dir[0] * dt * speed, dir[1] * dt * speed)
player.rect = player.rect.move(velocity)
# I'm not showing the code below...
If you run your ‘play test’ now, you can see how it works.
if you forget to clear the screen your run test will look something like this
We still have to scale down our player image and only allow a change in direction at the tile center. We should move the new player code to the player object.
NOTE Setting max frame rates has some benefits. I suggest you read up on it as I can’t give you a good argument here except that it is kind to the device.
Previous Next