我写了一个简单的Python蛇游戏,它约有250行代码。有人可以给我一些有关如何重构/使其更好的建议吗? >
# game.py - 3/22/2013

import pygame, sys, os
from pygame.locals import *
from classes import *


def main():
    pygame.init()
    pygame.display.set_caption('PyGame Snake')

    window = pygame.display.set_mode((480, 480))
    screen = pygame.display.get_surface()
    clock = pygame.time.Clock()
    font = pygame.font.Font('freesansbold.ttf', 20)

    game = SnakeGame(window, screen, clock, font)

    while game.run(pygame.event.get()):
        pass

    pygame.quit()
    sys.exit()


if __name__ == '__main__':
    main()


#1 楼

1.简介

考虑到这是您用PyGame编写的第一个程序,总体来说还不错。我在下面发表了很多评论,但请不要太在意这个答案的长度:关于这段长度的代码,总是有很多话要说。

2。游戏设计问题


游戏可以执行一些说明。我必须查看源代码才能看到需要使用WASD进行移动。另外,您也可以允许玩家也使用箭头键(玩家可以尝试使用这些自然键)。

您使用FPS(每秒帧数)来控制蛇的速度。此设计决定使您以与蛇移动相同的频率来处理游戏中的所有内容。每秒的概念帧和每秒移动的蛇的速度是不同的,因此最好将它们分开。

目前游戏中除了蛇没有其他东西,所以您可以逃脱有了这个。但是,一旦添加了需要以不同速度进行动画处理的其他游戏元素,您就会遇到这种困难。最好在事情还很简单的时候就做好这项工作。

请参阅第5节,以解决此问题的一种方法。

蛇!
新游戏开始时,食物位置不会重置。 (这可能会导致蛇在游戏开始时与食物重叠。)
在游戏过程中不会得出比分。

3。主要评论



collidesWithSelf的文档字符串如下所示:

"""
# Because of the way new pieces are added when the snake grows, eating a
# new food block could cause the snake to die if it's in a certain position. 
# So instead of checking if any of the spots have two pieces at once, the new
# algorithm only checks if the position of the head piece contains more than one block.

for p in self.pieces:
    if len(self.pieces) - len([c for c in self.pieces if c != p]) > 1: return True
return False
"""


docstring。文档字符串的目的是向试图使用该方法的程序员解释方法的接口。但是在这里,您需要自己了解一下此功能的历史以及为什么要按原样实现它。这些注释正确地属于注释。

您在此功能中遇到问题的原因是蛇的生长不正确。在grow()方法中,您将在与蛇当前运动相反的方向上生长一个新的尾段。但这会导致蛇自相交。

“蛇”游戏起作用的通常方式是,当蛇吃一些食物时,它不会立即长出新的尾巴。相反,它会等到下一次移动,然后在原来的旧尾巴位置上增加一个新的尾巴段。这很容易实现,方法是在蛇每次进食时都增加一个计数器:

def grow(self):
    self.growth_pending += 1


然后递减计数器而不是删除尾段:

if self.growth_pending > 0:
    self.growth_pending -= 1
else:
    # Remove tail
    self.pieces.pop()


这避免了自相交,因此这使您可以使用原始方法来实现碰撞操作。但是,您可能会考虑使用这种更简单的方法:

it = iter(self.pieces)
head = next(it)
return head in it



您用1到4之间的数字表示方向。很难记住哪个方向,所以容易出错,将1在代码的一部分中视为“向上”,而在另一部分中将其视为“向下”。如果使用名称DIRECTION_UP等,则您犯此错误的可能性将大大降低。创建这些名称很麻烦:为什么不使用它们?

(但请参见下面的3.4,以获得更好的建议。)看起来很狡猾,因为在一系列测试的末尾没有else:。 1到4。当然,您希望设计程序时不会发生这种情况。因此,您可以通过像下面这样重写代码来使内容清晰明了:
为什么不使用一个从1到4的数字来表示一个方向(很难记住哪个是哪个方向),为什么不使用一个对(δx,δy)呢?例如,您可以编写:

head = ()
if self.direction == 1: head = (headX, headY - 1)
elif self.direction == 2: head = (headX, headY + 1)
elif self.direction == 3: head = (headX - 1, headY)
elif self.direction == 4: head = (headX + 1, headY)


这将为您节省大量的self.direction测试。例如,您可以像上面这样重写我提供的代码:

if   self.direction == DIRECTION_UP:    head = (headX, headY - 1)
elif self.direction == DIRECTION_DOWN:  head = (headX, headY + 1)
elif self.direction == DIRECTION_LEFT:  head = (headX - 1, headY)
elif self.direction == DIRECTION_RIGHT: head = (headX + 1, headY)
else: raise RuntimeError("Bad direction: {}".format(self.direction))



类似地,而不是对输入进行一堆if ... elif ...测试:

DIRECTION_UP    =  0, -1
DIRECTION_DOWN  =  0,  1
DIRECTION_LEFT  = -1,  0
DIRECTION_RIGHT =  1,  0


您可以找到一个将键映射到方向的查找表:

old_head = self.getHead()
new_head = old_head[0] + self.direction[0], old_head[1] + self.direction[1]


,然后测试变为:

if   e.key == K_w: self.nextDirection = 1
elif e.key == K_s: self.nextDirection = 2
elif e.key == K_a: self.nextDirection = 3
elif e.key == K_d: self.nextDirection = 4



您检查与游戏区域边缘的碰撞是否是这样的:

# Input mapping
KEY_DIRECTION = {
    K_w: DIRECTION_UP,    K_UP:    DIRECTION_UP,   
    K_s: DIRECTION_DOWN,  K_DOWN:  DIRECTION_DOWN, 
    K_a: DIRECTION_LEFT,  K_LEFT:  DIRECTION_LEFT, 
    K_d: DIRECTION_RIGHT, K_RIGHT: DIRECTION_RIGHT,
}


但是PyGame使用if .. elif ...方法定义了Rect类,因此您可以在collidepoint中设置如下矩形:

if e.key in KEY_DIRECTION:
    self.next_direction = KEY_DIRECTION[e.key]


,然后像这样测试碰撞:

(hx, hy) = self.snake.getHead()
if hx < 1 or hy < 1 or hx > self.sizeX or hy > self.sizeY:


您从1开始对坐标进行编号,这会给您带来一些困难。例如,您必须从每个坐标中减去1,然后再将其传递给SnakeGame.__init__。如果您的坐标从0开始,则不必这样做。

您的许多代码都处理以对(x,y)表示的位置或向量。这意味着每次处理一个位置时,都必须将其分解为x和y坐标,处理这些坐标,然后将结果重新组合为新的对。例如:

self.world = Rect(1, 1, 21, 21)


您可以通过制作一个表示位置和向量的类来简化此代码。遗憾的是,PyGame没有提供此类,但是您可以轻松地在网络上找到许多实现,也可以自己编写一个实现:

if not self.world.collidepoint(self.snake.getHead()):


现在您可以编写:

(headX, headY) = self.getHead()
if self.direction == 1: head = (headX, headY - 1)


并进行许多其他简化。 (有关pygame.draw.rect类的更多方法,请参见第5节。)


4。次要评论


Python样式指南(PEP8)表示方法名称应“使用函数命名规则:小写字母,必要时用下划线分隔单词,以提高可读性”。因此,您应该考虑将Vector重命名为collidesWithSelf,依此类推。 (您没有义务遵循PEP8,但是它使其他Python程序员可以更轻松地阅读您的代码。)
将代码组织到文件collides_with_selfgame.py中似乎不是出于任何原则,含糊不清的名字classes.py证实了这一点。对于这样的小型游戏,我认为将所有代码放在一个文件中不会丢失任何内容。 (如果它增长到了要拆分的程度,显然要做的就是将classes.py类放在其自己的模块中。)
对我来说,不清楚从分离游戏初始化中得到什么编码到Snakemain中。为什么不将所有初始化都放在后者中呢?
SnakeGame.__init__的末尾不需要sys.exit():Python结束运行程序后会自动退出。添加此行只会使您很难从交互式解释器测试您的程序,因为当您退出游戏时,它也会退出交互式解释器。

在很多地方都有不必要的括号。像这样的行:

class Vector(tuple):
    def __add__(self, other): return Vector(v + w for v, w in zip(self, other))


可以这样写:

new_head = self.getHead() + self.direction


,因为逗号比分配中的赋值更紧密蟒蛇。 “好像您懂语言一样编程”!

main拼写错误。 (这就是为什么您不使用它的原因吗?)

您使用排长队来代表蛇,这是一个很好的方法。但是,您可以使用Python列表实现队列。这里的麻烦在于,Python列表可以有效地在结尾处添加和删除元素,但不能在一开始就有效。特别是操作

(headX, headY) = self.getHead()


需要的时间与DIRECTON_DOWN的长度成正比。 (您可以查询Python Wiki上的TimeComplexity页面,以查看内置Python数据结构上操作的时间复杂性。)在这里这没什么大不了的,因为蛇永远不会变长,但是值得实践在考虑算法的复杂性时。

要实现有效的队列,请使用self.pieces


该行:

headX, headY = self.getHead()


可以重写:

self.pieces.insert(0, head)


因为负列表索引从列表的末尾开始倒数。


而不是进行除法并将结果强制为整数:

return self.pieces[len(self.pieces) - 1]


使用Python的地板除法运算:

return self.pieces[-1]



5。修改后的代码

此代码解决了上面的注释,并提供了一些其他改进供您发现。

int(width / self.sizeX)


评论


\ $ \ begingroup \ $
嗨,谢谢您,这段代码非常有用。但是我不知道如何实现世界界限,所以当蛇离开世界时,它会从另一侧返回。如果不是self.world.collidepoint(self.snake.head()):#在此处重新放置蛇。任何帮助都是非常欢迎的,因为我对向量一无所知:-(
\ $ \ endgroup \ $
– Kasper Taeymans
2014年9月11日12:40



\ $ \ begingroup \ $
@kasperTaeymans:如果您先尝试一下,然后再请他们帮助您修复代码,那对于Stack Overflow来说将是一个好问题。
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
2014年9月11日下午14:37

\ $ \ begingroup \ $
Gareth嗨,谢谢您的答复!有时间我会在stackoverflow上询问它。我现在对项目的其他部分感到困惑。当问题和尝试在线时,我将在评论中发布一个链接。这可能对其他感兴趣的读者很有用。我还将链接回此代码审查。
\ $ \ endgroup \ $
– Kasper Taeymans
2014-09-12 13:48



\ $ \ begingroup \ $
我问了一个与stackoverflow上的no boundary选项有关的问题。这是该问题的链接:stackoverflow.com/questions/25891680/…
\ $ \ endgroup \ $
– Kasper Taeymans
2014年9月17日下午13:17