问题描述
在我用 pygame 制作的简单蛇游戏中,我试图将我的输入循环与我的游戏逻辑分开,但是,我真的很难弄清楚为什么在我运行程序时什么都没有发生。
我尝试在子进程中导入 pygame,我检查了子进程中的错误,但一无所获。我在谷歌上查看,但找不到任何可用的示例或类似问题。有没有人想过这些东西?
好的,这是代码:
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,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,255),((ci * 10) + 5,(ri * 10) + 5),5)
elif c == '\u25c9':
# wprint("Placing food at {},{}".format(ci,ri))
pygame.draw.circle(self.game_surf,127,5)
timg = self.font.render("score: {},Level: {}".format(self.g.score,self.g.score // 10 + 1),True,0))
self.screen.blit(self.game_surf,0))
self.screen.blit(timg,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?",0))
self.screen.blit(timg,((self.size[0] >> 1) - 150,self.size[1] >> 1))
timg = self.font.render("Yes",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()
抱歉,有点奇怪,这个是从控制台迁移到pygame的,所以有些逻辑还在使用unicode符号。
解决方法
通常在 GUI 应用程序中,希望将 GUI 与逻辑分开是很常见的。 这样做有好处,因为这意味着即使您的逻辑,您的 GUI 也能保持响应 忙。但是,为了并发运行,有很多缺点,包括 间接费用。知道 python 不是“线程安全的”也很重要,所以你可以打破 如果你不小心,事情(见比赛条件)。
没有并发的简化示例
您的示例非常复杂,所以让我们从一个简单的示例开始:一个简单的 pygame 设置 一个移动的点
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))
pygame.draw.circle(window,(0,255),position,10)
pygame.display.update()
pygame.quit()
quit()
我们将把游戏逻辑从 gui 中分离出来
多处理和其他选项:
因此,python 中的多处理允许您通过多个解释器同时使用多个内核。 虽然这听起来不错,但就 I/O 而言:它带来了更高的开销并且根本没有帮助(它可能会 损害你的表现)。线程和 asyncio 都在单核上运行,即它们不是“并行”计算。但 他们允许在等待其他代码完成的同时完成代码。换句话说,您可以输入命令 当您的逻辑在其他地方愉快地运行时。
TLDR:作为一般规则:
- CPU 绑定(100% 的核心)程序:使用多处理,
- I/O 绑定程序:使用线程或异步
螺纹版
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,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,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))
pygame.draw.circle(self.window,self.logic.position,10)
pygame.display.update()
pygame.time.wait(10)
pygame.quit()
quit()
if __name__ == '__main__':
game = Game([800,600])
game.start()
那么取得了什么成果?
像这样简单的东西真的不需要任何性能升级。不过,这确实允许的是
pygame GUI 将保持反应性,即使其背后的逻辑挂起。为了看到这一点,我们可以把逻辑放在
循环睡眠,看看我们仍然可以移动 GUI,点击东西,输入命令等。
改变:
# 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