# 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_self
和game.py
中似乎不是出于任何原则,含糊不清的名字classes.py
证实了这一点。对于这样的小型游戏,我认为将所有代码放在一个文件中不会丢失任何内容。 (如果它增长到了要拆分的程度,显然要做的就是将classes.py
类放在其自己的模块中。)对我来说,不清楚从分离游戏初始化中得到什么编码到
Snake
和main
中。为什么不将所有初始化都放在后者中呢?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