在编程和编写游戏方面,我是一个完整的初学者,这是我有史以来的第一个游戏。我使用pygame库与python 3结合使用。我真的很感谢任何反馈。

from __future__ import annotations
from typing import Tuple, List
import pygame
import random
import sys


# screen width & height and block size
bg_width = 500
bg_height = 500
block_size = 10

# direction strings
left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"

# colors (RGB)
bg_color = black = (0, 0, 0)
yellow = (255, 255, 0)
green = (0, 128, 0)
red = (255, 0, 0)
blue = (0, 0, 255)

# pygame & font initialization
pygame.init()
window = pygame.display.set_mode((bg_width, bg_height))
pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')
fps = pygame.time.Clock()


class Snake:
    """This class represents a snake object. Every snake object consists of a head
        and its body.
        ===Attributes===
        head: snake head
        color: snake color
        body: snake body
        direction: direction of head
        size: size of snake (head and body)
        """
    head: List[int, int]
    color: Tuple[int, int, int]
    body: List[List[int, int]]
    direction: str
    size: int

    def __init__(self, color: Tuple[int, int, int]) -> None:
        self.head = [int(10*block_size), int(5*block_size)]
        self.color = color
        self.body = [self.head, [9*block_size, 5*block_size]]
        self.direction = right
        self.size = 2

    def change_dir(self, direc: str) -> None:
        if self.direction != left and direc == right:
            self.direction = right
        elif self.direction != right and direc == left:
            self.direction = left
        elif self.direction != down and direc == up:
            self.direction = up
        elif self.direction != up and direc == down:
            self.direction = down

    def move(self) -> None:
        if self.direction == right:
            self.head[0] += block_size
        elif self.direction == left:
            self.head[0] -= block_size
        elif self.direction == up:
            self.head[1] -= block_size
        elif self.direction == down:
            self.head[1] += block_size
        self.body.insert(0, list(self.head))
        self.body.pop()
        if self.body[0] != self.head:
            self.head = self.body[0]

    def add_to_tail(self) -> None:
        new_part = [self.body[-1][0], self.body[-1][1]]
        self.body.append(new_part)
        self.size += 1

    def get_body(self) -> List[List[int, int]]:
        return self.body


class Food:
    """This class represents a food object. Each food object has an x
       and a y value, a color and a state.
       ===Attributes===
       x: x-coordinate of food object
       y: y-coordinate of food object
       color: color of food object
       state: whether food object is on screen or not
       position: x,y-coordinates pair of food object
       """
    x: int
    y: int
    color: Tuple[int, int, int]
    state: bool
    position: Tuple[int, int]

    def __init__(self, color: Tuple[int, int, int]) -> None:
        self.x = random.randint(0, bg_width//block_size - 1)*block_size
        self.y = random.randint(0, bg_height//block_size - 1)*block_size
        self.color = color
        self.state = True
        self.position = self.x, self.y

    def spawn(self) -> Tuple[int, int]:
        if self.state:
            return self.x, self.y
        else:
            self.state = True
            self.x = random.randint(0, bg_width // block_size-1) * block_size
            self.y = random.randint(0, bg_height // block_size-1) * block_size
            return self.x, self.y

    def update(self, state) -> None:
        self.state = state


def collision(snake_: Snake, food_target_x: int, food_target_y: int) -> int:
    snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
    food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
                            block_size)
    if snake_rect == food_rect:
        snake_.add_to_tail()
        return 1
    return 0


def wall_collision(s: Snake) -> bool:
    if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)\
            or (s.head[1] > bg_height-block_size):
        return True
    return False


def game():

    # initialize food and snake
    food = Food(blue)
    snake = Snake(green)

    # initialize loop logic
    running = True
    is_over = False

    # initialize score
    score = 0

    # game loop
    while running:

        # Game over Screen
        while is_over:
            text_on_screen = font.render("You scored: " + str(score) +
                                         ", Press R to play again or Q to quit",
                                         True, text_colour)
            window.blit(text_on_screen, [55, 225])
            for event in pygame.event.get():
                pressed_key = pygame.key.get_pressed()
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if pressed_key[pygame.K_q]:
                    pygame.quit()
                    sys.exit()
                if pressed_key[pygame.K_r]:
                    game()
            pygame.display.update()

        # check events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            pressed = pygame.key.get_pressed()
            if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
                snake.change_dir(right)
            elif pressed[pygame.K_LEFT] or pressed[pygame.K_a]:
                snake.change_dir(left)
            elif pressed[pygame.K_UP] or pressed[pygame.K_w]:
                snake.change_dir(up)
            elif pressed[pygame.K_DOWN] or pressed[pygame.K_s]:
                snake.change_dir(down)

        # fill window and draw snake
        window.fill(black)
        for item in snake.get_body():
            pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                                                   block_size])

        # move snake
        snake.move()

        # check for collision with wall:
        collision_with_wall = wall_collision(snake)
        if collision_with_wall:
            is_over = True

        # check if food is still on screen and draw it
        food_pos = food.spawn()
        collision_ = collision(snake, *food_pos)
        if collision_ == 1:
            score += 1
            food.update(False)
        pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],
                                              block_size, block_size])

        # renders display
        pygame.display.flip()

        # time delay
        pygame.time.delay(60)
        fps.tick(30)

    pygame.quit()
    quit()

game()


评论

看起来不错,尤其是对于您的第一个游戏:)

我几乎不了解Python:正文:列表[Tuple [int,int]]会更好吗?您有XY对,而不是可变长度列表的列表。

您从哪里获得__future__软件包的?我无法使用PyCharm安装它。

#1 楼

PEP 8

PEP 8建议将常量写为“所有大写字母,下划线分隔单词”。因此,例如:

yellow = (255, 255, 0)


应该是:

YELLOW = (255, 255, 0)


,或者可能是enum(稍后会详细介绍)

报价不一致

通常情况下,除非有特殊原因,否则项目将坚持使用""''。1但是例如:

pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')


不必要地违反了均匀性。

枚举

引用enum上的文档:


枚举是绑定到唯一,恒定值的一组符号名称(成员)。在枚举内,可以按身份比较成员,并且可以迭代枚举本身。例如,

left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"


成为:

from enum import Enum
class Direction(Enum):
    LEFT = "LEFT"
    RIGHT = "RIGHT"
    DOWN = "DOWN"
    UP = "UP"


请参阅此处,了解更多原因,为什么您应该将严格的代码移植到枚举中。另外,可能有一些整数算术技巧可用于消除很多if-elif-else链。 (您将需要做一些进一步的重构才能使其正常运行。)

不必要的注释

考虑:

# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:


我个人将忽略所有评论。我可以看到在某人试图在不了解游戏循环的情况下试图理解代码的情况下,可以在游戏循环注释中添加一个案例,但是我认为游戏循环的概念无处不在,以至于这些注释会成为障碍。 。

如果您不得不写评论,我会写:

# Initialize entities/data for new game
food = Food(blue)
snake = Snake(green)
score = 0

# Init/start game loop
running = True
is_over = False
while running:
    ...


即使那样,我也不是很满意,但是道德上不要添加多余的评论。但是,可能对此存在一些争论。

最好的例子可能是:

br />
不必要的作业

我不确定为什么要这样写:

    # move snake
    snake.move()


何时:

    # check for collision with wall:
    collision_with_wall = wall_collision(snake)
    if collision_with_wall:
        is_over = True


就足够了。

添加main函数

您应该考虑向项目添加main函数。这通常是通过引入以下内容来完成的:

    if wall_collision(snake):
        is_over = True


这使我可以import您的项目,而不能在底部自动执行游戏功能。提高可重用性。

1例如,它可能是项目的默认值"",但是您需要打印带有引号的" Hi ",因此可以选择print('" Hi "')

评论


\ $ \ begingroup \ $
好的答案! +1也是主要功能的出色总结。您链接到的线程有很大帮​​助,但是您的一句话快速摘要是最好的TL之一;我见过。
\ $ \ endgroup \ $
–布鲁斯·韦恩(BruceWayne)
19年5月28日在14:27



\ $ \ begingroup \ $
在“不必要的分配”部分中,如果wall_collision(snake):is_over = True而不是is_over = wall_collision(snake),我们有理由要这样做吗?
\ $ \ endgroup \ $
– vakus
19年5月29日在9:53

\ $ \ begingroup \ $
@vakus,那是两回事。也许上面有很多可能的检查,也可以将is_over设置为True,在这种情况下,我们不想将其设置为False(建议的代码可以这样做)。
\ $ \ endgroup \ $
–密勒德
19年5月29日在13:01

#2 楼

除了另一个答案,我还将添加以下地方来改进(我不会重复该答案的所有内容,我的答案只是一个补充):

1。代码不一致

您的代码中有几个地方代码不一致

def collision():
...
        return 1
    return 0


def wall_collision():
...
        return True
    return False


两个函数都在检查冲突,但是一个函数返回整数(0 / 1)和另一个-布尔值(True / False)。对于head,您不要这样做。

我建议您始终检查代码是否可能不一致。

2。名称不一致

我将其移至另一点,因为它与您的代码的工作方式无关—与其他程序员如何读取您的代码有关。

看看这两个行:

self.head = [int(10*block_size), int(5*block_size)]
self.body = [self.head, [9*block_size, 5*block_size]]


您对方向实体使用了三种不同的命名方式:


X*block_size
body
dir

最好使用一致的变量名。如果您使用不同的方向变量,请仅使用一个单词来获得方向名称。例子如下:


direct
direction
snake_direction

3。小代码改进

有时您看一下代码并想:“嗯,它正在工作,但看起来并不好”。通常在这些情况下,您可以稍微重新组织代码以使其更好一点:)
br />
这行代码也是正确的,但很难阅读:它可以用于:

def change_dir(self, direc: str) -> None:
        if self.direction != left and direc == right:


甚至这个(有时我会使用很长的时间):

for event in pygame.event.get():
    pressed_key = pygame.key.get_pressed()
    if event.type == pygame.QUIT:
        pygame.quit()
        sys.exit()
    if pressed_key[pygame.K_q]:
        pygame.quit()
        sys.exit()


#3 楼

    size: int
...

    self.size = 2


您从不会阅读size,并且Python列表已经知道它们的长度,以防万一您需要它。

您只是在复制List已经通过手动提供的功能保持size与列表长度保持同步。


将游戏逻辑与屏幕渲染详细信息分开

使蛇坐标按像素缩放。这似乎使使用block_size而不是1的代码变得很复杂;我怀疑将蛇坐标保持在块单位会更容易,并且仅出于绘图目的而缩放到像素即可。

例如那么您的外壁检查可能类似于s.head[0] >= bg_width而不是s.head[0] > bg_width-block_size。您的1想法是最初将多个蛇形片段放在同一位置,因此在将来的回合中>实际上会使蛇形在屏幕上变长。该高级设计可能值得一提。 (这就是我的期望,因为我没有想到您的想法)。如果您希望食物引起多个增长部分,则您的方式会更有效率,并且仍然可以使用。吃完食物后,您仍然可以全部做。


单独保存>=:我不确定这对您有帮助。想看第一部分的函数可以执行add_to_tail

我不明白为什么move需要这样做:在我看来,像插入经过修改的head作为新的第一个元素一样,该条件已经创建。因此,这要么是多余的(并且令人困惑),要么是多余的工作,如果您没有冗余地将头部坐标保持在2个位置,那么您将不需要做这些额外的工作。 (列表的开头,位于其自己的对象中。)


       if self.body[0] != self.head:
            self.head = self.body[0]


这应该是枚举或整数,而不是字符串。字符串比较相当便宜,但不如整数便宜。当方向只能使用4个可能值中的1个时,就没有理由将方向设置为任意字符串。字符串并不能使您的代码更容易阅读。查找head = snake_.body[0]move等的表,因此您可以查找给定方向的x和y偏移量,而无需任何条件即可将它们添加到头部的x和y坐标中。 XY向量,删除了查找步骤。或给您一个不同的实现方式:检查反向方向:如果head,则您尝试翻倍。否则,您可以设置[-1, 0]

我喜欢您的设计,因为键盘输入只需调用[1, 0]来检查该输入的游戏逻辑。这样做非常干净,并且如果将direction定义为change_dir而不是new_dir + old_dir == [0,0],仍然可以使用。

#4 楼

祝您第一场比赛顺利。我完全同意别人提出的大多数观点。这里还有一些nitpicks。这个答案有点冗长,但可以作为补充。 :-)

解压缩坐标

这是当前195-197行的样子:

        for item in snake.get_body():
            pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                                                   block_size])


不用依赖索引,我们可以直接将item解压缩到其各自的xy坐标中。这使代码具有更好的简洁性和可读性。 br />关于wall_collision(Snake)


这是wall_collision函数当前的样子(第137-141行):

        for x, y in snake.get_body():
            pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])



作为谓词(即返回布尔值的函数),可以更好地命名该函数。您总是可以找到一种方式来命名谓词,使其以ishas为前缀。我可以使用wall_collisionis_colliding_with_wall代替collides_with_wall来更好地传达意图。
可以简化它。 vurmux的答案提出了几种可能的方法。但是,有一种更容易(而且更易读)的方式可以将其写成一行:
这可以通过Python的比较“链接”以及De Morgan的定律来实现。为了好玩,您可以尝试证明这两个片段是等效的。

点和坐标

我花了一些时间才弄清楚以下几行的含义:

# Before:
        food_pos = food.spawn()
        collision_ = collision(snake, *food_pos)   # 🤔
        if collision_ == 1:
            score += 1
            food.update(False)
        pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],  # 🤔
                                              block_size, block_size])


# After:
        food_x, food_y = food.spawn()
        # the rest is left as an exercise for the reader


我猜这是一个点/坐标?看起来很像。

列表在Python中如此普遍,以至于它们可以具有多种含义。而且,它们是可变长度的结构,可以传达其他信息:将来您可能会更改对象的长度。这可能意味着追加,删除或插入列表。

self.body会适当地使用一个列表,因为它将被添加到各个位置并从各个位置弹出。另一方面,self.head不会一次使用list.appendlist.poplist.insert,这引发了一个问题:它根本不需要是列表吗?最好做成一个元组,它是一个固定长度的结构,并立即向读者传达我们不会修改self.head的长度。非常有道理。但是,元组可以具有多种不同的含义。为了表示xy坐标,可以使用Food.spawn()并创建记录类型collections.namedtuple来做得更好。这可以极大地减少歧义并提高可读性。 :

def wall_collision(s: Snake) -> bool:
    if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)\
            or (s.head[1] > bg_height-block_size):
        return True
    return False


还可以使用Point代替未使用的Point

    return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


我认为,当与Cordes的建议(在他的游戏逻辑与屏幕渲染详细信息部分分开)一起使用时,这特别有用。 。

self.head = [int(10*block_size), int(5*block_size)]


重构随机点生成

在两个地方,食物的坐标通过随机整数更新,通过特殊公式。根据DRY原理,我建议将随机点的生成重构为单个函数Food.position

以上。

重构get_random_point()


当前,仅当调用self.position = get_random_point()Food为false时,Food才会更新其位置。似乎有点long。即使名称spawn()也会因为延迟更新而显得不真实。

我建议在调用state时立即更新位置。 >请注意,updateupdate()现在是冗余的,可以删除。那应该是三声欢呼(更少的代码行,是吗?)。最后一个。


名称Food.state含糊不清。我们可以改善吗?当然!请注意,从逻辑上讲,该函数返回一个布尔值,因此它是一个谓词。我们可以给它加上Food.spawn(self)前缀吗?当然!我认为collision(Snake, int, int)还不错。您也可以选择collisionis

更改了is_snake_on_food类中位置的存储方式,我们可以直接传递食物的位置。因此,该函数的签名可以简化为:

from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])  # we now have a Point type  

class Snake:
    ...
    def __init__(self, color: Tuple[int, int, int]) -> None:
        self.head = Point(10*block_size, 5*block_size)  # foolproof: self.head is a point      
        self.body = [self.head, Point(...)]
    ...
    def add_to_tail(self) -> None:
        self.body.append(Point(self.body[-1][0], self.body[-1][1]))
        self.size += 1


这也使我们无需在行209中解压is_food_reached。 br />

无需创建is_snake_colliding_with_food只是为了比较Food*food_pos。目前,第128-131行是

def move(self) -> None:
    if self.direction == right:

# Before:
        self.head.x += block_size   # AttributeError: can't set attribute   

# After:
        self.head = Point(self.head.x + block_size, self.head.y)   # preferable
# or:
        self.head = self.head._replace(x=self.head.x + block_size) # looks hacky 


相反,可以直接比较坐标:通过将pygame.Rect作为snake_.head传递后,我们可以将其简化为

#5 楼

感谢您的游戏。我应用了@Dair和@vurmux建议的大多数更改,以及@Peter Cordes的部分建议(其中一些并不是那么简单)。在那之后,还剩下一些东西:

游戏逻辑

我不确定这是否是故意的,但蛇不会与自身碰撞。在平常的蛇游戏中,如果蛇咬到她的尾巴,游戏也就结束了。需要时掉头。似乎第一次按键丢失了。

原因在这里:

pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
    ...


应更改为

if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
        snake.change_dir(Direction.RIGHT)
    elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
        snake.change_dir(Direction.LEFT)
    elif event.key == pygame.K_UP or event.key == pygame.K_w:
        snake.change_dir(Direction.UP)
    elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
        snake.change_dir(Direction.DOWN)


但是,这给我们带来了另一个有趣的问题:在不移动蛇的情况下进行2次更改会使蛇立即向相反的方向运行(如前所述,没有击中自己)。 >
由于pygame.event.get()消耗了所有事件,所以将由您自己决定,自己对事件进行排队并在下一帧进行处理。 />游戏结束时,它在我的计算机上使用了约14%的CPU时间,这意味着1个内核处于100%繁忙状态。在pygame.time.delay(60)循环中添加while is_over会有所帮助。

文件中的类

此外,我尝试应用另一原理,即将每个类放在单独的文件中。使用Pycharm之类的IDE这样做很简单,因为它具有移动重构功能。不幸的是,由于以下原因,它不再起作用:

重构后,Food

from game import bg_width, block_size, bg_height


由于使用了全局变量,导致进口周期。查看食品类的可能分辨率,您可以将必需的依赖项作为参数传递:

def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


重构后,Snake具有

以及类似的解决方案也可以适用。

静态成员

您的类似乎两次定义了属性,一次是在__init__中,一次是作为静态成员。我看不到静态成员的任何用法,因此可以在Snake中删除它们:

from game import block_size


Food中删除:

命名

假设bg是背景的缩写,我真的想知道这是否是正确的术语。蛇在木板上移动,而不是在背景上移动。在该行的注释中,您将其称为屏幕的宽度和高度,这也可能是偶然的情况。

考虑游戏的未来版本,您可以在其中添加漂亮的背景图形并显示分数在面板下面,背景和屏幕都不再匹配。

代码重复

通过之前的更改,我注意到Food类中有重复的代码。在__init__内部,基本上是spawn本身。

可以删除此重复项并添加
需要尽早产生。

游戏结束模式下潜在的堆栈溢出

游戏结束后,会有一个循环处理这种情况:

head: List[int, int]
color: Tuple[int, int, int]
body: List[List[int, int]]
direction: str
size: int


当新的回合开始时,我希望游戏会退出循环。但是,事实并非如此。而是有

x: int
y: int
color: Tuple[int, int, int]
state: bool
position: Tuple[int, int]


这是一个递归调用。不太可能在此特定游戏中引起问题,但一般而言,这可能会导致堆栈溢出。

可以通过引入另一个循环来解决

food = Food(BLUE)
food.spawn(bg_width, bg_height, block_size)


然后可以设置game()而不是调用is_over = False

未使用的变量/无法访问的代码

while running循环可以替换为while True,因为没有给running分配其他任何终止循环的方法。

这也意味着将永远无法访问while running之后的代码:

while is_over:


将退出例程更改为running = False,您保存了一些重复的代码,并且该代码运行到最后。这是例如如果以后要实现保存高分列表等功能,这将很有帮助。如果程序中有许多退出点,那么在游戏结束时将很难实现某些功能。

还可以省略quit() ,因为它对代码的最后一个语句没有帮助。从来没有用food.update()来调用它。因此,可以忽略此参数,并将其硬编码到False方法中。代码如下所示:

if event.key == pygame.K_r:
    game()


读取的内容就像是每帧都在一个新的地方产卵。恕我直言,它看起来像这样更好:

蛇的方向改变

注意:@Peter Cordes的矢量方法更加优雅。也许以下内容可能会向您显示重构,在向量不适合的情况下也可以在其他情况下应用。

应用枚举建议后,方向检查如下所示

while running:
    while is_over and running:
        ...

    # Initialization code here

    while running and not is_over:
        ...


Trueupdate()结合起来,我们可以简化
>
pygame.quit()
quit()


现在,我们可以说这是重复的代码并删除了重复项: d甚至更喜欢

while running:
    ...
    food_pos = food.spawn(board_width, board_height, block_size)
    if collision(snake, *food_pos):
        score += 1
        food.update()