Using multiprocessing with pygame? - python

I'm trying to separate my input loop from my game logic in my simple snake game that I've made with pygame, but, I'm really struggling to figure out why nothing is happening when I run the program.
I've tried importing pygame in the subprocess, I checked for errors on the subprocess, and got nowhere. I looked on google, but I wasn't able to find any usable examples, or similar issues. Has anybody ever figured any of this stuff out?
Okay, here's the code:
import pygame
import time
import multiprocessing as mp
import random as rnd
pygame.init()
def event_to_dict(event: pygame.event) -> dict:
return {
'type': event.type,
'key': event.key if event.type == pygame.KEYDOWN else None,
}
class SnakeBoard:
def __init__(self, rows: int, columns: int):
self.rows = rows
self.columns = columns
self.vertices = []
self.odd_column = False
self.buff = []
for _ in range(self.rows):
self.buff.append([' ' for _ in range(self.columns)])
def initialize(self):
for r in range(self.rows):
for c in range(self.columns):
self.buff[r][c] = ' '
self.odd_column = (self.columns >> 1) % 2 == 1
self.buff[self.rows >> 1][self.columns >> 1] = '\u25cb'
self.vertices = [(self.rows >> 1, self.columns >> 1)]
def place_food(self):
while True:
r = rnd.randint(0, self.rows - 1)
c = rnd.randint(0, self.columns - 1)
codd = c % 2 == 1
if (codd and self.odd_column or not codd and not self.odd_column) and self.buff[r][c] != '\u25cb':
self.buff[r][c] = '\u25c9'
break
def tick(self, direction: int) -> bool:
nr, nc = self.vertices[-1]
if direction == 0:
nr -= 1
elif direction == 1:
nc += 1
elif direction == 2:
nr += 1
elif direction == 3:
nc -= 1
else:
print("Invalid direction for snake")
exit(1)
if nr >= self.rows or nc >= self.columns or nr < 0 or nc < 0 or self.buff[nr][nc] == '\u25cb':
return False
self.vertices.append((nr, nc))
self.vertices.pop(0)
return True
class SnakeGame(SnakeBoard):
def __init__(self, rows: int, columns: int):
super().__init__(rows, columns)
self.score = 0
self.direction = 0
self.initialize()
self.place_food()
def tick(self, direction: int = -1) -> bool:
v = super().tick(self.direction if direction < 0 else direction)
if self.buff[self.vertices[-1][0]][self.vertices[-1][1]] == '\u25c9':
self.score += 1
self.vertices.append(self.vertices[-1])
self.place_food()
for r in range(self.rows):
for c in range(self.columns):
if (r, c) in self.vertices:
self.buff[r][c] = '\u25cb'
elif self.buff[r][c] != '\u25c9' and self.buff[r][c] != ' ':
self.buff[r][c] = ' '
return v
class GameLoop(mp.Process):
def __init__(self, q: object, size: list):
super().__init__()
self.q = q
self.size = size
self.g = SnakeGame(size[1] // 10, size[0] // 10)
self.g.initialize()
self.g.place_food()
self.screen = None
self.game_surf = None
self.font = None
def run(self) -> None:
try:
import pygame
pygame.init()
self.screen = pygame.display.set_mode(self.size)
self.game_surf = pygame.Surface(self.size)
self.font = pygame.font.SysFont('roboto', 16)
is_running = True
while is_running:
if self.q.poll(0):
d = self.q.recv()
if d is not None:
if d['type'] == pygame.KEYDOWN:
if d['key'] == pygame.K_a:
self.g.direction = 3
elif d['key'] == pygame.K_s:
self.g.direction = 2
elif d['key'] == pygame.K_d:
self.g.direction = 1
elif d['key'] == pygame.K_w:
self.g.direction = 0
elif d['key'] == pygame.K_ESCAPE:
is_running = False
else:
is_running = False
self.game_surf.fill((255, 255, 255))
for ri, r in enumerate(self.g.buff):
for ci, c in enumerate(r):
if c == '\u25cb':
# print("Drawing a snake at {}, {}".format(ri * 10, ci * 10))
pygame.draw.circle(self.game_surf,
(0, 0, 255),
((ci * 10) + 5, (ri * 10) + 5),
5)
elif c == '\u25c9':
# wprint("Placing food at {}, {}".format(ci, ri))
pygame.draw.circle(self.game_surf,
(0, 127, 255),
((ci * 10) + 5, (ri * 10) + 5),
5)
timg = self.font.render("Score: {}, Level: {}".format(self.g.score, self.g.score // 10 + 1),
True,
(0, 0, 0))
self.screen.blit(self.game_surf, (0, 0))
self.screen.blit(timg, (0, 0))
pygame.display.flip()
if self.g.tick():
time.sleep(1 / ((int(self.g.score / 10 + 1)) * 10))
else:
timg = self.font.render("Game Over! Would you like to try again?", True, (0, 0, 0))
self.screen.blit(timg, ((self.size[0] >> 1) - 150, self.size[1] >> 1))
timg = self.font.render("Yes", True, (0, 0, 0))
btn_pos = ((self.size[0] >> 1) - 25, (self.size[1] >> 1) + 20)
self.screen.blit(timg, btn_pos)
pygame.display.flip()
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT:
is_running = False
break
elif event.type == pygame.MOUSEBUTTONUP:
mx, my = pygame.mouse.get_pos()
if btn_pos[0] - 5 <= mx <= btn_pos[0] + 30 and btn_pos[1] - 5 <= my <= btn_pos[1] + 20:
self.g.initialize()
self.g.place_food()
self.g.score = 0
break
self.q.close()
except Exception as e:
print(e)
if __name__ == '__main__':
size = [800, 600]
parent, child = mp.Pipe()
p = GameLoop(child, size)
p.start()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
ed = event_to_dict(event)
parent.send(ed)
parent.close()
p.join()
pygame.quit()
Sorry, it's kinda strange, this was migrated from the console to pygame, so some of the logic is still using the unicode symbols.

Generally in GUI applications it's common to want to separate the GUI from the logic.
There are benefits to doing this as it means your GUI remains responsive even if your logic
is busy. However, in order to run things concurrently there are many drawbacks, including
overheads. It's also important to know that python is not 'thread safe', so you can break
things (see race conditions) if you're not careful.
Simplified example with no concurrency
Your example is quite complex so lets start with a simple example: A simple pygame setup with
a moving dot
import pygame
import numpy as np
# Initialise parameters
#######################
size = np.array([800, 600])
position = size / 2
direction = np.array([0, 1]) # [x, y] vector
speed = 2
running = True
pygame.init()
window = pygame.display.set_mode(size)
pygame.display.update()
# Game loop
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
direction = np.array([0, -1])
elif event.key == pygame.K_a:
direction = np.array([-1, 0])
elif event.key == pygame.K_s:
direction = np.array([0, 1])
elif event.key == pygame.K_d:
direction = np.array([1, 0])
position += direction * speed
if position[0] < 0 or position[0] > size[0] or position[1] < 0 or position[1] > size[1]:
running = False
pygame.time.wait(10) # Limit the speed of the loop
window.fill((0, 0, 0))
pygame.draw.circle(window, (0, 0, 255), position, 10)
pygame.display.update()
pygame.quit()
quit()
We're going to split off the game logic from the gui
Mutliprocessing and other options:
So multiprocessing in python allows you to utilise multiple cores at the same time, through multiple interpreters.
While this sounds good, as far as I/O goes: it comes with higher overheads and doesn't help at all (it will likely
hurt your performance). Threading and asyncio both run on a single core i.e. they aren't 'parrallel' computing. But
what they allow is to complete code while waiting for other code to finish. In other words you can input commands
while your logic is running happily elsewhere.
TLDR: as a general rule:
CPU Bound (100% of the core) program: use multiprocessing,
I/O bound program: use threading or asyncio
Threaded version
import pygame
import numpy as np
import threading
import time
class Logic:
# This will run in another thread
def __init__(self, size, speed=2):
# Private fields -> Only to be edited locally
self._size = size
self._direction = np.array([0, 1]) # [x, y] vector, underscored because we want this to be private
self._speed = speed
# Threaded fields -> Those accessible from other threads
self.position = np.array(size) / 2
self.input_list = [] # A list of commands to queue up for execution
# A lock ensures that nothing else can edit the variable while we're changing it
self.lock = threading.Lock()
def _loop(self):
time.sleep(0.5) # Wait a bit to let things load
# We're just going to kill this thread with the main one so it's fine to just loop forever
while True:
# Check for commands
time.sleep(0.01) # Limit the logic loop running to every 10ms
if len(self.input_list) > 0:
with self.lock: # The lock is released when we're done
# If there is a command we pop it off the list
key = self.input_list.pop(0).key
if key == pygame.K_w:
self._direction = np.array([0, -1])
elif key == pygame.K_a:
self._direction = np.array([-1, 0])
elif key == pygame.K_s:
self._direction = np.array([0, 1])
elif key == pygame.K_d:
self._direction = np.array([1, 0])
with self.lock: # Again we call the lock because we're editing
self.position += self._direction * self._speed
if self.position[0] < 0 \
or self.position[0] > self._size[0] \
or self.position[1] < 0 \
or self.position[1] > self._size[1]:
break # Stop updating
def start_loop(self):
# We spawn a new thread using our _loop method, the loop has no additional arguments,
# We call daemon=True so that the thread dies when main dies
threading.Thread(target=self._loop,
args=(),
daemon=True).start()
class Game:
# This will run in the main thread and read data from the Logic
def __init__(self, size, speed=2):
self.size = size
pygame.init()
self.window = pygame.display.set_mode(size)
self.logic = Logic(np.array(size), speed)
self.running = True
def start(self):
pygame.display.update()
self.logic.start_loop()
# any calls made to the other thread should be read only
while self.running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
# Here we call the lock because we're updating the input list
with self.logic.lock:
self.logic.input_list.append(event)
# Another lock call to access the position
with self.logic.lock:
self.window.fill((0, 0, 0))
pygame.draw.circle(self.window, (0, 0, 255), self.logic.position, 10)
pygame.display.update()
pygame.time.wait(10)
pygame.quit()
quit()
if __name__ == '__main__':
game = Game([800, 600])
game.start()
So what was achieved?
Something light like this doesn't really need any performance upgrades. What this does allow though, is that
the pygame GUI will remain reactive, even if the logic behind it hangs. To see this in action we can put the logic
loop to sleep and see that we can still move the GUI around, click stuff, input commands etc.
change:
# Change this under _loop(self) [line 21]
time.sleep(0.01)
# to this
time.sleep(2)
# if we tried this in the original loop the program becomes glitchy

Related

Optimizing Negamax Function with 5x5 Hexapawn

I need to improve the speed of this program, because at the moment it is pretty slow. I know that representing game states in binary can be very effective, however, I don't know how to do that. I have also tried using numba, however that seems to make it slower. I have attached the code below. Thank you to anyone who can help!
import pygame, sys, time, hashlib
from copy import deepcopy
pygame.init()
red = pygame.Color(255,0,0)
white = pygame.Color(255,255,255)
black = pygame.Color(0,0,0)
pygame.display.set_caption('Hexapawn AI')
width, height = 700,700
game_window = pygame.display.set_mode((width, height))
def set_pawns():
global game_window, board
for y in range(5):
for x in range(5):
if board[y][x] == 1:
game_window.blit( blue_pawn, ( (width/5)*x, (height/5)*(4-y) ))
if board[y][x] == -1:
game_window.blit( red_pawn, ( (width/5)*x , (height/5)*(4-y) ))
def build_lines():
global game_window
for x in range(1,5):
pygame.draw.line(game_window, black, (width/5 * x, 0), (width/5 * x, height), 7)
pygame.draw.line(game_window, black, (0, height/5 * x), (width, height/5 * x), 7)
def get_possible_moves(board, player):
possible_moves = []
forward = 1 if player == 1 else -1
opponent = -1 if player == 1 else 1
for y in range(5):
for x in range(5):
if board[y][x] != player:
continue
if x-1 >= 0 and y+forward < 5 and board[y+forward][x-1] == opponent:
possible_moves.append([x,y,x-1,y+forward])
if x+1 < 5 and y+forward < 5 and board[y+forward][x+1] == opponent:
possible_moves.append([x,y,x+1,y+forward])
if (y+1 < 5 and player == 1) or (y+1 > -1 and player == -1):
if board[y+forward][x] == " ":
possible_moves.append([x,y,x,y+forward])
return possible_moves
def make_move(board,move,player):
global game_window, width, height
game_window.fill(white)
build_lines()
board[move[1]][move[0]] = " "
board[move[3]][move[2]] = player
set_pawns()
def neg_make_move(board, move, player):
x1, y1, x2, y2 = move
board = deepcopy(board)
board[y1][x1] = " "
board[y2][x2] = player
return board
def check_for_win(board,player):
if player == -1:
if -1 in board[0]:
return True
if get_possible_moves(board,1) == []:
return True
elif player == 1:
if 1 in board[4]:
return True
if get_possible_moves(board,-1) == []:
return True
return False
TRANSPOSITION_TABLE = {}
def state_hash(board):
serialized = str(board).encode()
return hashlib.sha256(serialized).hexdigest()
def store(table, board, alpha, beta, best, depth):
state = state_hash(board)
if best[1] <= alpha:
flag = 'UPPERCASE'
elif best[1] >= beta:
flag = 'LOWERCASE'
else:
flag = 'EXACT'
table[state] = [best, flag, depth]
def negamax(board, depth, turn, alpha, beta):
alpha_org = alpha
state = state_hash(board)
if state in TRANSPOSITION_TABLE:
tt_entry = TRANSPOSITION_TABLE[state]
if tt_entry[2] >= depth:
if tt_entry[1] == 'EXACT':
return tt_entry[0]
elif tt_entry[1] == 'LOWERCASE':
alpha = max(alpha, tt_entry[0][1])
elif tt_entry[1] == 'UPPERCASE':
beta = min(beta, tt_entry[0][1])
if alpha >= beta:
return tt_entry[0]
if check_for_win(board, -turn):
return None, -(25+depth)
if depth == 0:
return get_possible_moves(board,turn)[0], (depth)
best_score = -200
for move in get_possible_moves(board,turn):
new_board = neg_make_move(board, move, turn)
score = -negamax(new_board, depth - 1, -turn, -beta, -alpha)[1]
alpha = max(alpha,score)
if score > best_score:
best_score, best_move = score, move
if alpha >= beta:
break
store(TRANSPOSITION_TABLE, board, alpha_org, beta, [best_move,best_score], depth)
return best_move, best_score
# Build board
board = [[1 for x in range(5)]]
for x in range(3):
board.append([" " for x in range(5)])
board.append([-1 for x in range(5)])
game_window.fill(white)
# Draw game board lines
build_lines()
# Load sprites with correct sizes
tile_size = (width/5,height/5)
blue_pawn = pygame.transform.scale(pygame.image.load("blue_pawn.png"), tile_size)
red_pawn = pygame.transform.scale(pygame.image.load("red_pawn.png"), tile_size)
# Draw the pawns to the board
set_pawns()
pygame.display.update()
while True:
for event in pygame.event.get():
# if user clicks the X or they type esc then the screen will close
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
start = time.time()
move = negamax(board,12,1,-10000,10000)[0]
print(f"Blue move took {time.time()-start} seconds to calculate.")
make_move(board,move,1)
pygame.display.update()
if check_for_win(board,1):
print("Blue Wins!")
pygame.quit()
sys.exit()
time.sleep(1)
start = time.time()
move = negamax(board,12,-1,-10000,10000)[0]
print(f"Red move took {time.time()-start} seconds to calculate.")
make_move(board,move,-1)
pygame.display.update()
if check_for_win(board,-1):
print("Red Wins!")
pygame.quit()
sys.exit()
pygame.display.update()
time.sleep(1)

Doing long calculations while looking for pygame events [duplicate]

The couple other questions related to this were solved by moving a draw event out of a loop it shouldn't be in. I don't have this issue though. Any help would be greatly appreciated!
Python: 3.8
Pygame: 1.9.6
If you need to test play:
Run the game. Draw on the screen to place live cells. Click 'R' to start. You can also click 'S' after starting to stop and draw again, but you'll have to wait a few generations after clicking before it actually stops (due to the same lag I assume).
import pygame
import numpy
class Game():
def __init__(self):
self.Run()
def GetAdj(self, x, y):
nb = 0
for c in range (-1, 2):
for r in range (-1, 2):
if r == 0 and c == 0:
pass
else:
nposx = x + r
nposy = y + c
if nposx < len(self.pixels):
if nposy < len(self.pixels[nposx]):
if self.pixels[nposx][nposy] == 1:
nb += 1
return nb
def NextGeneration(self):
newGeneration = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
for x, c in enumerate(self.pixels):
for y, cell in enumerate(c):
nbrs = self.GetAdj(x, y)
if cell == 1:
if nbrs in [2, 3]:
newGeneration[x][y] = 1
else:
if nbrs == 3:
newGeneration[x][y] = 1
self.pixels = newGeneration
def DrawBG(self):
black = (0,0,0)
white = (255,255,255)
self.bg.fill(black)
for c in range(self.ScreenWidth // self.cellsize):
for r in range(self.ScreenHeight // self.cellsize):
if self.pixels[c][r] == 1:
pygame.draw.rect(self.bg, white, (c*self.cellsize, r*self.cellsize, self.cellsize, self.cellsize))
def Run(self):
pygame.init()
self.ScreenHeight = 720
self.ScreenWidth = 1280
self.ScreenSize = (self.ScreenWidth, self.ScreenHeight)
screen = pygame.display.set_mode(self.ScreenSize)
self.bg = pygame.Surface(self.ScreenSize)
clock = pygame.time.Clock()
mousedown = False
self.pixels = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
self.cellsize = 10
stage = 'Draw'
running = True
while running:
clock.tick(60)
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
if stage == 'Draw':
for event in events:
if event.type == pygame.MOUSEBUTTONUP and mousedown:
mousedown = False
elif event.type == pygame.MOUSEBUTTONDOWN:
mousedown = True
elif event.type == pygame.MOUSEMOTION and mousedown and stage == 'Draw':
mposx, mposy = pygame.mouse.get_pos()
self.pixels[mposx//self.cellsize][mposy//self.cellsize] = 1
elif event.type == pygame.KEYDOWN and event.key == pygame.K_r:
stage = 'Run'
self.NextGeneration()
elif stage == 'Run':
for event in events:
if event.type == pygame.KEYDOWN and event.key == pygame.K_s:
stage = 'Draw'
self.NextGeneration()
self.DrawBG()
screen.blit(self.bg, (0,0))
pygame.display.flip()
if __name__ == "__main__":
Game()
The numpy array is way too big. You create an array for every pixel, not every cell. So don't calculate it for every cell, but for every pixel.
Change the size of the array in the NextGeneration and the Run method:
newGeneration = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
newGeneration = numpy.zeros((self.ScreenWidth//self.cellsize, self.ScreenHeight//self.cellsize), dtype=int)
self.pixels = numpy.zeros(self.ScreenWidth//2, self.ScreenHeight//2, dtype=int)
self.cellsize = 10
self.pixels = numpy.zeros((self.ScreenWidth//self.cellsize, self.ScreenHeight//self.cellsize), dtype=int)
The GetAdj method can be greatly simplified with numpy.sum:
class Game():
# [...]
def GetAdj(self, x, y):
x0, y0 = max(0, x-1), max(0, y-1)
nb = numpy.sum(self.pixels[x0 : x+2, y0 : y+2]) - self.pixels[x, y]
return nb
The performance can be improved further using scipy.ndimage.convolve and a lookup table (see Indexing Multi-dimensional arrays):
import numpy
from scipy.ndimage import convolve
class Game():
def __init__(self):
self.kernel = numpy.array([[1,1,1], [1,0,1], [1,1,1]])
self.lookup = numpy.array([0,0,0,1,0,0,0,0,0, 0,0,1,1,0,0,0,0,0])
self.Run()
def NextGeneration(self):
adjacent = convolve(self.pixels, self.kernel, mode='constant')
newGeneration = self.lookup[self.pixels * 9 + adjacent]
self.pixels = newGeneration
In addition to #Rabbid76's answer, you cal also implement the game with a sparse structure.
As we can see, having huge Numpy arrays is the bottleneck of your implementation. If you are just storing the information about cells that are alive, you can probably use a SciPy sparse boolean matrix, which essentially just initialises the information about cells that are alive. So even if you have a 1000x1000 grid with one cell alive, it will not actually store all those dead cells.
But even better, why not just use a Python set? For example, you could have a set of int tuples that represent the cells currently alive in each iteration. I found that this makes the implementation and logic a lot simpler, and the overheads of importing and creating huge numpy arrays kind of disappear.
I recently posted a Conway's Game Of Life implementation in fewer than 80 lines of Python, using Pygame. I use sets to store the live cells, so take a look and see how your implementation can be simplified with this approach.
Of course, if you are planning on having a full live cell board all the time, then this approach has its drawbacks. But I assume that, since you're using Python in the first place, you don't intend on having many computationally intensive setups (I'd recommend using native code for that, with heavy optimisations).

Python_textinput issue while using two instances of it

I'm working on a choice based adventure game with python, and I'm using pygame to create some graphics for the game.
As I was trying to create a screen so the player can input his real name and the main character name I faced an issue: as I try to create both text inputs (using pygame_textinput module) on the same screen, it just clones what I write in one of them.
I thought I could solve this by putting the other input on a new screen but when I hit enter on the first screen it just passes through the rest of the code and the second input stays empty.
How could I solve this issue?
#Imports
import contextlib
with contextlib.redirect_stdout(None):
import pygame
import pickle
import time
import random
import pygame_textinput.pygame_textinput as textinput
#Really messy, I know#
#Save Stuff
Choices = {}
def save(to_save, save_filepath):
pickle.dump(to_save, open(save_filepath, "wb"))
def load(save_filepath):
return pickle.load(open(save_filepath, "rb"))
#Initializations
pygame.init()
#Screen
scrWidth = 640
scrHeight = 480
screen = pygame.display.set_mode((scrWidth, scrHeight))
pygame.display.set_caption('Something')
#Images
startscreenbg = pygame.image.load('assets/Images/startscreen.jpg').convert()
#Text Input
real_nametxtinput = textinput.TextInput(text_color=(255,255,255))
char_nametxtinput = textinput.TextInput(text_color=(255,255,255))
#Clock
clock = pygame.time.Clock()
#Game Constants
next = False
real_name = ''
char_name = ''
real_name_done = False
char_name_done = False
##### Global Functions #####
#Buttons stuff
buttonTextFont = pygame.font.SysFont("comicsansms", 20)
def createButton(msg, msgcolor,x,y,width,height,color1,color2,action=None):
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
if x+width > mouse[0] > x and y+height > mouse[1] > y:
pygame.draw.rect(screen, color2,(x,y,width,height))
if click[0] == 1 and action != None:
action()
else:
pygame.draw.rect(screen, color1,(x,y,width,height))
buttontext = buttonTextFont.render(msg, 1, msgcolor)
screen.blit(buttontext, (x + (width//2) - (buttontext.get_width()//2), (y + (height//2) - (buttontext.get_height()//2))))
##### Screens/Chapters #####
def update_next1():
global next
next = True
def start_screen():
screen.blit(startscreenbg, (0, 0))
new_game = createButton('New Game',(255,255,255), 80, 100, 200, 50, (0,180,0), (0,255,0), update_next1)
load_game = createButton('Load Game', (255,255,255), 360, 100, 200, 50, (0,0,180), (0,0,255))
#Names
def real_name_screen():
global real_name, real_name_done
screen.blit(startscreenbg, (0,0))
pygame.draw.rect(screen, (102, 255, 102), (150, 90, 200, 50))
screen.blit(real_nametxtinput.get_surface(), (150,100))
if real_nametxtinput.update(events):
real_name = real_nametxtinput.get_text()
real_name_done = True
def char_name_screen():
global char_name, char_name_done
screen.blit(startscreenbg, (0, 0))
pygame.draw.rect(screen, (255, 102, 255), (150, 90, 200, 50))
screen.blit(char_nametxtinput.get_surface(), (150, 100))
if char_nametxtinput.update(events):
char_name = char_nametxtinput.get_text()
if char_name != '':
char_name_done = True
run = True
while run:
clock.tick(27)
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
run = False
if not next:
start_screen()
if next:
real_name_screen()
if real_name_done:
char_name_screen()
if char_name_done:
#This is just so I could test it
print(real_name, char_name)
run = False
pygame.display.update()
pygame.quit()
quit()
You need a way to keep track if one of the text input widgets is active, and a way to switch between all widgets.
I hacked together this example, adjust as needed (there's a lot that could be improved):
import os.path
import pygame
import pygame.locals as pl
pygame.font.init()
class WidgetManager:
def __init__(self):
self.widgets = []
def update(self, events, dt):
for e in events:
if e.type == pygame.MOUSEBUTTONDOWN:
for w in self.widgets:
w.active = False
if w.rect.collidepoint(e.pos):
w.active = True
for w in self.widgets:
w.update(events, dt)
def draw(self, surface):
for w in self.widgets:
surface.blit(w.surface, w.rect)
class TextInput:
"""
This class lets the user input a piece of text, e.g. a name or a message.
This class let's the user input a short, one-lines piece of text at a blinking cursor
that can be moved using the arrow-keys. Delete, home and end work as well.
"""
def __init__(
self,
initial_string="",
font_family="",
font_size=35,
antialias=True,
active=False,
text_color=(0, 0, 0),
rect=pygame.Rect(0, 0, 10, 10),
cursor_color=(0, 0, 1),
repeat_keys_initial_ms=400,
repeat_keys_interval_ms=35):
"""
:param initial_string: Initial text to be displayed
:param font_family: name or list of names for font (see pygame.font.match_font for precise format)
:param font_size: Size of font in pixels
:param antialias: Determines if antialias is applied to font (uses more processing power)
:param text_color: Color of text (duh)
:param cursor_color: Color of cursor
:param repeat_keys_initial_ms: Time in ms before keys are repeated when held
:param repeat_keys_interval_ms: Interval between key press repetition when helpd
"""
# Text related vars:
self.antialias = antialias
self.text_color = text_color
self.font_size = font_size
self.input_string = initial_string # Inputted text
self.active = active
self.rect = rect
if not os.path.isfile(font_family):
font_family = pygame.font.match_font(font_family)
self.font_object = pygame.font.Font(font_family, font_size)
# Text-surface will be created during the first update call:
self.surface = pygame.Surface((1, 1))
self.surface.set_alpha(0)
# Vars to make keydowns repeat after user pressed a key for some time:
self.keyrepeat_counters = {} # {event.key: (counter_int, event.unicode)} (look for "***")
self.keyrepeat_intial_interval_ms = repeat_keys_initial_ms
self.keyrepeat_interval_ms = repeat_keys_interval_ms
# Things cursor:
self.cursor_surface = pygame.Surface((int(self.font_size/20+1), self.font_size))
self.cursor_surface.fill(cursor_color)
self.cursor_position = len(initial_string) # Inside text
self.cursor_visible = True # Switches every self.cursor_switch_ms ms
self.cursor_switch_ms = 500 # /|\
self.cursor_ms_counter = 0
def update(self, events, dt):
for event in events:
if event.type == pygame.KEYDOWN and self.active:
self.cursor_visible = True # So the user sees where he writes
# If none exist, create counter for that key:
if event.key not in self.keyrepeat_counters:
self.keyrepeat_counters[event.key] = [0, event.unicode]
if event.key == pl.K_BACKSPACE:
self.input_string = (
self.input_string[:max(self.cursor_position - 1, 0)]
+ self.input_string[self.cursor_position:]
)
# Subtract one from cursor_pos, but do not go below zero:
self.cursor_position = max(self.cursor_position - 1, 0)
elif event.key == pl.K_DELETE:
self.input_string = (
self.input_string[:self.cursor_position]
+ self.input_string[self.cursor_position + 1:]
)
elif event.key == pl.K_RETURN:
return True
elif event.key == pl.K_RIGHT:
# Add one to cursor_pos, but do not exceed len(input_string)
self.cursor_position = min(self.cursor_position + 1, len(self.input_string))
elif event.key == pl.K_LEFT:
# Subtract one from cursor_pos, but do not go below zero:
self.cursor_position = max(self.cursor_position - 1, 0)
elif event.key == pl.K_END:
self.cursor_position = len(self.input_string)
elif event.key == pl.K_HOME:
self.cursor_position = 0
else:
# If no special key is pressed, add unicode of key to input_string
self.input_string = (
self.input_string[:self.cursor_position]
+ event.unicode
+ self.input_string[self.cursor_position:]
)
self.cursor_position += len(event.unicode) # Some are empty, e.g. K_UP
elif event.type == pl.KEYUP:
# *** Because KEYUP doesn't include event.unicode, this dict is stored in such a weird way
if event.key in self.keyrepeat_counters:
del self.keyrepeat_counters[event.key]
# Update key counters:
for key in self.keyrepeat_counters:
self.keyrepeat_counters[key][0] += dt # Update clock
# Generate new key events if enough time has passed:
if self.keyrepeat_counters[key][0] >= self.keyrepeat_intial_interval_ms:
self.keyrepeat_counters[key][0] = (
self.keyrepeat_intial_interval_ms
- self.keyrepeat_interval_ms
)
event_key, event_unicode = key, self.keyrepeat_counters[key][1]
pygame.event.post(pygame.event.Event(pl.KEYDOWN, key=event_key, unicode=event_unicode))
# Re-render text surface:
self.surface = pygame.Surface(self.rect.size)
self.surface.blit(self.font_object.render(self.input_string, self.antialias, self.text_color), (0, 0))
pygame.draw.rect(self.surface, self.text_color, (0, 0, *self.rect.size), 1)
# Update self.cursor_visible
self.cursor_ms_counter += dt
if self.cursor_ms_counter >= self.cursor_switch_ms:
self.cursor_ms_counter %= self.cursor_switch_ms
self.cursor_visible = not self.cursor_visible
if self.cursor_visible and self.active:
cursor_y_pos = self.font_object.size(self.input_string[:self.cursor_position])[0]
# Without this, the cursor is invisible when self.cursor_position > 0:
if self.cursor_position > 0:
cursor_y_pos -= self.cursor_surface.get_width()
self.surface.blit(self.cursor_surface, (cursor_y_pos, 0))
return False
def get_surface(self):
return self.surface
def get_text(self):
return self.input_string
def get_cursor_position(self):
return self.cursor_position
def set_text_color(self, color):
self.text_color = color
def set_cursor_color(self, color):
self.cursor_surface.fill(color)
def clear_text(self):
self.input_string = ""
self.cursor_position = 0
def main():
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
manager = WidgetManager()
manager.widgets.append(TextInput(text_color=pygame.Color('grey'), cursor_color=pygame.Color('grey'), rect=pygame.Rect(5, 5, 790, 35)))
manager.widgets.append(TextInput(text_color=pygame.Color('orange'), cursor_color=pygame.Color('orange'), rect=pygame.Rect(5, 55, 790, 35), active=True))
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
screen.fill((30, 30, 30))
manager.draw(screen)
manager.update(events, dt)
dt = clock.tick()
pygame.display.update()
if __name__ == '__main__':
main()
You see TextInput now has an active flag. If it's not set, the key input is ignored. Also, we store the position and size of TextInput in the new attribute rect, so the new class WidgetManager can switch between the widgets by clicking one with the mouse.
I also removed the Clock from the TextInput class, since calling self.clock.tick() is something the main loop should do.

Conway's game of life list index error

So I'm trying to make Conway's game of life in Python/pygame, and the first iteration of making the new grid works, but the second wont because of a list index out of range error. I have been trying to figure out what's wrong, but the list index shouldn't be out of range. This is my code, the mistake is supposedly in changevalue() but i suspect it isn't, since the first iteration works:
import pygame
import random
width = 400
height = 400
blocksize = 10
white = (255, 255, 255)
black = (0, 0, 0)
visual = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()
IsOn = True
grid = []
templist = []
tempgrid = []
class square(object):
def __init__(self, x, y, alive):
self.x = x
self.y = y
self.alive = alive
for y in range(height/blocksize):
templist = []
for x in range(width/blocksize):
templist.append(square(x, y, random.choice([True, False, False, False])))
grid.append(templist)
def changevalue(cx, cy, cgrid):
neighbours = []
for dy in range(3):
ddy = dy - 1
for dx in range(3):
ddx = dx - 1
if not (dx - 1 == 0 and dy - 1 == 0):
#print cgrid[(cy + ddy)%len(cgrid)][(cx + ddx)%len(cgrid[y])].alive
#NO ERRORS
#print len(cgrid) > (cy + ddy)%len(cgrid), len(cgrid[y]) > (cx + ddx)%len(cgrid[cy])
#NO ERRORS
neighbours.append(cgrid[(cy + ddy)%len(cgrid)][(cx + ddx)%len(cgrid[cy])].alive)
return len(filter(lambda p: p == True, neighbours))
while IsOn:
for event in pygame.event.get():
if event.type == pygame.QUIT:
IsOn = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_c:
proceed = True
tempgrid = []
for times in range(len(grid)):
tempgrid.append([])
for ty in range(len(grid)):
for tx in range(len(grid[ty])):
if changevalue(tx, ty, grid) < 2 and grid[ty][tx].alive == True:
tempgrid[ty].append(square(tx, ty, False))
elif changevalue(tx, ty, grid) > 3 and grid[ty][tx].alive == True:
tempgrid[ty].append(square(tx, ty, False))
elif changevalue(tx, ty, grid) == 3 and grid[ty][tx].alive == False:
tempgrid[ty].append(square(tx, ty, True))
grid = list(tempgrid)
visual.fill(white)
for y in range(len(grid)):
for x in range(len(grid[y])):
if grid[y][x].alive == True:
pygame.draw.rect(visual, black, (grid[y][x].x*blocksize, grid[y][x].y*blocksize, blocksize, blocksize))
pygame.display.update()
clock.tick(2)
pygame.quit()
quit()
Thanks for your help!
You don't copy square which doesn't change value - so new rows have different length - and later you have problem with index
You need something like this
if changevalue ...:
...
elif changevalue ...:
...
elif changevalue ...:
...
else:
# copy other elements
tempgrid[ty].append(grid[ty][tx])

pygame: Stopping movement behind line

I am attempting to create a game that lets a block move "jump" and land on a platform above it. Then, jump again to the next platform.
Unfortunately, my code currently just stops when the block touches the bottom of the platform and moves no further. I am unsure why as i believe it should only stop when the bottom of the block hits the line
Specifically am looking at this bit of code, but full code below for context:
#the floor landing code
def hasJumperLanded(rect1, rect2):
for a, b in [(rect1, rect2), (rect2, rect1)]:
if isFloorTouching(a.bottom, b):
return True
def isFloorTouching(y, rect):
if (y > rect.top) and (y < rect.bottom):
return True
else:
return False
snip
#stop when land on floor
for n in range(len(floors)):
if (hasJumperLanded(j['rect'], floors[n]['line'])):
j['jump'] = STILL
Full code context:
import pygame, sys, time
from pygame.locals import *
#the deadzone collision code
def doRectsOverlap(rect1, rect2):
for a, b in [(rect1, rect2), (rect2, rect1)]:
if ((isPointInsideRect(a.left, a.top, b)) or
(isPointInsideRect(a.left, a.bottom, b)) or
(isPointInsideRect(a.right, a.top, b)) or
(isPointInsideRect(a.right, a.bottom, b))):
return True
return False
def isPointInsideRect(x, y, rect):
if (x > rect.left) and (x < rect.right) and (y > rect.top) and (y < rect.bottom):
return True
else:
return False
#the floor landing code
def hasJumperLanded(rect1, rect2):
for a, b in [(rect1, rect2), (rect2, rect1)]:
if isFloorTouching(a.bottom, b):
return True
def isFloorTouching(y, rect):
if (y > rect.top) and (y < rect.bottom):
return True
else:
return False
# set up pygame
pygame.init()
mainClock = pygame.time.Clock()
# set up the window
WINDOWWIDTH = 480
WINDOWHEIGHT = 800
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT), 0, 32)
pygame.display.set_caption('Jumper')
#Directions
LEFT = 4
RIGHT = 6
UP = 8
DOWN = 2
STILL = 5
#blocks location for jumping
#BLOCKLOCY = 700
#Binary for stopping movement
#STOPPER = 0
MOVESPEED = 1
# set up the colors
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
j = {'rect':pygame.Rect(240, 700, 20, 20), 'color':GREEN, 'dir':LEFT, 'jump':STILL}
f1 = {'line':pygame.Rect(0,720,480,2), 'color':GREEN, 'dir':STILL}
f2 = {'line':pygame.Rect(0,650,480,2), 'color':GREEN, 'dir':STILL}
floors = [f1,f2]
# run the game loop
while True:
# check for the QUIT event
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
# draw the black background onto the surface
windowSurface.fill(BLACK)
# This way or that way. Speed Code
if j['dir'] == LEFT:
j['rect'].left -= MOVESPEED
if j['dir'] == RIGHT:
j['rect'].left += MOVESPEED
#JUST JUMP ALREADY!
if j['jump'] == UP:
j['rect'].bottom -= MOVESPEED
#BLOCKLOCY -= MOVESPEED
#Bouce when side hitting
if j['rect'].left < 0:
j['dir'] = RIGHT
if j['rect'].left > WINDOWWIDTH-j['rect'].width:
j['dir'] = LEFT
#Press to Jump
if event.type == KEYDOWN:
if event.key == K_SPACE:
j['jump'] = UP
#stop when land on floor
for n in range(len(floors)):
if (hasJumperLanded(j['rect'], floors[n]['line'])):
j['jump'] = STILL
#Floor controll code for moving level - not working currently
for f in floors:
#if f['dir'] == DOWN:
# f['line'].y += MOVESPEED
# if event.type == KEYDOWN:
# if event.key == K_SPACE:
# f['dir'] = DOWN
# if f['line'].top == BLOCKLOCY:
# f['dir'] = STILL
# STOPPER = 1
#if f['line'].bottom == BLOCKLOCY:
# f['dir'] = STILL
# STOPPER = 1
# draw the block onto the surface
pygame.draw.rect(windowSurface, j['color'], j['rect'])
pygame.draw.rect(windowSurface, f['color'], f['line'])
# draw the window onto the screen
pygame.display.update()
mainClock.tick(1000)
#the floor landing code
def hasJumperLanded(rect1, rect2):
for a, b in [(rect1, rect2), (rect2, rect1)]: ## **
if isFloorTouching(a.bottom, b):
return True
Take a look at the line I marked in this snippet.
Here You are checking the rects to collide both ways.
So you loose the meaning of which one is the floor and which one is moving.
If you just check (rect1, rect2) you will see the difference.
--
EDIT:
Check this out
def hasJumperLanded(rect1, rect2):
for a, b in [(rect1, rect2), (rect2, rect1)]:
if isFloorTouching(rect1, rect2):
return True
def isFloorTouching(y, rect):
if (y.bottom > rect.top) and (y.bottom < rect.bottom):
return True
else:
return False
It is more logical to handle the meaning of the floor inside the isFloorTouching() function.

Categories