小小白祈祷中...

前言

通过前两章对Pygame的学习,我们了解了它的基本使用,现在,我们就开始真正动手写一个游戏。

这个游戏名字为:ink spill,中文名:墨水溢出。这是Python中一个非常典型的游戏,我们首先来看看游戏长什么样子以及应该怎么玩:

img点击并拖拽以移动编辑

小伙伴们看完后,应该差不多明白这个游戏的玩法了。现在,让我们站在“设计者”的角度来考虑,这个游戏应该怎么去制作。

如何制作游戏?

一般说来,游戏制作要考虑三个方面的内容:

  • 游戏道具 (图片,音效等)
  • 逻辑控制 (游戏状态的逻辑控制)
  • UI设计 (游戏界面的设计)

游戏道具 ,主要通过两种方式获得:一是加载图片,二是用代码直接绘制

本游戏中,需要用到的图片有以下几个:


img

img

img

img

img


其余的,咱均用代码直接绘制。

ink spill 分析

游戏主界面

img点击并拖拽以移动编辑

它主要由以下几个部分组成:

  • 界面正中央的大格子
  • 界面左边的生命“计”
  • 界面正下方的六个不同颜色的调色板
  • 界面右下方的“重新游戏”按钮和“设置”按钮

如何设计 ink spill 的难度?

大伙首先想到的肯定是,控制小格子的数量,数量越多,难度越大

img点击并拖拽以移动编辑

除此之外,关于游戏难度,咱还可以创建一个 boxesToChange 变量,随机将某个小格子的周围格子变成与之同色,这样的格子数用boxesToChange来描述,这样,“简单难度”就将boxesToChange的值设大一点(使得同色的越多),“困难难度”就将boxesToChange的值设小一点(使得同色的越少)(或者直接设为0)。

ink spill 有哪些游戏状态?

前面几章说过,一个游戏会有各种各样的游戏状态,比如角色的血量,武器类型等。首先,让我们思考一下,在这个游戏中,需要哪些变量来存储哪些游戏状态呢?

不知道小伙伴们能想到多少个游戏状态,答案在下面的代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 根据设置界面,有不同的整个格子尺寸、小格子数量和生命

# 小格子大小
SMALL_BOX_SIZE = 60 # 大小以像素为单位
MEDIUM_BOX_SIZE = 20
LARGE_BOX_SIZE = 11

# 整个格子的大小
SMALL_BOARD_SIZE = 6 # 大小以一个小格子为单位
MEDIUM_BOARD_SIZE = 17
LARGE_BOARD_SIZE = 30

# 最多几次操作(生命)
SMALL_MAX_LIFE = 10
MEDIUM_MAX_LIFE = 30
LARGE_MAX_LIFE = 64

FPS = 30
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
boxSize = MEDIUM_BOX_SIZE
PALETTE_GAP_SIZE = 10 # 调色板间隔大小
PALETTE_SIZE = 45
EASY = 0 # 难度:简单
MEDIUM = 1 # 难度:中等
HARD = 2 # 难度:困难

difficulty = MEDIUM # 游戏以难度“中等”模式开始
maxLife = MEDIUM_MAX_LIFE
boardWidth = MEDIUM_BOARD_SIZE
boardHeight = MEDIUM_BOARD_SIZE

# R G B
WHITE = (255, 255, 255)
DARKGRAY = (70, 70, 70)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 128, 0)
PURPLE = (255, 0, 255)

# 每个方案中的第一种颜色是背景色,接下来的六种是调色板颜色。
COLOR_SCHEMES = (((150, 200, 255), RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE),
((0, 155, 104), (97, 215, 164), (228, 0, 69), (0, 125, 50), (204, 246, 0), (148, 0, 45),
(241, 109, 149)),
((195, 179, 0), (255, 239, 115), (255, 226, 0), (147, 3, 167), (24, 38, 176), (166, 147, 0),
(197, 97, 211)),
((85, 0, 0), (155, 39, 102), (0, 201, 13), (255, 118, 0), (206, 0, 113), (0, 130, 9), (255, 180, 115)),
((191, 159, 64), (183, 182, 208), (4, 31, 183), (167, 184, 45), (122, 128, 212), (37, 204, 7),
(88, 155, 213)),
((200, 33, 205), (116, 252, 185), (68, 56, 56), (52, 238, 83), (23, 149, 195), (222, 157, 227),
(212, 86, 185)))
# 对颜色的处理
for i in range(len(COLOR_SCHEMES)):
assert len(COLOR_SCHEMES[i]) == 7, '颜色方案 %s 没有7种颜色!.' % (i)
# 背景色,调色板设置默认色
bgColor = COLOR_SCHEMES[0][0]
paletteColors = COLOR_SCHEMES[0][1:]

你会看到,记录游戏状态,用到了非常多的变量。依据变量的名称,咱可以很容易推出这个变量的作用是什么。实际上,之后的游戏逻辑控制,就是在更改这些变量的值。

ink spill 游戏框架

接着,我们来考虑编写整个游戏框架

接下来编写的代码会非常之多,请小伙伴们仔细体会每个函数的具体作用,这样才能更好的理解整个游戏。

框架编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def main():
# 需要用到函数之外的变量
global FPS_CLOCK, DISPLAY_SURF, LOGO_IMAGE, SPOT_IMAGE, SETTINGS_IMAGE, SETTINGS_BUTTON_IMAGE, RESET_BUTTON_IMAGE

# 初始化
pygame.init()

# 控制帧率
FPS_CLOCK = pygame.time.Clock()

# 创建窗口
DISPLAY_SURF = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

# 加载图片
LOGO_IMAGE = pygame.image.load('inkspilllogo.png')
SPOT_IMAGE = pygame.image.load('inkspillspot.png')
SETTINGS_IMAGE = pygame.image.load('inkspillsettings.png')
SETTINGS_BUTTON_IMAGE = pygame.image.load('inkspillsettingsbutton.png')
RESET_BUTTON_IMAGE = pygame.image.load('inkspillresetbutton.png')

# 设置窗口标题
pygame.display.set_caption('墨水溢出')

# 生成界面
mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)
life = maxLife

# 记录上次点击界面下方调色板的颜色
lastPaletteClicked = None

while True: # 主游戏循环
paletteClicked = None
resetGame = False

# 画屏幕
DISPLAY_SURF.fill(bgColor)

# 加载图标和相关按钮
drawLogoAndButtons()

# 加载正中央的大格子并随机初始化每个小格子的颜色
drawBoard(mainBoard)

# 加载左侧的"生命计"
drawLifeMeter(life)

# 加载屏幕底部的六个调色板
drawPalettes()

# 判断玩家是否想要退出游戏
checkForQuit()

代码中有许多函数如generateRandomBoard(),我们还未实现,下面我们来实现这些函数:

generateRandomBoard()

产生游戏主窗口-----中央的整个大格子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def generateRandomBoard(width, height, difficulty=MEDIUM):

# 为整个大格子中的每个小格子创建具有随机颜色的数据结构。
board = []
for x in range(width):
column = []
for y in range(height):
column.append(random.randint(0, len(paletteColors) - 1))
board.append(column)

# 通过将一些小格子设置为与相邻格子相同的颜色,使同色整个大格子更容易。

# 确定要更改的小格子数。
if difficulty == EASY:
if boxSize == SMALL_BOX_SIZE:
boxesToChange = 100
else:
boxesToChange = 1500
elif difficulty == MEDIUM:
if boxSize == SMALL_BOX_SIZE:
boxesToChange = 5
else:
boxesToChange = 200
else:
boxesToChange = 0

# 挂邻居格子的颜色:
for i in range(boxesToChange):
# 随机选择要复制其颜色的框
x = random.randint(1, width - 2)
y = random.randint(1, height - 2)

# 随机选择要更改的邻居格子
direction = random.randint(0, 3)
if direction == 0: # 左,下
board[x - 1][y] = board[x][y]
board[x][y - 1] = board[x][y]
elif direction == 1: # 右,上
board[x + 1][y] = board[x][y]
board[x][y + 1] = board[x][y]
elif direction == 2: # 左,下
board[x][y - 1] = board[x][y]
board[x + 1][y] = board[x][y]
else: # 左,上
board[x][y + 1] = board[x][y]
board[x - 1][y] = board[x][y]

return board

drawLogoAndButtons()

给界面加上部分按钮和图标

1
2
3
4
5
6
7
8
9
def drawLogoAndButtons():
# 绘制墨水溢出徽标、设置和重置按钮。
# bait()函数用于将图片加载到DISPALY_SURF这个Surface对象上
DISPLAY_SURF.blit(LOGO_IMAGE, (WINDOW_WIDTH - LOGO_IMAGE.get_width(), 0))
DISPLAY_SURF.blit(SETTINGS_BUTTON_IMAGE,
(WINDOW_WIDTH - SETTINGS_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height()))
DISPLAY_SURF.blit(RESET_BUTTON_IMAGE, (WINDOW_WIDTH - RESET_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height() - RESET_BUTTON_IMAGE.get_height()))

代码中使用了大量坐标运算来控制图片的位置,请小伙伴们细细体会。

drawBoard(mainBoard)

加载界面正中央的大格子并随机初始化每个小格子的颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def drawBoard(board, transparency=255):  # 透明度设置默认为255
# 彩色方块将绘制到临时曲面,然后绘制到DISPLAY_SURF曲面。这样我们就可以在DISPLAY_SURF的顶部绘制具有透明度的正方形了。
tempSurf = pygame.Surface(DISPLAY_SURF.get_size()) # 获取窗口尺寸
tempSurf = tempSurf.convert_alpha() # 支持透明
tempSurf.fill((0, 0, 0, 0)) # 先全部处理为黑色

for x in range(boardWidth):
for y in range(boardHeight):
# 获取每个小格子左上角的坐标
left, top = leftTopPixelCoordOfBox(x, y)
r, g, b = paletteColors[board[x][y]]
pygame.draw.rect(tempSurf, (r, g, b, transparency), (left, top, boxSize, boxSize))
left, top = leftTopPixelCoordOfBox(0, 0)
# 为防止边缘的小格子颜色与窗口背景色相同,在整个大格子外面包一层黑色细线,厚度为1px
pygame.draw.rect(tempSurf, BLACK, (left - 1, top - 1, boxSize * boardWidth + 1, boxSize * boardHeight + 1), 1)
DISPLAY_SURF.blit(tempSurf, (0, 0))

代码中调用了leftTopPixelCoordOfBox(x,y),实现如下:

1
2
3
4
5
6
def leftTopPixelCoordOfBox(boxx, boxy):
# 返回某个小格子最左上方像素的x和y。
# 注意整个大格子位于窗口的正中央
x_margin = int((WINDOW_WIDTH - (boardWidth * boxSize)) / 2) # x_margin 为整个大格子最左边与窗口最左边的间距
y_margin = int((WINDOW_HEIGHT - (boardHeight * boxSize)) / 2) # y_margin 为整个大格子最上边与窗口最上边的间距
return boxx * boxSize + x_margin, boxy * boxSize + y_margin

drawLifeMeter(life)

绘制游戏界面左边的生命“计”并进行简单的生命“逻辑计算”

1
2
3
4
5
6
7
8
9
10
11
12
def drawLifeMeter(currentLife):
# '生命计' 竖直放置,与上下边缘各相距20px
lifeBoxSize = int((WINDOW_HEIGHT - 40) / maxLife)

# 绘制"生命计"的背景色,'生命计'的左上角位于坐标(20,20),宽20px,高20 + (maxLife * lifeBoxSize) px
pygame.draw.rect(DISPLAY_SURF, bgColor, (20, 20, 20, 20 + (maxLife * lifeBoxSize)))

for i in range(maxLife):
if currentLife >= (maxLife - i): # 画一个实心的红色方框
pygame.draw.rect(DISPLAY_SURF, RED, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize))
# 加1px白色的边框
pygame.draw.rect(DISPLAY_SURF, WHITE, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize), 1)

在图形的位置定位上,我们用了大量的坐标运算,请小伙伴们细细体会。

drawPalettes()

给界面添上正下方的六个调色板并进行简单的逻辑控制:

1
2
3
4
5
6
7
8
9
10
def drawPalettes():
# 在屏幕底部绘制六个调色板
numColors = len(paletteColors)
x_margin = int((WINDOW_WIDTH - ((PALETTE_SIZE * numColors) + (PALETTE_GAP_SIZE * (numColors - 1)))) / 2)
for i in range(numColors):
left = x_margin + (i * PALETTE_SIZE) + (i * PALETTE_GAP_SIZE)
top = WINDOW_HEIGHT - PALETTE_SIZE - 10
pygame.draw.rect(DISPLAY_SURF, paletteColors[i], (left, top, PALETTE_SIZE, PALETTE_SIZE))
# 为了美观,在格子外2px再加2px厚度的对应颜色的区域
pygame.draw.rect(DISPLAY_SURF, bgColor, (left + 2, top + 2, PALETTE_SIZE - 4, PALETTE_SIZE - 4), 2)

checkForQuit()

玩家想要退出游戏时的处理:

1
2
3
4
5
6
7
8
9
10
def checkForQuit():
# 如果存在任何退出事件,则终止程序
for event in pygame.event.get(QUIT): # 获取所有退出事件
pygame.quit() # 如果存在任何退出事件,则终止
sys.exit()
for event in pygame.event.get(KEYUP): # 获取所有KEYUP(按键按下)事件
if event.key == K_ESCAPE:
pygame.quit() # 如果KEYUP(按键按下)事件是针对Esc键的,则终止
sys.exit()
pygame.event.post(event) # 将其他KEYUP(按键按下)事件对象放回原处

逻辑控制

接着,我们来考虑游戏的主要逻辑控制

由于我们并未创建按钮,所以当“鼠标点击”这个事件发生时,我们尝试着获取其点击的位置坐标(利用Event对象pos 属性:mousex, mousey = event.pos),将这个坐标与界面上某个图标的位置区域比较(使用**pygame.Rest.collidepoint(mousex, mousey)函数),如果在这个区域内,就说明玩家想要点击该图标来实现对应的功能(如鼠标点击“reset**”图标表示玩家想重新开始游戏),我们编写代码做出响应即可。基于这样的思想,咱可以将main()函数补充完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def main():
global FPS_CLOCK, DISPLAY_SURF, LOGO_IMAGE, SPOT_IMAGE, SETTINGS_IMAGE, SETTINGS_BUTTON_IMAGE, RESET_BUTTON_IMAGE

pygame.init()
FPS_CLOCK = pygame.time.Clock()
DISPLAY_SURF = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

# 加载图片
LOGO_IMAGE = pygame.image.load('inkspilllogo.png')
SPOT_IMAGE = pygame.image.load('inkspillspot.png')
SETTINGS_IMAGE = pygame.image.load('inkspillsettings.png')
SETTINGS_BUTTON_IMAGE = pygame.image.load('inkspillsettingsbutton.png')
RESET_BUTTON_IMAGE = pygame.image.load('inkspillresetbutton.png')

# 设置窗口标题
pygame.display.set_caption('墨水溢出')
mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)
life = maxLife
lastPaletteClicked = None

while True: # 主游戏循环
paletteClicked = None
resetGame = False

# 画屏幕
DISPLAY_SURF.fill(bgColor)
# 加载图标和相关按钮
drawLogoAndButtons()
# 加载正中央的大格子并随机初始化每个小格子的颜色
drawBoard(mainBoard)
# 加载左侧的"生命计"
drawLifeMeter(life)
# 加载屏幕底部的六个调色板
drawPalettes()
# 判断玩家是否想要退出游戏
checkForQuit()

for event in pygame.event.get(): # 事件处理循环
if event.type == MOUSEBUTTONUP: # 如果事件是鼠标点击
mousex, mousey = event.pos # 获取鼠标点击的坐标
# 如果点击的是"SETTINGS"字样处
if pygame.Rect(WINDOW_WIDTH - SETTINGS_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height(),
SETTINGS_BUTTON_IMAGE.get_width(),
SETTINGS_BUTTON_IMAGE.get_height()).collidepoint(mousex, mousey):
# 就展示设置界面
resetGame = showSettingsScreen() # showSettingsScreen() 待实现
# 如果点击的是"RESET"字样处
elif pygame.Rect(WINDOW_WIDTH - RESET_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height() - RESET_BUTTON_IMAGE.get_height(),
RESET_BUTTON_IMAGE.get_width(),
RESET_BUTTON_IMAGE.get_height()).collidepoint(mousex, mousey):
# 就重新开始游戏
resetGame = True
else:
# 检查是否点击了调色板按钮,如果点击了,返回六个调色板中点击的那一个颜色的索引
paletteClicked = getColorOfPaletteAt(mousex, mousey) # getColorOfPaletteAt()待实现

if paletteClicked is not None and paletteClicked != lastPaletteClicked:

# 单击的调色板按钮与上次单击的调色板按钮不同,防止鼠标意外单击同一调色板两次
lastPaletteClicked = paletteClicked

# 对大格子填充颜色,floodAnimation()待实现
floodAnimation(mainBoard, paletteClicked)
# 点击一次,"生命"减少1
life -= 1

resetGame = False
if hasWon(mainBoard): # 如果赢了,hasWon()待实现
for i in range(4): # 成功的界面效果:闪烁边框4次
flashBorderAnimation(WHITE, mainBoard) # flashBorderAnimation()待实现
# "闪光"结束后,重新开始游戏
resetGame = True
# 暂停2s后再开始游戏
pygame.time.wait(2000)
elif life == 0:
# 生命降为零,玩家失败
drawLifeMeter(0)
# 更新界面
pygame.display.update()
# 等待0.4s
pygame.time.wait(400)
# 失败的结束效果:用黑色"闪光"4次
for i in range(4):
flashBorderAnimation(BLACK, mainBoard)。 # flashBorderAnimation()待实现
resetGame = True
# 暂停2s后开始重新游戏
pygame.time.wait(2000)

if resetGame:
# 重新开始游戏
mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)
life = maxLife
lastPaletteClicked = None
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)

上述主要逻辑控制代码中,还有部分函数未实现:

showSettingsScreen()

展示“设置”界面并提供相应逻辑控制:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def showSettingsScreen():
# 获取全局变量
global difficulty, boxSize, boardWidth, boardHeight, maxLife, paletteColors, bgColor

# 此函数中的像素坐标是通过将inkspillsettings.png图像加载到图形编辑器中并从中读取像素坐标获得的

origDifficulty = difficulty
origBoxSize = boxSize
screenNeedsRedraw = True

while True:
if screenNeedsRedraw:
DISPLAY_SURF.fill(bgColor)
# 加载"设置"图片
DISPLAY_SURF.blit(SETTINGS_IMAGE, (0, 0))

# 将"墨迹"图片标记放在选定的颜色左边
if difficulty == EASY:
DISPLAY_SURF.blit(SPOT_IMAGE, (30, 4))
if difficulty == MEDIUM:
DISPLAY_SURF.blit(SPOT_IMAGE, (8, 41))
if difficulty == HARD:
DISPLAY_SURF.blit(SPOT_IMAGE, (30, 76))

# 将墨迹标记放置在选定尺寸旁边
if boxSize == SMALL_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (22, 150))
if boxSize == MEDIUM_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (11, 185))
if boxSize == LARGE_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (24, 220))

# 加载设置界面右边的颜色选择框
for i in range(len(COLOR_SCHEMES)):
drawColorSchemeBoxes(500, i * 60 + 30, i) # 待实现

# 更新界面
pygame.display.update()

screenNeedsRedraw = False # 默认情况下,不重新绘制屏幕
# 事件处理循环
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == KEYUP:
if event.key == K_ESCAPE:
# 设置屏幕上的Esc键返回游戏
return not (origDifficulty == difficulty and origBoxSize == boxSize)
elif event.type == MOUSEBUTTONUP:
screenNeedsRedraw = True # 屏幕应该重新绘制
mousex, mousey = event.pos # 鼠标点击的坐标

# 检查难度按钮上是否有咔嗒声
if pygame.Rect(74, 16, 111, 30).collidepoint(mousex, mousey):
difficulty = EASY
elif pygame.Rect(53, 50, 104, 29).collidepoint(mousex, mousey):
difficulty = MEDIUM
elif pygame.Rect(72, 85, 65, 31).collidepoint(mousex, mousey):
difficulty = HARD

# 检查尺寸按钮上是否有点击
elif pygame.Rect(63, 156, 84, 31).collidepoint(mousex, mousey):
# 小板尺寸设置:
boxSize = SMALL_BOX_SIZE
boardWidth = SMALL_BOARD_SIZE
boardHeight = SMALL_BOARD_SIZE
maxLife = SMALL_MAX_LIFE
elif pygame.Rect(52, 192, 106, 32).collidepoint(mousex, mousey):
# 中板尺寸设置:
boxSize = MEDIUM_BOX_SIZE
boardWidth = MEDIUM_BOARD_SIZE
boardHeight = MEDIUM_BOARD_SIZE
maxLife = MEDIUM_MAX_LIFE
elif pygame.Rect(67, 228, 58, 37).collidepoint(mousex, mousey):
# 大板尺寸设置:
boxSize = LARGE_BOX_SIZE
boardWidth = LARGE_BOARD_SIZE
boardHeight = LARGE_BOARD_SIZE
maxLife = LARGE_MAX_LIFE
elif pygame.Rect(178, 418, 215, 34).collidepoint(mousex, mousey):
# 点击“Back To Game”按钮
return not (origDifficulty == difficulty and origBoxSize == boxSize)

for i in range(len(COLOR_SCHEMES)):
# 单击颜色方案按钮
if pygame.Rect(500, 30 + i * 60, MEDIUM_BOX_SIZE * 3, MEDIUM_BOX_SIZE * 2).collidepoint(mousex,
mousey):
bgColor = COLOR_SCHEMES[i][0]
paletteColors = COLOR_SCHEMES[i][1:]

上述代码中调用了 drawColorSchemeBoxes():实现如下:

1
2
3
4
5
6
7
8
9
def drawColorSchemeBoxes(x, y, schemeNum):
# 绘制“设置”屏幕上显示的颜色方案框
for boxy in range(2):
for boxx in range(3):
pygame.draw.rect(DISPLAY_SURF, COLOR_SCHEMES[schemeNum][3 * boxy + boxx + 1],
(x + MEDIUM_BOX_SIZE * boxx, y + MEDIUM_BOX_SIZE * boxy, MEDIUM_BOX_SIZE, MEDIUM_BOX_SIZE))
if paletteColors == COLOR_SCHEMES[schemeNum][1:]:
# 将"墨迹"图片放置在所选配色方案旁边
DISPLAY_SURF.blit(SPOT_IMAGE, (x - 50, y))

getColorOfPaletteAt(mousex, mousey)

获取被点击的调色板颜色的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def getColorOfPaletteAt(x, y):
# 返回x和y参数覆盖的调色板颜色的索引
numColors = len(paletteColors)
xmargin = int((WINDOW_WIDTH - ((PALETTE_SIZE * numColors) + (PALETTE_GAP_SIZE * (numColors - 1)))) / 2)
# 调色板底端距窗口底端10px
top = WINDOW_HEIGHT - PALETTE_SIZE - 10
for i in range(numColors):
# 六个调色板左上角的坐标:(left,top)
left = xmargin + (i * PALETTE_SIZE) + (i * PALETTE_GAP_SIZE)
r = pygame.Rect(left, top, PALETTE_SIZE, PALETTE_SIZE)
# 找出鼠标单击区域是否在任何调色板内
if r.collidepoint(x, y):
return i # 如果在,则返回所点击的调色板的序号
# 如果x和y不在任何调色板上,则返回None
return None

hasWon()

判断玩家是否胜利,实现如下:

1
2
3
4
5
6
7
8
def hasWon(board):
# 如果整个棋盘颜色相同,则表示玩家获胜
for x in range(boardWidth):
for y in range(boardHeight):
# 只要发现一个颜色与左上角的颜色不同,玩家还没有赢
if board[x][y] != board[0][0]:
return False
return True

floodAnimation(mainBoard, paletteClicked)

paletteClicked对应的颜色填充mainBoard(中央的大格子):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def floodAnimation(board, paletteClicked, animationSpeed=25):
origBoard = copy.deepcopy(board) # 深拷贝整个大格子
# 从左上角处的格子开始同色填充,floodFill()待实现
floodFill(board, board[0][0], paletteClicked, 0, 0)

for transparency in range(0, 255, animationSpeed):
# “新”大格子在大格子上慢慢变得不透明
drawBoard(origBoard)
# 更新透明度
drawBoard(board, transparency)
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)

floodFill()

填充算法,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def floodFill(board, oldColor, newColor, x, y):
# 洪水填充算法
if oldColor == newColor or board[x][y] != oldColor:
# 如果左上角的颜色和点击颜色没有相邻,直接返回
return
board[x][y] = newColor # 更改当前格子的颜色
# 对任何相邻格子进行递归调用:
if x > 0:
floodFill(board, oldColor, newColor, x - 1, y)
if x < boardWidth - 1:
floodFill(board, oldColor, newColor, x + 1, y)
if y > 0:
floodFill(board, oldColor, newColor, x, y - 1)
if y < boardHeight - 1:
floodFill(board, oldColor, newColor, x, y + 1)

flashBorderAnimation(Color, mainBoard)

游戏胜利或失败的结束画面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def flashBorderAnimation(color, board, animationSpeed=30):
# "闪光"效果结束后,需要回到原来(游戏刚结束)的界面,此处先保存原来的界面
origSurf = DISPLAY_SURF.copy()
flashSurf = pygame.Surface(DISPLAY_SURF.get_size())
flashSurf = flashSurf.convert_alpha()
# 实现"闪光"效果
for start, end, step in ((0, 256, 1), (255, 0, -1)):
# 外循环的第一次迭代将内循环的透明度设置为从0到255,第二次迭代将透明度设置为从255到0。这就是“闪光”。
for transparency in range(start, end, animationSpeed * step):
DISPLAY_SURF.blit(origSurf, (0, 0))
r, g, b = color
flashSurf.fill((r, g, b, transparency))
DISPLAY_SURF.blit(flashSurf, (0, 0))
drawBoard(board) # 在透明层的顶部绘制板
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)
# 绘制原来(游戏刚结束)的界面
DISPLAY_SURF.blit(origSurf, (0, 0))

我猜,大伙们可能已经懵了…但是做游戏,考虑的东西会非常之多,也许咱只有好好的理解每一个细节,才能真正做出好的游戏。

完整代码

最后,附上游戏完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
import random, sys, copy, pygame
from pygame.locals import * # 将所有的 Pygame 常量导入

# 根据设置界面,有不同的大格子尺寸、小格子数量和寿命

# 小格子大小
SMALL_BOX_SIZE = 60 # 大小以像素为单位
MEDIUM_BOX_SIZE = 20
LARGE_BOX_SIZE = 11

# 整个格子的大小
SMALL_BOARD_SIZE = 6 # 大小以一个小格子为单位
MEDIUM_BOARD_SIZE = 17
LARGE_BOARD_SIZE = 30

# 最多几次操作(生命)
SMALL_MAX_LIFE = 10
MEDIUM_MAX_LIFE = 30
LARGE_MAX_LIFE = 64

FPS = 30
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
boxSize = MEDIUM_BOX_SIZE
PALETTE_GAP_SIZE = 10 # 调色板间隔大小
PALETTE_SIZE = 45
EASY = 0 # 难度:简单
MEDIUM = 1 # 难度:中等
HARD = 2 # 难度:困难

difficulty = MEDIUM # 游戏以难度“中等”模式开始
maxLife = MEDIUM_MAX_LIFE
boardWidth = MEDIUM_BOARD_SIZE
boardHeight = MEDIUM_BOARD_SIZE

# R G B
WHITE = (255, 255, 255)
DARKGRAY = (70, 70, 70)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 128, 0)
PURPLE = (255, 0, 255)

# 每个方案中的第一种颜色是背景色,接下来的六种是调色板颜色。
COLOR_SCHEMES = (((150, 200, 255), RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE),
((0, 155, 104), (97, 215, 164), (228, 0, 69), (0, 125, 50), (204, 246, 0), (148, 0, 45),
(241, 109, 149)),
((195, 179, 0), (255, 239, 115), (255, 226, 0), (147, 3, 167), (24, 38, 176), (166, 147, 0),
(197, 97, 211)),
((85, 0, 0), (155, 39, 102), (0, 201, 13), (255, 118, 0), (206, 0, 113), (0, 130, 9), (255, 180, 115)),
((191, 159, 64), (183, 182, 208), (4, 31, 183), (167, 184, 45), (122, 128, 212), (37, 204, 7),
(88, 155, 213)),
((200, 33, 205), (116, 252, 185), (68, 56, 56), (52, 238, 83), (23, 149, 195), (222, 157, 227),
(212, 86, 185)))
# 对颜色的处理
for i in range(len(COLOR_SCHEMES)):
assert len(COLOR_SCHEMES[i]) == 7, '颜色方案 %s 没有7种颜色!.' % (i)
# 背景色,调色板设置默认色
bgColor = COLOR_SCHEMES[0][0]
paletteColors = COLOR_SCHEMES[0][1:]


def main():
global FPS_CLOCK, DISPLAY_SURF, LOGO_IMAGE, SPOT_IMAGE, SETTINGS_IMAGE, SETTINGS_BUTTON_IMAGE, RESET_BUTTON_IMAGE

pygame.init()
FPS_CLOCK = pygame.time.Clock()
DISPLAY_SURF = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

# 加载图片
LOGO_IMAGE = pygame.image.load('inkspilllogo.png')
SPOT_IMAGE = pygame.image.load('inkspillspot.png')
SETTINGS_IMAGE = pygame.image.load('inkspillsettings.png')
SETTINGS_BUTTON_IMAGE = pygame.image.load('inkspillsettingsbutton.png')
RESET_BUTTON_IMAGE = pygame.image.load('inkspillresetbutton.png')

# 设置窗口标题
pygame.display.set_caption('墨水溢出')
mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)
life = maxLife
lastPaletteClicked = None

while True: # 主游戏循环
paletteClicked = None
resetGame = False

# 画屏幕
DISPLAY_SURF.fill(bgColor)
# 加载图标和相关按钮
drawLogoAndButtons()
# 加载正中央的大格子并随机初始化每个小格子的颜色
drawBoard(mainBoard)
# 加载左侧的"生命计"
drawLifeMeter(life)
# 加载屏幕底部的六个调色板
drawPalettes()
# 判断玩家是否想要退出游戏
checkForQuit()

for event in pygame.event.get(): # 事件处理循环
if event.type == MOUSEBUTTONUP: # 如果事件是鼠标点击
mousex, mousey = event.pos # 获取鼠标点击的坐标
# 如果点击的是"SETTINGS"字样处
if pygame.Rect(WINDOW_WIDTH - SETTINGS_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height(),
SETTINGS_BUTTON_IMAGE.get_width(),
SETTINGS_BUTTON_IMAGE.get_height()).collidepoint(mousex, mousey):
# 就展示设置界面
resetGame = showSettingsScreen()
# 如果点击的是"RESET"字样处
elif pygame.Rect(WINDOW_WIDTH - RESET_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height() - RESET_BUTTON_IMAGE.get_height(),
RESET_BUTTON_IMAGE.get_width(),
RESET_BUTTON_IMAGE.get_height()).collidepoint(mousex, mousey):
# 就重新开始游戏
resetGame = True
else:
# 检查是否点击了调色板按钮
paletteClicked = getColorOfPaletteAt(mousex, mousey)

if paletteClicked is not None and paletteClicked != lastPaletteClicked:

# 单击的调色板按钮与上次单击的调色板按钮不同,防止播放器意外单击同一调色板两次
lastPaletteClicked = paletteClicked

# 填充颜色
floodAnimation(mainBoard, paletteClicked)
# 点击一次,"生命"减少1
life -= 1

resetGame = False
if hasWon(mainBoard): # 如果赢了
for i in range(4): # 成功的界面效果:闪烁边框4次
flashBorderAnimation(WHITE, mainBoard)
# "闪光"结束后,重新开始游戏
resetGame = True
# 暂停2s后再开始游戏
pygame.time.wait(2000)
elif life == 0:
# 生命降为零,玩家失败
drawLifeMeter(0)
# 更新界面
pygame.display.update()
# 等待0.4s
pygame.time.wait(400)
# 失败的结束效果:用黑色"闪光"4次
for i in range(4):
flashBorderAnimation(BLACK, mainBoard)
resetGame = True
# 暂停2s后开始重新游戏
pygame.time.wait(2000)

if resetGame:
# 重新开始游戏
mainBoard = generateRandomBoard(boardWidth, boardHeight, difficulty)
life = maxLife
lastPaletteClicked = None
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)


def checkForQuit():
# 如果存在任何退出事件,则终止程序
for event in pygame.event.get(QUIT): # 获取所有退出事件
pygame.quit() # 如果存在任何退出事件,则终止
sys.exit()
for event in pygame.event.get(KEYUP): # 获取所有KEYUP(按键按下)事件
if event.key == K_ESCAPE:
pygame.quit() # 如果KEYUP(按键按下)事件是针对Esc键的,则终止
sys.exit()
pygame.event.post(event) # 将其他KEYUP(按键按下)事件对象放回原处


def hasWon(board):
# 如果整个棋盘颜色相同,则表示玩家获胜
for x in range(boardWidth):
for y in range(boardHeight):
# 只要发现一个颜色与左上角的颜色不同,玩家还没有赢
if board[x][y] != board[0][0]:
return False
return True


def showSettingsScreen():
# 获取全局变量
global difficulty, boxSize, boardWidth, boardHeight, maxLife, paletteColors, bgColor

# 此函数中的像素坐标是通过将inkspillsettings.png图像加载到图形编辑器中并从中读取像素坐标获得的

origDifficulty = difficulty
origBoxSize = boxSize
screenNeedsRedraw = True

while True:
if screenNeedsRedraw:
DISPLAY_SURF.fill(bgColor)
# 加载"设置"图片
DISPLAY_SURF.blit(SETTINGS_IMAGE, (0, 0))

# 将"墨迹"图片标记放在选定的颜色左边
if difficulty == EASY:
DISPLAY_SURF.blit(SPOT_IMAGE, (30, 4))
if difficulty == MEDIUM:
DISPLAY_SURF.blit(SPOT_IMAGE, (8, 41))
if difficulty == HARD:
DISPLAY_SURF.blit(SPOT_IMAGE, (30, 76))

# 将墨迹标记放置在选定尺寸旁边
if boxSize == SMALL_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (22, 150))
if boxSize == MEDIUM_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (11, 185))
if boxSize == LARGE_BOX_SIZE:
DISPLAY_SURF.blit(SPOT_IMAGE, (24, 220))

# 加载设置界面右边的颜色选择框
for i in range(len(COLOR_SCHEMES)):
drawColorSchemeBoxes(500, i * 60 + 30, i)

# 更新界面
pygame.display.update()

screenNeedsRedraw = False # 默认情况下,不重新绘制屏幕
# 事件处理循环
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == KEYUP:
if event.key == K_ESCAPE:
# 设置屏幕上的Esc键返回游戏
return not (origDifficulty == difficulty and origBoxSize == boxSize)
elif event.type == MOUSEBUTTONUP:
screenNeedsRedraw = True # 屏幕应该重新绘制
mousex, mousey = event.pos # 鼠标点击的坐标

# 检查难度按钮上是否有咔嗒声
if pygame.Rect(74, 16, 111, 30).collidepoint(mousex, mousey):
difficulty = EASY
elif pygame.Rect(53, 50, 104, 29).collidepoint(mousex, mousey):
difficulty = MEDIUM
elif pygame.Rect(72, 85, 65, 31).collidepoint(mousex, mousey):
difficulty = HARD

# 检查尺寸按钮上是否有点击
elif pygame.Rect(63, 156, 84, 31).collidepoint(mousex, mousey):
# 小板尺寸设置:
boxSize = SMALL_BOX_SIZE
boardWidth = SMALL_BOARD_SIZE
boardHeight = SMALL_BOARD_SIZE
maxLife = SMALL_MAX_LIFE
elif pygame.Rect(52, 192, 106, 32).collidepoint(mousex, mousey):
# 中板尺寸设置:
boxSize = MEDIUM_BOX_SIZE
boardWidth = MEDIUM_BOARD_SIZE
boardHeight = MEDIUM_BOARD_SIZE
maxLife = MEDIUM_MAX_LIFE
elif pygame.Rect(67, 228, 58, 37).collidepoint(mousex, mousey):
# 大板尺寸设置:
boxSize = LARGE_BOX_SIZE
boardWidth = LARGE_BOARD_SIZE
boardHeight = LARGE_BOARD_SIZE
maxLife = LARGE_MAX_LIFE
elif pygame.Rect(178, 418, 215, 34).collidepoint(mousex, mousey):
# 点击“Back To Game”按钮
return not (origDifficulty == difficulty and origBoxSize == boxSize)

for i in range(len(COLOR_SCHEMES)):
# 单击颜色方案按钮
if pygame.Rect(500, 30 + i * 60, MEDIUM_BOX_SIZE * 3, MEDIUM_BOX_SIZE * 2).collidepoint(mousex,
mousey):
bgColor = COLOR_SCHEMES[i][0]
paletteColors = COLOR_SCHEMES[i][1:]


def drawColorSchemeBoxes(x, y, schemeNum):
# 绘制“设置”屏幕上显示的颜色方案框
for boxy in range(2):
for boxx in range(3):
pygame.draw.rect(DISPLAY_SURF, COLOR_SCHEMES[schemeNum][3 * boxy + boxx + 1],
(x + MEDIUM_BOX_SIZE * boxx, y + MEDIUM_BOX_SIZE * boxy, MEDIUM_BOX_SIZE, MEDIUM_BOX_SIZE))
if paletteColors == COLOR_SCHEMES[schemeNum][1:]:
# 将"墨迹"图片放置在所选配色方案旁边
DISPLAY_SURF.blit(SPOT_IMAGE, (x - 50, y))


def flashBorderAnimation(color, board, animationSpeed=30):
# "闪光"效果结束后,需要回到原来(游戏刚结束)的界面,此处先保存原来的界面
origSurf = DISPLAY_SURF.copy()
flashSurf = pygame.Surface(DISPLAY_SURF.get_size())
flashSurf = flashSurf.convert_alpha()
# 实现"闪光"效果
for start, end, step in ((0, 256, 1), (255, 0, -1)):
# 外循环的第一次迭代将内循环的透明度设置为从0到255,第二次迭代将透明度设置为从255到0。这就是“闪光”。
for transparency in range(start, end, animationSpeed * step):
DISPLAY_SURF.blit(origSurf, (0, 0))
r, g, b = color
flashSurf.fill((r, g, b, transparency))
DISPLAY_SURF.blit(flashSurf, (0, 0))
drawBoard(board) # 在透明层的顶部绘制板
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)
# 绘制原来(游戏刚结束)的界面
DISPLAY_SURF.blit(origSurf, (0, 0))


def floodAnimation(board, paletteClicked, animationSpeed=25):
origBoard = copy.deepcopy(board) # 深拷贝整个大格子
# 从左上角处的格子开始同色填充
floodFill(board, board[0][0], paletteClicked, 0, 0)

for transparency in range(0, 255, animationSpeed):
# “新”大格子在大格子上慢慢变得不透明
drawBoard(origBoard)
# 更新透明度
drawBoard(board, transparency)
# 更新界面
pygame.display.update()
# 控制帧率
FPS_CLOCK.tick(FPS)


def generateRandomBoard(width, height, difficulty=MEDIUM):
# 为整个大格子中的每个小格子创建具有随机颜色的数据结构。
board = []
for x in range(width):
column = []
for y in range(height):
column.append(random.randint(0, len(paletteColors) - 1))
board.append(column)

# 通过将一些小格子设置为与相邻格子相同的颜色,使解决整个大格子更容易。

# 确定要更改的小格子数。
if difficulty == EASY:
if boxSize == SMALL_BOX_SIZE:
boxesToChange = 100
else:
boxesToChange = 1500
elif difficulty == MEDIUM:
if boxSize == SMALL_BOX_SIZE:
boxesToChange = 5
else:
boxesToChange = 200
else:
boxesToChange = 0

# 挂邻居格子的颜色:
for i in range(boxesToChange):
# 随机选择要复制其颜色的框
x = random.randint(1, width - 2)
y = random.randint(1, height - 2)

# 随机选择要更改的邻居格子
direction = random.randint(0, 3)
if direction == 0: # 左,下
board[x - 1][y] = board[x][y]
board[x][y - 1] = board[x][y]
elif direction == 1: # 右,上
board[x + 1][y] = board[x][y]
board[x][y + 1] = board[x][y]
elif direction == 2: # 左,下
board[x][y - 1] = board[x][y]
board[x + 1][y] = board[x][y]
else: # 左,上
board[x][y + 1] = board[x][y]
board[x - 1][y] = board[x][y]
return board


def drawLogoAndButtons():
# 绘制墨水溢出徽标、设置和重置按钮。
DISPLAY_SURF.blit(LOGO_IMAGE, (WINDOW_WIDTH - LOGO_IMAGE.get_width(), 0))
DISPLAY_SURF.blit(SETTINGS_BUTTON_IMAGE,
(WINDOW_WIDTH - SETTINGS_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height()))
DISPLAY_SURF.blit(RESET_BUTTON_IMAGE, (WINDOW_WIDTH - RESET_BUTTON_IMAGE.get_width(),
WINDOW_HEIGHT - SETTINGS_BUTTON_IMAGE.get_height() - RESET_BUTTON_IMAGE.get_height()))


def drawBoard(board, transparency=255): # 透明度设置默认为255
# 彩色方块将绘制到临时曲面,然后绘制到DISPLAY_SURF曲面。这样我们就可以在DISPLAY_SURF的顶部绘制具有透明度的正方形了。
tempSurf = pygame.Surface(DISPLAY_SURF.get_size()) # 获取窗口尺寸
tempSurf = tempSurf.convert_alpha() # 支持透明
tempSurf.fill((0, 0, 0, 0)) # 先全部处理为黑色

for x in range(boardWidth):
for y in range(boardHeight):
left, top = leftTopPixelCoordOfBox(x, y)
r, g, b = paletteColors[board[x][y]]
pygame.draw.rect(tempSurf, (r, g, b, transparency), (left, top, boxSize, boxSize))
left, top = leftTopPixelCoordOfBox(0, 0)
# 为防止边缘的小格子颜色与窗口背景色相同,在整个大格子外面包一层黑色细线,厚度为1px
pygame.draw.rect(tempSurf, BLACK, (left - 1, top - 1, boxSize * boardWidth + 1, boxSize * boardHeight + 1), 1)
DISPLAY_SURF.blit(tempSurf, (0, 0))


def drawPalettes():
# 在屏幕底部绘制六个调色板
numColors = len(paletteColors)
x_margin = int((WINDOW_WIDTH - ((PALETTE_SIZE * numColors) + (PALETTE_GAP_SIZE * (numColors - 1)))) / 2)
for i in range(numColors):
left = x_margin + (i * PALETTE_SIZE) + (i * PALETTE_GAP_SIZE)
top = WINDOW_HEIGHT - PALETTE_SIZE - 10
pygame.draw.rect(DISPLAY_SURF, paletteColors[i], (left, top, PALETTE_SIZE, PALETTE_SIZE))
# 为了美观,在格子外2px再加2px厚度的对应颜色的区域
pygame.draw.rect(DISPLAY_SURF, bgColor, (left + 2, top + 2, PALETTE_SIZE - 4, PALETTE_SIZE - 4), 2)


def drawLifeMeter(currentLife):
# '生命计' 竖直放置,与上下边缘各相距20px
lifeBoxSize = int((WINDOW_HEIGHT - 40) / maxLife)

# 绘制"生命计"的背景色,'生命计'的左上角位于坐标(20,20),宽20px,高20 + (maxLife * lifeBoxSize) px
pygame.draw.rect(DISPLAY_SURF, bgColor, (20, 20, 20, 20 + (maxLife * lifeBoxSize)))

for i in range(maxLife):
if currentLife >= (maxLife - i): # 画一个实心的红色方框
pygame.draw.rect(DISPLAY_SURF, RED, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize))
# 加1px白色的边框
pygame.draw.rect(DISPLAY_SURF, WHITE, (20, 20 + (i * lifeBoxSize), 20, lifeBoxSize), 1)


def getColorOfPaletteAt(x, y):
# 返回x和y参数覆盖的调色板颜色的索引
numColors = len(paletteColors)
xmargin = int((WINDOW_WIDTH - ((PALETTE_SIZE * numColors) + (PALETTE_GAP_SIZE * (numColors - 1)))) / 2)
# 调色板底端距窗口底端10px
top = WINDOW_HEIGHT - PALETTE_SIZE - 10
for i in range(numColors):
# 六个调色板左上角的坐标:(left,top)
left = xmargin + (i * PALETTE_SIZE) + (i * PALETTE_GAP_SIZE)
r = pygame.Rect(left, top, PALETTE_SIZE, PALETTE_SIZE)
# 找出鼠标单击区域是否在任何调色板内
if r.collidepoint(x, y):
return i # 如果在,则返回所点击的调色板的序号
# 如果x和y不在任何调色板上,则返回None
return None


def floodFill(board, oldColor, newColor, x, y):
# 洪水填充算法
if oldColor == newColor or board[x][y] != oldColor:
# 如果左上角的颜色和点击颜色没有相邻,直接返回
return
board[x][y] = newColor # 更改当前格子的颜色
# 对任何相邻格子进行递归调用:
if x > 0:
floodFill(board, oldColor, newColor, x - 1, y)
if x < boardWidth - 1:
floodFill(board, oldColor, newColor, x + 1, y)
if y > 0:
floodFill(board, oldColor, newColor, x, y - 1)
if y < boardHeight - 1:
floodFill(board, oldColor, newColor, x, y + 1)


def leftTopPixelCoordOfBox(boxx, boxy):
# 返回某个小格子最左上方像素的x和y。
# 注意整个大格子位于窗口的正中央
x_margin = int((WINDOW_WIDTH - (boardWidth * boxSize)) / 2) # x_margin 为整个大格子最左边与窗口最左边的间距
y_margin = int((WINDOW_HEIGHT - (boardHeight * boxSize)) / 2) # y_margin 为整个大格子最上边与窗口最上边的间距
return boxx * boxSize + x_margin, boxy * boxSize + y_margin


if __name__ == '__main__':
main()

本文作者:LuoYing @ 小小白的笔记屋

本文链接:https://luoying.netlify.app/2021/10/10/y1s02h4v/

本文标题:游戏实战之--《ink spill》(附游戏完整源码)

本文版权:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!