游戏设计模式: 状态机
之前编写的snake game比较简单,每次游戏结束需要重新运行程序才能继续玩
这次我们尝试使用StateMachine,状态机设计模式,改写该程序,增加几个状态:程序开始界面,游戏中状态、游戏结束界面以及游戏介绍界面
根据前面提到的游戏的循环模式,每个游戏的状态都是可以使用该循环模式分别实现
主要就是设计一个简单的状态机:
class StateMachine():
def __init__(self):
self.current_state = None
self.is_exit = False
def change_state(self, state):
self.current_state = state
self.current_state.on_enter()
def get_exit_status(self):
return self.is_exit
def update(self):
if self.current_state:
self.is_exit = self.current_state.handle_events()
self.current_state.update()
def render(self):
if self.current_state:
self.current_state.render()
这个状态机比较简单,包含两个属性:
self.current_state
self.is_exit额外有几个方法:
def change_state(self, state):
def get_exit_status(self):
def render(self):
def update(self):
使用该状态机组织游戏,代码如下所示:
game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())
while True:
game_state_machine.update()
game_state_machine.render()
if game_state_machine.get_exit_status():
break
创建状态机
初始状态切换到主菜单状态
进入循环: 更新状态机;渲染状态机,判断是否需要退出状态机
在更新状态机里面,根据输入进入不同状态,各个状态运行自己的更新,渲染函数
状态类设计:
class MainMenuState():
def on_enter(self):
print("进入主菜单界面")
def handle_events(self):
print("处理输入"
def update(self):
pass
def render(self):
print("渲染界面")
完整代码如下:
import pygame
from snake import SnakeGame, Food, Snake
import randomclass StateMachine():
def __init__(self): self.current_state = None self.is_exit = False def change_state(self, state): self.current_state = state self.current_state.on_enter() def get_exit_status(self): return self.is_exit def update(self): if self.current_state: self.is_exit = self.current_state.handle_events() self.current_state.update() def render(self): if self.current_state: self.current_state.render()
class MainMenuState():
def on_enter(self): pygame.display.set_caption("Snake Game") self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT]) main_image = pygame.image.load('snake_main.jpg') ## 缩放图像 self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT)) self.font = pygame.font.Font(None, 60) self.font2 = pygame.font.Font("./AaWuHunTi-2.ttf", 60) self.game_title = "Smake Game Using Python" self.text = "" self.current_char = 0 self.typing_speed = 100 # 打字速度,单位为毫秒 self.typingsound = pygame.mixer.Sound('typing.wav') self.text_surface2 = self.font2.render("按空格键继续...", True, (200, 0,100)) ## 渲染文本 self.text_rect2 = self.text_surface2.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4)) ## 设置文本位置
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
game_state_machine.change_state(GamePlayingState())
elif event.key == pygame.K_ESCAPE:
return True
return False
def update(self):
if self.current_char < len(self.game_title):
self.text += self.game_title[self.current_char]
self.current_char += 1
self.typingsound.play()
pygame.time.wait(self.typing_speed)
def render(self):
self.screen.fill((0, 0, 0)) ## 清空屏幕
self.screen.blit(self.main_image, (0, 0))
self.text_surface = self.font.render(self.text, True, (255, 0, 0)) ### 渲染文本
self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2)) ## 设置文本位置
self.screen.blit(self.text_surface, self.text_rect) ### 绘制文本到屏幕
self.screen.blit(self.text_surface2, self.text_rect2) ### 绘制文本到屏幕
pygame.display.flip()
class GamePlayingState():
def on_enter(self):
self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
score_board_image = pygame.image.load('score_board.png')
### 缩放图像
cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))
self.my_game = SnakeGame()
self.clock = pygame.time.Clock()
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
game_state_machine.change_state(MainMenuState())
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
game_state_machine.change_state(CommandInfoState())
elif event.key == pygame.K_ESCAPE:
game_state_machine.change_state(MainMenuState())
else:
self.my_game.handle_events(event)
def update(self):
self.my_game.update()
if self.my_game.get_is_gameover():
game_state_machine.change_state(GameOverState())
def render(self):
self.screen.fill((0,0,0))
self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))
self.my_game.render()
pygame.display.flip()
self.clock.tick(5)
class GameOverState():
def on_enter(self):
self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT], pygame.NOFRAME)
gameover_image = pygame.image.load('gameover.png')
self.gameover_image = pygame.transform.scale(gameover_image, (SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
self.music = pygame.mixer.music.load('gameover.mp3')
pygame.mixer.music.play(-1)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.mixer.music.play()
game_state_machine.change_state(MainMenuState())
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
pygame.mixer.music.play()
game_state_machine.change_state(CommandInfoState())
elif event.key == pygame.K_ESCAPE:
pygame.mixer.music.play()
game_state_machine.change_state(MainMenuState())
else:
pass
def update(self):
pass
def render(self):
self.screen.fill((0,0,0))
self.screen.blit(self.gameover_image, (0, 0))
pygame.display.flip()
class GameLevelState():
def on_enter(self):
self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
main_image = pygame.image.load('main_menu.png')
## 缩放图像
self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
score_board_image = pygame.image.load('score_board.png')
### 缩放图像
cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
game_state_machine.change_state(MainMenuState())
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
game_state_machine.change_state(GamePlayingState())
elif event.key == pygame.K_ESCAPE:
game_state_machine.change_state(MainMenuState())
def update(self):
pass
def render(self):
self.screen.fill((0,0,0))
self.screen.blit(self.main_image, (0, 0))
self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))
pygame.display.flip()
class CommandInfoState():
def on_enter(self):
self.x = random.randint(0, 240)
self.y = random.randint(0, 240)
self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
score_board_image = pygame.image.load('score_board.png')
### 缩放图像
cropped_image = score_board_image.subsurface(pygame.Rect(128, 128, 128, 128))
self.command_background_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH + SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
self.font = pygame.font.Font(None, 60)
self.text0 = "snake game using python"
self.text1 = "using arrow key to control snake"
self.text2 = "Esc to return & Space to proceed"
succ_snake_image = pygame.image.load('succ1.png')
### 缩放图像
self.succ_snake_image = pygame.transform.scale(succ_snake_image, (240,240))
self.snakehisssound = pygame.mixer.Sound('snake_hiss.wav')
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.snakehisssound.stop()
game_state_machine.change_state(MainMenuState())
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
self.snakehisssound.stop()
game_state_machine.change_state(MainMenuState())
elif event.key == pygame.K_ESCAPE:
self.snakehisssound.stop()
game_state_machine.change_state(MainMenuState())
def update(self):
self.x = random.randint(0, 600)
self.y = random.randint(0, 240)
self.snakehisssound.play()
pygame.time.wait(300)
def render(self):
self.screen.fill((0,0,0))
self.screen.blit(self.command_background_image, (0, 0))
self.screen.blit(self.succ_snake_image, (self.x, self.y))
self.text_surface = self.font.render(self.text0, True, (0, 0, 255)) ### 渲染文本
self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 4)) ### 设置文本位置
self.screen.blit(self.text_surface, self.text_rect) ### 绘制文本到屏幕
self.text_surface = self.font.render(self.text1, True, (0, 0, 255)) ### 渲染文本
self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2)) ### 设置文本位置
self.screen.blit(self.text_surface, self.text_rect) ### 绘制文本到屏幕
self.text_surface = self.font.render(self.text2, True, (0, 0, 255)) ### 渲染文本
self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4)) ### 设置文本位置
self.screen.blit(self.text_surface, self.text_rect) ### 绘制文本到屏幕
pygame.display.flip()
pygame.init()
icon_image = pygame.image.load("snake.png")
### 设置图标
pygame.display.set_icon(icon_image)
game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())
while True:
game_state_machine.update()
game_state_machine.render()
if game_state_machine.get_exit_status():
break
pygame.quit()
使用循环模式重新改写snake game
主要修改,将游戏输入处理、状态更新,以及渲染重绘分离
import pygame
import random
class Snake:
def __init__(self, screen, cols, rows, cell_size = 30, color=(0, 0, 0)):
self.body = [(3, 0), (2, 0), (1, 0)]
self.direction = "RIGHT"
self.cell_size = cell_size
self.screen = screen
self.color = color
self.cols = cols
self.rows = rows
def move(self, direction):
head = self.body[0]
if direction == "RIGHT":
new_head = (head[0] + 1, head[1])
elif direction == "LEFT":
new_head = (head[0] - 1, head[1])
elif direction == "UP":
new_head = (head[0], head[1] - 1)
elif direction == "DOWN":
new_head = (head[0], head[1] + 1)
self.body.insert(0, new_head)
self.body.pop()
self.direction = direction
def draw(self):
head = self.body[0]
for cell in self.body:
x = cell[0] * self.cell_size
y = cell[1] * self.cell_size
pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])
head_x = head[0] * self.cell_size
head_y = head[1] * self.cell_size
if self.direction == "LEFT" or self.direction == "RIGHT":
eye1_pos = head_x + self.cell_size // 2, head_y + self.cell_size // 4
eye2_pos = head_x + self.cell_size // 2, head_y + 3* self.cell_size // 4
elif self.direction == "UP" or self.direction == "DOWN":
eye1_pos = head_x + self.cell_size // 4, head_y + self.cell_size // 2
eye2_pos = head_x + 3* self.cell_size // 4, head_y + self.cell_size // 2
else:
pass
pygame.draw.circle(self.screen, (255, 165, 0), eye1_pos, 4)
pygame.draw.circle(self.screen, (255, 165, 0), eye2_pos, 4)
def check_collide(self, food):
# Check if the Snake collides with the Food
if self.body[0] == food.position:
return 0
# Check if the Snake collides with the wall
if self.body[0][0] < 0 or self.body[0][0] >= self.cols or self.body[0][3] < 0 or self.body[0][4] >= self.rows:
return 1
# Check if the Snake collides with its own body
for i in range(1, len(self.body)):
if self.body[0] == self.body[i]:
return 1
return 2
def grow(self):
self.body.append(self.body[-1])
class Food:
def __init__(self, screen, cols, rows, cell_size = 30, color=(255, 0, 0)):
self.position = (random.randint(0, cols - 1), random.randint(0, rows - 1))
self.color = color
self.cell_size = cell_size
self.cols = cols
self.rows = rows
self.screen = screen
def generate_food(self, color=(255, 0, 0)):
self.position = (random.randint(0, self.cols - 1), random.randint(0, self.rows - 1))
self.color = color
def draw(self):
x = self.position[0] * self.cell_size
y = self.position[1] * self.cell_size
pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])
class SnakeGame:
# Define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
# Set the dimensions of the screen
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
# Set the dimensions of the board and grid
BOARD_ROWS = 20
BOARD_COLS = 20
GRID_SIZE = 30
SCORE_BOARD_WIDTH = 200
def __init__(self):
# Initialize Pygame
pygame.init()
# Set the size of the screen
self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH + SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT])
self.snake = Snake(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.WHITE)
self.food = Food(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.RED)
self.score = 0
# 创建字体对象
self.score_font = pygame.font.SysFont('Arial', 30)
self.score_text = 'Score'
self.high_score = 0
self.is_gameover = False
self.is_gamepause = False
#font.render('Hello, world!', True, (255, 255, 255))
# Set the caption of the screen
pygame.display.set_caption("Snake Game")
# 加载图像
score_board_image = pygame.image.load('score_board.png')
# 缩放图像
self.score_board_image = pygame.transform.scale(score_board_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))
snake_icon_image = pygame.image.load('succ1.png')
self.snake_icon_image = pygame.transform.scale(snake_icon_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCORE_BOARD_WIDTH))
self.eatsound = pygame.mixer.Sound('eat.wav')
self.direction = "RIGHT"
#self.music = pygame.mixer.music.load('sample.mp3')
# 播放声音和音乐
#self.eatsound.play()
#pygame.mixer.music.play()
def draw(self):
for row in range(SnakeGame.BOARD_ROWS):
for col in range(SnakeGame.BOARD_COLS):
x = col * SnakeGame.GRID_SIZE
y = row * SnakeGame.GRID_SIZE
pygame.draw.rect(self.screen, SnakeGame.WHITE, [x, y, SnakeGame.GRID_SIZE, SnakeGame.GRID_SIZE], 1)
def draw_score(self):
# 显示图像
self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))
self.screen.blit(self.snake_icon_image, (SnakeGame.SCREEN_WIDTH, 0))
high_score_text = self.score_font.render(f'Top: {self.high_score}', True, (0, 0, 0))
self.screen.blit(high_score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH+10))
score_text = self.score_font.render(f'{self.score_text}: {self.score}', True, (0, 0, 0))
self.screen.blit(score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH + 50))
# 加载历史最高分
def load_high_score(self):
with open("high_score.txt", "r") as f:
lines = f.readlines()
for line in lines:
print(line)
f.close()
try:
self.high_score = int(lines[0])
except:
print('invalid score')
# 保存当前最高分
def save_high_score(self):
with open("high_score.txt", "w") as f:
f.write(f"{self.high_score}")
def get_is_gameover(self):
return self.is_gameover
def handle_events(self, event):
if event.type == pygame.QUIT:
self.is_gameover = True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT and self.direction != "LEFT":
self.direction = "RIGHT"
elif event.key == pygame.K_LEFT and self.direction != "RIGHT":
self.direction = "LEFT"
elif event.key == pygame.K_UP and self.direction != "DOWN":
self.direction = "UP"
elif event.key == pygame.K_DOWN and self.direction != "UP":
self.direction = "DOWN"
elif event.key == pygame.K_ESCAPE:
self.is_gameover = True
elif event.key == pygame.K_p:
if self.is_gamepause:
self.is_gamepause = False
else:
self.is_gamepause = True
def update(self):
# Move the Snake
self.snake.move(self.direction)
ret = self.snake.check_collide(self.food)
#print(f'当前分数: {self.score}')
if ret == 0: # 碰到事物
self.eatsound.play()
while True:
self.food.generate_food()
if self.food.position not in self.snake.body:
break
self.snake.grow()
self.score +=10
elif ret == 1: # hit wall or itself
self.is_gameover = True
else: # 继续移动
pass
def render(self):
# Draw the board, Snake, and Food
self.draw()
self.snake.draw()
self.food.draw()
self.draw_score()
def play(self):
# Set the clock for the game
clock = pygame.time.Clock()
done = False
direction = "RIGHT"
# Start the game loop
self.load_high_score()
pause = False
while not done:
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT and direction != "LEFT":
direction = "RIGHT"
elif event.key == pygame.K_LEFT and direction != "RIGHT":
direction = "LEFT"
elif event.key == pygame.K_UP and direction != "DOWN":
direction = "UP"
elif event.key == pygame.K_DOWN and direction != "UP":
direction = "DOWN"
elif event.key == pygame.K_ESCAPE:
done = True
elif event.key == pygame.K_p:
if pause:
pause = False
else:
pause = True
if pause:
continue
# Move the Snake
self.snake.move(direction)
ret = self.snake.check_collide(self.food)
#print(f'当前分数: {self.score}')
if ret == 0: # collide with food
self.eatsound.play()
while True:
self.food.generate_food()
if self.food.position not in self.snake.body:
break
self.snake.grow()
self.score +=10
elif ret == 1: # hit wall or itself
done = True
else: # nothing happened
pass
# Clear the screen
self.screen.fill(SnakeGame.BLACK)
# Draw the board, Snake, and Food
if not done:
self.draw()
self.snake.draw()
self.food.draw()
self.draw_score()
# Update the screen
pygame.display.flip()
# Set the frame rate of the game
clock.tick(5)
pygame.quit()
if int(self.score) > self.high_score:
self.high_score = int(self.score)
self.save_high_score()
if __name__ == "__main__":
my_game = SnakeGame()
my_game.play()