写在前: 本小游戏只实现了消灭星星的基本功能,包括消除星星、下落、分数更新,以及两个功能按钮:重新开始和炸弹。
游戏展示:
界面设计结构图:
主要函数说明:
初始化函数(init_stars)
按钮点击函数(on_button_press)
消灭星星函数(eliminate_stars)
移动方块函数(move_blocks_down)
代码实现
1.初始化界面
a.设置界面背景颜色以及常量ROWS,COLS
Window.size = 500,650
Window.clearcolor=get_color_from_hex('#00023A') #设置背景颜色
ROWS = 10
COLS = 10
b.定义Scene类,用来设计界面。
定义初始化函数
class Scene(BoxLayout):
def __init__(self): #初始化变量
super().__init__()
self.image=[
"",
'./picture/1.png',
'./picture/2.png',
'./picture/3.png',
'./picture/4.png',
'./picture/5.png'
]
self.orientation = 'vertical'
self.spacing=3
self.matrix_stars = [] #存储按钮
self.score = 0 # 计分
self.high_score_file = 'E:/high_score.txt' #记录最高分
if not os.path.exists(self.high_score_file):
with open(self.high_score_file, 'w') as f:
f.write('0')
self.high = self.get_high_score(self.high_score_file)
self.n = 0 # 计算相连个数
self.flag = 0 # 没有相同颜色相连的标志
self.residual_sum = 0 # 剩余星星个数
self.residual_score = 0 # 剩余星星奖励分数
self.f = 0 # 标记效果加分是否成功
self.blabel=False #记录炸弹按钮按下的状态
self.boom_1=0 #炸弹按钮按下次数
self.count_old=0 #记录空列数
self.init_stars() #初始化按钮
self.init_matrix() #初始化矩阵
self.dcolor = [[0 for _ in range(COLS)] for _ in range(ROWS)]
self.matrix0 = [[0 for _ in range(COLS)] for _ in range(ROWS)]
c.初始化界面(init_stars函数)
界面上方放置重新开始按钮、炸弹按钮和分数标签
def init_stars(self): #初始化stars按钮矩阵
self.clear_widgets()
#上方放置功能键和积分
button_layout = FloatLayout()
self.add_widget(button_layout)
# 重新开始
restart_button = Button(text='重新开始', size_hint=(.2, .6),
font_name='msyh.ttf', pos_hint={'x': 0.1, 'y': .2})
restart_button.bind(on_press=self.restart_game)
button_layout.add_widget(restart_button)
重新开始按钮绑定了restart_button点击事件
#炸弹按钮
self.boom_button = Button(text='炸弹', size_hint=(.2, .6),background_color=(1,1,1,.5),
background_normal="",font_name='msyh.ttf', pos_hint={'x': 0.32, 'y': .2},
disabled=True) # 初始时不能点击按钮
button_layout.add_widget(self.boom_button)
self.boom_button.bind(on_press=self.boom_star)
self.boom_button.bind(on_release=self.boom_touch_up)
炸弹按钮绑定了boom_star点击事件以及boom_touch_up释放事件。
#分数按钮
self.score_label = Label(text='Score: {}'.format(self.score), color=(1, 1, 1),
pos_hint={'x': 0.3, 'y': 0},font_size=30, )
self.add_widget(self.score_label)
self.highscore_label = Label(size_hint=(1,.2),text='Highest: {}'.format(self.high), color=(1, 1, 1, .5),
pos_hint={'x': 0.3, 'y': .4}, font_size=20)
self.add_widget(self.highscore_label)
self.small_label = Label(size_hint=(1,.5), color=(0, 0, 1, 1),
pos_hint={'x': 0, 'y': 2}, font_size=20,
text='{} eliminate, score+ {}'.format(self.n, self.n * self.n))
self.add_widget(self.small_label)
#最后游戏结束的提示标签
self.reward_label = Label(text='{} star remaining, reward {} points'.\
format(self.residual_sum, self.residual_score),
color=(1, 0, 0, 1), pos_hint={'x': 0, 'y': 3}, font_size=30)
self.add_widget(self.reward_label)
下方添加10*10的按钮矩阵来代表星星,使用self.matrix_stars数组来存储所有按钮
self.matrix_stars = [] #按钮置空
# 创建按钮矩阵
for i in range(ROWS): # 每行的按钮
row_layout = BoxLayout(orientation='horizontal', spacing=3)
self.add_widget(row_layout)
row_buttons = [] # 记录每行的方块
for j in range(COLS): # 每列的按钮
btn = Button(size=(80, 80), background_down='',background_normal='')
btn.bind(on_press=self.on_button_press) # 绑定点击事件
row_layout.add_widget(btn) # 行布局中添加按钮
row_buttons.append(btn)
self.matrix_stars.append(row_buttons) # 将行方块添加到矩阵中
self.update_score() #更新分数
设置星星按钮点击时无图像使用background_down='',以及默认无背景图像background_normal='',每个按钮都绑定on_button_press事件。
d.初始化颜色矩阵(init_matrix函数)
设置每个方块的颜色随机,同时限制每种颜色的星星数量不超过一半
def init_matrix(self): #初始化颜色矩阵
# 存储每个按钮的颜色
self.matrix = [[random.randint(1, len(self.image)-1 )
for _ in range(COLS)] for _ in range(ROWS)] #数字0-5
# 每种颜色的星星数量不超过一半
max_count = ROWS * COLS // 2
for _ in range(max_count):
# 保证每种颜色的星星数量不超过一半
color = random.randint(1, len(self.image)-1 )
count = sum(row.count(color) for row in self.matrix) #该颜色在矩阵中的数量
while count >= max_count: #若数量超过一半--->重新生成一个颜色
color = random.randint(1, len(self.image)-1 )
count = sum(row.count(color) for row in self.matrix)
# 随机生成星星的位置
row = random.randint(0, ROWS - 1)
col = random.randint(0, COLS - 1)
# 将随机位置的星星颜色设置为指定颜色
self.matrix[row][col] = color
将每个星星方块的颜色添加到按钮的背景图像中,使用到background_normal属性。
#更新按钮背景图像
for i in range(ROWS):
for j in range(COLS):
c = self.matrix[i][j]
self.matrix_stars[i][j].background_normal=self.image[c] #背景图片
e.更新按钮背景颜色作为一个函数,方块后续调用
def update_matrix(self,dt):#更新按钮颜色
self.init_stars()
for i in range(ROWS):
for j in range(COLS):
c = self.matrix[i][j]
self.matrix_stars[i][j].background_normal=self.image[c] #背景图片
if c==0: #变透明
self.matrix_stars[i][j].background_color=(0,0,0,0)
2.星星按钮点击事件
a.绑定按钮点击事件(on_button_press函数)
如果点击了炸弹:
def on_button_press(self, instance):
#如果点击了炸弹
if self.blabel==True:
self.boom_1=1 #炸弹按钮按下一次
for i in range(ROWS):
for j in range(COLS):
if instance == self.matrix_stars[i][j]:
for x in range(ROWS):
self.matrix[x][j]=0 #消除行
for y in range(COLS):
self.matrix[i][y]=0 #消除列
break
self.move_blocks_down() #移动按钮,下落
Clock.schedule_once(self.update_matrix, .5) #更新颜色
self.blabel=False
设置Clock.schedule_once()是为了在方块下落的动画完成后再进行下一步操作。
如果是正常点击:进行消除星星、移动按钮以及更新矩阵,同时还要判断是否是最后的游戏结束状态
else:#正常点击
# 获取被点击按钮的位置
for i in range(ROWS):
for j in range(COLS):
if instance == self.matrix_stars[i][j]:
# 检查并消除星星
self.eliminate_stars(i, j)
# 移动方块
self.move_blocks_down()
Clock.schedule_once(self.update_matrix, .5) #0.5秒后再更新,即等动画完成后
self.different_stars()
return
b.消灭星星函数(eliminate_stars)
使用深度优先遍历点击方块的上下左右位置,找到所有连续的相同颜色的方块,将它们的颜色置为0,即空方块。还需要判断同色方块数是否大于一,如果只有一个方块则不进行消除。
def eliminate_stars(self, row, col): #消灭星星
target_color = self.matrix[row][col]
count = [0] #记录相同方块的个数
#dfs深度优先算法寻找连续的相同颜色的星星
def dfs(r, c):
if (r < 0 or r >= ROWS or c < 0 or c >= COLS #越界
or self.matrix[r][c] != target_color or self.matrix[r][c] == 0):
return
count[0] += 1 # 更新列表中的计数器值
if count[0] > 1: #相同颜色的方块数大于1
self.matrix[r][c] = 0
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
dfs(row, col)
if (count[0] - 1) <= 0:
self.n = 0
else:
self.n = count[0] - 1
c.移动方块函数(move_blocks_down)
①下移方块:
即当方块消除后变为0,而上方还有非0的方块,需要将上方的非0方块进行下移,同时制作动画效果,模拟方块下落的过程。算法过程如下图所示:当第j列中间存在0值时,从底向上遍历,找到0值的位置x,再次向上找到非0值的方块,记录下此时的0值方块数n=2,将,x-2的方块移动x位置上,将x-2-1的方块移动到x-1位置上。实际上是两个按钮的交换。
def move_blocks_down(self): #移动星星
# 从底部往上遍历每列,将空位上面的方块往下移动
diss = self.matrix_stars[1][0].y - self.matrix_stars[2][0].y #纵距离
for j in range(COLS): #遍历每一列
for i in range(ROWS - 1, -1, -1): #从底向上遍历
x=i #此时行标
if self.matrix[i][j] == 0:
n=1 #记录空格
self.matrix_stars[i][j].background_color=(1,1,1,0) #颜色变透明
(这里让变为0的方块变透明,是为了在有色方块下落时能够显示出来)
如果找到了非空方块,则将上面的所有非空方块移动到下面的空方块位置,由于已经记录了空方块个数,设置动画让按钮移动相应的距离(空方块数*单个方块高度)
for k in range(i - 1, -1, -1): #该行向上
if self.matrix[k][j] != 0:
#改变按钮坐标
anim= Animation(y=self.matrix_stars[k][j].y-n*diss, duration=.5)
anim.start(self.matrix_stars[k][j])
按钮进行动画下落后,还需要更新self.matrix_stars和self.matrix存储的值,实现真正的交换
#交换按钮
self.matrix_stars[k][j],self.matrix_stars[k+n][j]=self.matrix_stars[k+n][j],self.matrix_stars[k][j]
# 交换颜色矩阵
self.matrix[k+n][j] = self.matrix[k][j]
self.matrix[k][j] = 0
如果没有找到非空方块,证明该位置仍然是空方块,记录空方块数n+1,让改按钮变透明,同时继续向上遍历
else: #=0
n+=1
self.matrix_stars[k][j].background_color = (1, 1, 1, 0) # 颜色变透明
x -= 1 # 移动的行标向上
②左移方块
当方块中存在某列全为空的情况,就会触发左移。需要先找到空列的位置,再向右继续遍历直到找到非空列,同时记录下空列的个数count,将右边的非空列按顺序移动到左边的空列位置上,注意是整列的移动。算法过程如下图所示:图中有2个空列,即count=2,b为第一个空列的索引,从左往右遍历,找到空列b,将b+count列移至b列位置上,将b+count+1列移动至b+1列位置上,实现了空列的向左补齐。实际上还是按钮的交换。
遍历每列,计算每列的空列数到lie数组中,如果找到全为空的列(空方块数=行数)则计数count+1,需要注意已经移动到右边的空列不再进行统计,所以要用count_old变量记录。
#从右往左移 整列移动
count=0 #统计空列数
lie=[] #每列的空格数
for j in range(COLS):
n=0
for i in range(ROWS):
if self.matrix[i][j]==0: #统计j列空值
n+=1
lie.append(n)
for j in range(COLS-self.count_old): #已经移动的列不算
if lie[j]==ROWS:#整列为空
count+=1
self.count_old+=count #已经移动的空列数
从左往右遍历每列,如果找到空列,记该列为j,从上往下遍历每行,遍历j列后面的列b,找到非空列(b+count),将b+count列左移至b列位置上,设置动画,让方块移动相应的距离,同时交换两列的按钮和颜色矩阵。
diss = self.matrix_stars[1][1].x - self.matrix_stars[1][0].x #横距离
for j in range(COLS-1):
#存在空列
if lie[j]==ROWS: #空格数=行数
for a in range(ROWS):
for b in range(j,COLS-count): #将空列右边的列左移
self.matrix_stars[a][b].background_color = (1, 1, 1, 0) # 颜色变透明
#移动
anim = Animation(x=self.matrix_stars[a][b+count].x-diss*count, duration=.5)
anim.start(self.matrix_stars[a][b+count])
# 交换按钮
self.matrix_stars[a][b], self.matrix_stars[a][b+count] = self.matrix_stars[a][b+count], \
self.matrix_stars[a][b]
#交换颜色矩阵
self.matrix[a][b],self.matrix[a][b+count]=self.matrix[a][b+count],self.matrix[a][b]
# self.matrix[a][COLS-1]=0 #最后一列置为0
break #找到一次非空列,就将所有列进行了移动,退出循环
d.更新方块颜色函数(update_matrix)
方块完成移动后在相应的矩阵中(self.matrix)已经更新了值,需要根据更新的值重新设置每个按钮的背景图像。首先需要重新初始化界面,再遍历每个按钮进行设置。
def update_matrix(self,dt):#更新按钮颜色
self.init_stars()
for i in range(ROWS):
for j in range(COLS):
c = self.matrix[i][j]
self.matrix_stars[i][j].background_normal=self.image[c] #背景图片
if c==0: #变透明
self.matrix_stars[i][j].background_color=(0,0,0,0)
e.判断游戏结束函数(different_stars)
游戏结束的判断方法为:界面中没有相连的相同颜色的星星
再次使用dfs函数遍历每个方块查看相连方块的颜色状态
def different_stars(self):
for i in range(ROWS): #临时保存
for j in range(COLS):
self.matrix0[i][j] = self.matrix[i][j]
for i in range(ROWS):
for j in range(COLS):
target_color = self.matrix[i][j]
count = [0] # 使用列表来保存计数器
def dfs(r, c):
if (r < 0 or r >= ROWS or c < 0 or c >= COLS
or self.matrix[r][c] != target_color or self.matrix[r][c] == 0):
return
count[0] += 1 # 更新列表中的计数器值
if count[0] > 1:
self.matrix[r][c] = 0
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
dfs(i, j)
将方块的颜色状态改为0和1,即如果没有相连的同色方块,则状态为1,否则为0
if (count[0] - 1) <= 0:
count[0] = 0
else:
count[0] = count[0] - 1
if count[0] == 0:
self.dcolor[i][j] = 1
else:
self.dcolor[i][j] = 0
统计所有方块的颜色状态和,如果总数为100,即所有方块都没有相连的颜色,代表游戏结束。
sum = 0
for i in range(ROWS):
for j in range(COLS):
sum += self.dcolor[i][j]
if sum == COLS * ROWS:
self.flag = 1
else:
self.flag = 0
for i in range(ROWS):
for j in range(COLS):
self.matrix[i][j] = self.matrix0[i][j]
self.highscore_label.text = 'Highest: {}'.format(self.high)
3.更新分数
当每次点击了方块进行消除后,都会有相应的得分,需要加入到总分标签中,计算公式为消灭星星的方块数n的平方,这样每次消灭的方块数越多,就可以产生越高的得分。
①定义更新得分函数update_score
def update_score(self): # 更新计分
# 分数标签
self.score += self.n * self.n
self.score_label.text = 'Score: {}'.format(self.score)
self.high = self.get_high_score(self.high_score_file)
self.reward_label.text = ''
首先记录每次消除的分数到small_label标签中
# 小分标签
if self.n == 0:
self.small_label.text = ''
else:
self.small_label.text = '{} eliminate, score+{}'.format(self.n, self.n * self.n)
如果已经没有相同颜色的相连方块,则代表游戏结束,需要更新游戏结束的状态,同时在界面中显示游戏结束的提示词,记录到reward_label中
# 历史分数标签
if self.flag == 1:
self.residual_stars()
if self.f == 1:
self.score += self.residual_score
self.reward_label.text = '{} star remaining, reward {} points'.format(self.residual_sum,
self.residual_score)
self.f += 1
else:
self.f += 1
如果此次的游戏总分大于历史最高分,则需要更新最高分标签highscore_label
if self.score > self.high:
self.update_high_score(self.score, self.high_score_file)
self.high = self.get_high_score(self.high_score_file)
self.highscore_label.text = 'Highest: {}'.format(self.high)
else:
self.highscore_label.text = 'Highest: {}'.format(self.high)
elif self.flag == 0:
self.highscore_label.text = 'Highest: {}'.format(self.high)
同时,在更新分数时,要判断总分是否超过200,若超过200分即可开启炸弹按钮
# 炸弹按钮
if self.score > 200 and self.boom_1 == 0 and self.flag == 0: # 大于200分且没按过按钮且不是最后状态可以开启炸弹按钮
self.boom_button.disabled = False
②更新游戏状态函数(residual_stars),主要是用来记录最后的剩余星星数,如果剩余数小于10,则可以进行加分,否则不加分。
def residual_stars(self): #剩余星星加分
self.f+=1
for i in range(ROWS):
for j in range(COLS):
if self.matrix[i][j]!=0:
self.residual_sum+=1
if self.residual_sum<10:
self.residual_score=(10-self.residual_sum)*10
else:
self.residual_score=0
③读取最高分函数(get_high_score)
读取文件中的最高分
def get_high_score(self, high_score_file): # 读取最高分
with open(high_score_file, 'r') as f:
return int(f.read())
④更新最高分函数(update_high_score)
向最高分文件中写入当前最高分
def update_high_score(self, score, high_score_file): #更新最高分
with open(high_score_file, 'w') as f:
f.write(str(score))
4.功能按钮
①重新开始游戏(restart_game函数)
重玩按钮绑定了重新开始函数,需要将所有变量置为0,重新初始化界面
def restart_game(self,instance):#重新开始
self.score = 0
self.residual_score = 0
self.residual_sum = 0
self.f = 0
self.n=0
self.flag = 0
self.high = self.get_high_score(self.high_score_file)
self.count_old = 0 # 记录空列数
self.small_label.text = ''
self.reward_label.text = ''
self.boom_button.disabled = True # 按钮不可按
self.boom_1 = 0
self.init_stars() # 重绘画布
self.init_matrix()
②炸弹功能
首先设置了炸弹按钮不可点击,需要达到一定的条件才可开启(在init_star函数中已经设置)。
炸弹按钮绑定事件相应函数(boom_star函数),将此时的blabel设置为True,表示开启了炸弹功能
def boom_star(self,instance): #炸弹功能
self.blabel=True
炸弹按钮绑定释放按钮事件(boom_touch_up函数),将炸弹按钮设置为红色,提示玩家此时点击了炸弹按钮
def boom_touch_up(self,instance):#点击后变换按钮颜色
instance.background_color = (1, 0, 0, 1) # 设置为红色
在星星按钮点击事件中添加炸弹功能:如果点击了炸弹按钮,则消除点击按钮的同行同列按钮,接着移动方块和更新矩阵。(在on_press函数中已经设置)
5.App类调用
class StarApp(App):
def build(self):
return Scene()
StarApp().run()
程序运行结果
①主界面
②消灭星星
点击相同颜色的方块可进行消灭星星,同时会显示得分,星星可下移和左移:
③炸弹按钮
当分数大于200分,炸弹按钮会开启
点击后按钮变为红色,再点击星星方块可消除同行同列:
④游戏结束
当界面中没有相连的同色方块时,游戏结束,上方显示剩余方块以及奖励分数:
⑤重新开始
游戏结束或游戏中途,都能点击重新开始游戏开启新一局的游戏,星星方块重新随机分布,分数会重新清零:
总结
该小游戏是移动平台设计与开发课程的期末项目,历时几个周完成,时间比较紧张,所以还有很多地方没有完善,除了炸弹按钮,还可以设计出其他的辅助性道具来增加游戏的趣味性;另外一轮游戏结束后界面只会进行提示,玩家必须手动点击重新开始才能开始新的游戏,可以设计不同的关卡来提升游戏的难度。
事实上,在游戏测试中左移方块有问题T_T,当空的列是间隔的而不是连续时(使用炸弹会出现此问题)没办法正确左移,还需要改进。另外使用dfs算法遍历寻找相同颜色的方块时间复杂度较大,可以考虑使用bfs算法。
最后,由于Kivy打包过程中遇到了一些问题,并没有成功打包成手机程序,会出现闪退问题,可能是版本不兼容问题。