2024秋软件工程个人作业(第二次)
程序实现了“羊了个羊”风格消除类小游戏的基本功能,会随机生成图案摆放顺序,要求玩家自上到下进行消消乐,具有计时器机制、排行榜功能、道具功能(简单的撤销操作)、BGM静音选项。完整代码详见github,此处针对具体功能实现的详细说明如下
任务要求汇报
github链接:https://github.com/Dmc-fur/Dmc-fur/tree/main/“羊了个羊”小程序开发
1. 游戏功能实现
- 界面设计:使用Pygame zero设计游戏界面,包括主菜单、游戏界面和结束界面。
-
实现图案的生成与分层摆放,确保图案能够被合理匹配和消除。
-
玩家通过点击选择图案并消除,当所有图案被消除时游戏结束。
-
设置倒计时机制,时间结束时游戏失败。
-
难度设置:随机生成图案摆放顺序
2. 代码要求(见下)
参见下方关于属性定义、功能代码介绍、搭建过程的叙述
3. 结合AIGC
-
安装并利用现有的AIGC应用,如VScode中的Copilot插件、Cursor IDE等,辅助完成代码。
我并未太过使用VS中的AIGC,但Bing和ChatGPT在开发过程中解答了我很多问题,ChatGPT帮助我生成了通关时展示的图片(上图“vin”)
-
AIGC表格任务
任务描述 AIGC技术优点 AIGC技术缺点 适合应用方面 不适合实现功能 我的实际使用感受 界面设计 提高设计效率,快速迭代 可能缺乏个性化和创新性 UI设计、原型生成 复杂交互设计、定制化界面设计 一般,需要精准的语言描述 图案生成与摆放 快速生成图案,减少手动劳动 可能图案单一,缺乏多样性 图形生成、布局设计 复杂图案设计、艺术创作 一般,基本没用上 玩家点击消除逻辑 快速实现基本逻辑 可能逻辑简单,不够灵活 游戏逻辑编写、简单交互 高度复杂逻辑、AI对战系统 优秀,这方面解答了我很多疑问 倒计时机制 易于实现时间控制功能 可能需要手动调整时间参数 计时器、进度条 复杂时间控制、多条件计时 优秀,倒计时主体代码基本由AI生成,插入程序中能直接运行 难度设置 快速生成不同难度的关卡 可能难度控制不够精细 关卡设计、难度调整 动态难度调整、玩家行为分析 未利用 代码生成 提高编码效率,减少重复劳动 可能生成的代码需要人工优化和调整 代码编写、模板生成 复杂算法实现、系统优化 优秀,解答了很多问题,甚至可以说pygame就是利用AI学的 代码结构与注释 需要人工进行结构设计和注释 - 代码维护、文档编写 代码生成、自动注释 有帮助
4.PSP表格
任务描述 | 预估耗时(小时) | 实际耗时(小时) | 完成情况评价 |
---|---|---|---|
界面设计 | 2 | 3 | 设计耗时比预期长,需要提高设计效率 |
图案生成与摆放 | 3 | 2 | 完成效率高,图案生成工具选择得当 |
玩家点击消除逻辑 | 4 | 5 | 逻辑实现复杂,需要更多时间调试 |
倒计时机制 | 2 | 2 | 功能实现顺利,代码复用性好 |
难度设置 | 1 | 1 | 难度控制实现简单,效果符合预期 |
代码生成与优化 | 5 | 4 | 代码生成工具辅助效果好,优化时间减少 |
排行榜功能 | 2 | 3 | 数据存储和读取需要进一步优化 |
撤销操作实现 | 2 | 2 | 功能实现符合预期,交互体验良好 |
音乐和音效设置 | 1 | 1 | 音效设置简单,满足基本需求 |
5.个人作业自评
完成过程:
- 做得好的地方:
- 图案生成与摆放任务完成效率高,工具选择得当。
- 倒计时机制和难度设置的实现顺利,符合预期。
- 撤销操作实现符合预期,交互体验良好。
- 改进之处:
- 界面设计耗时比预期长,需要提高设计效率。
- 玩家点击消除逻辑实现复杂,需要更多时间调试。
- 排行榜功能的数据存储和读取需要进一步优化。
- 整合与测试阶段发现多个bug,需要更多时间修复。
最终效果:
- 游戏基本功能实现完整,包括界面设计、游戏逻辑、倒计时机制、难度设置、排行榜功能和撤销操作。
- 音效和音乐设置简单,满足基本需求。
- 整体游戏体验良好,但存在一些需要优化的细节。
改进建议
- 提高界面设计效率,可能通过使用更高效的设计工具或方法。
- 对玩家点击消除逻辑进行进一步优化和简化。
- 对排行榜功能的数据存储和读取进行优化,提高性能。
- 在整合与测试阶段投入更多时间,确保游戏稳定性。
通过这次作业,我学到了很多关于游戏开发的知识,尤其是在使用AIGC工具辅助开发方面。未来我将更加注重效率和代码质量,以实现更好的开发成果。
定义相关属性
本程序使用Pygame zero作为图形库设计游戏界面,该图形库从Pygame库衍生而来,可以说Pgzero就是Pygame的一个精简版本。
Pgzero能够实现Pygame库的大多数功能,屏蔽了一些复杂的细节,内嵌许多便于交互的函数,方便开发。使用此框架导致代码看起来与一般的代码稍有不同,会有很多未定义的方法和变量,故在一些编辑器里会报错。但其实是可以运行的,无需手动增加 import。
学习中主要参考了:https://zhuanlan.zhihu.com/p/219699040 https://pygame-zero.readthedocs.io/zh-cn/latest/index.html
首先执行pip install pgzero安装pygame zero。此后:
定义该程序所需的头文件
import pgzrun
import pygame
import random
import math
import os
import time
定义游戏的基本常量
在pgzero中:WIDTH和HEGIHT是预设的两个常量,分别用来表示程序窗口的宽度和高度值(单位为像素)
其中 T_WIDTH 和 T_HEIGHT 是游戏中被消除卡牌的尺寸属性。这些卡牌尺寸各不相同,我使用kimi AI得到了解决方案,利用picresize.com提供的图片批量修改解决此问题。
TITLE = '回来吧牢羊'
WIDTH = 600
HEIGHT = 720
T_WIDTH = 60
T_HEIGHT = 66
功能代码介绍
计时器机制
利用clock.schedule_interval(update_timer, 1.0)达成定时更新。update_timer 函数会每秒被调用一次,计时器的时间会自动更新,而不需要依赖鼠标点击事件。
# 计时器前序定义
start_time = None
time_limit = 301 # 5分钟
# 计时器在def draw():中的绘制
elapsed_time = time.time() - start_time
remaining_time = max(0, time_limit - elapsed_time)
screen.draw.text(f'Time: {int(remaining_time)}', (25, 25), color='red', fontsize=32)
#fontsize是字体大小参数
#计时器功能实现函数
def update_timer():
global game_state
if game_state == 'playing':
elapsed_time = time.time() - start_time
remaining_time = max(0, time_limit - elapsed_time)
if remaining_time <= 0:
game_state = 'gameover'
clock.schedule_interval(update_timer, 1.0) # 每1.0秒调用一次 update_timer
简单的撤销操作
利用记录中间变量的方式实现简单的撤销操作,能够将上一个抓取的(未成3消去的)元素直接放回原位,空出位置选择新的元素
# 初始化一个变量来存储被移除的元素
last_removed_tiles = None
#储存上一个元素
def remove_tile(tile):
global last_removed_tile, last_removed_tile_pos,last_below_tile
last_removed_tile = tile
last_removed_tile_pos = tile.pos
#恢复上一个元素
def restore_last_removed_tile():
global last_removed_tile, last_removed_tile_pos,last_below_tile,flag
if last_removed_tile:
docks.remove(last_removed_tile)
last_removed_tile.pos = last_removed_tile_pos
last_removed_tile.status = 1 #保证上一个移除的元素返回后可以点击
tiles.append(last_removed_tile)
last_removed_tile = None
last_removed_tile_pos = None
last_below_tile = None
#在def on_mouse_down(pos):的game_state == 'playing'分支中加入储存行动,即remove(tile)的部分
for tile in reversed(tiles):
if tile.status == 1 and tile.collidepoint(pos):
# 状态1可点,并且鼠标在范围内
tile.status = 2
tiles.remove(tile)
diff = [t for t in docks if t.tag != tile.tag] # 获取牌堆内不相同的牌
if len(docks) - len(diff) < 2: # 如果相同的牌数量<2,就加进牌堆
remove_tile(tile) #储存这个加进牌堆的东西
docks.append(tile)
else: # 否则用不相同的牌替换牌堆(即消除相同的牌)
#remove_tile(None) #发生了消去操作,不应回退。但这个代码会导致消去时程序崩溃,因而消去时不能按返回按键
docks = diff
#在def on_mouse_down(pos):的game_state == 'playing'分支中加入撤销行动
if undo_button.collidepoint(pos):
restore_last_removed_tile()
排行榜功能
加入排行榜功能,可以显示从小到大排序的历史记录表,记录玩家的最快速度!
# 基本元素
leaderboard = []
completion_times = []
completion_time = 0
#在def draw():中加入排行榜界面
elif game_state == 'leaderboard':
screen.clear()
screen.draw.text('Leaderboard', (WIDTH // 2 - 75, 50), color='white', fontsize=40)
for i, record in enumerate(sorted(completion_times)):
screen.draw.text(f'{i + 1}. {record:.2f} seconds', (WIDTH // 2 - 75, 100 + i * 30), color='white',fontsize=30)
screen.draw.filled_rect(return_button, 'white')
#在def on_mouse_down(pos):中加入速度登记
elif game_state == 'victory':
completion_time = time.time() - start_time
completion_times.append(completion_time)
if pos:
reset_game()
实际搭建过程
初始化游戏内容,搭建游戏交互页面,播放音乐
在按钮设计中:定义了一个start_button矩形,并在draw函数中绘制了按钮背景和文字。
要修改start_button的位置和大小,可以调整定义按钮时传入的参数,即start_button = Rect((新位置_x, 新位置_y), (新宽度, 新高度))
^: 以start_button = Rect((WIDTH // 2 - 100, HEIGHT // 2 + 100), (200, 100)) 为例: WIDTH // 2 - 100表示按钮的左上角的x坐标。WIDTH // 2表示屏幕宽度的一半,即屏幕的中心位置。减去100是为了将按钮向左移动100个像素,使按钮的中心对齐屏幕的中心。 HEIGHT // 2 + 100:表示按钮的左上角的y坐标。HEIGHT // 2表示屏幕高度的一半,即屏幕的中心位置。 + 100 是为了将按钮向下移动100个像素
在此后的on_mouse_down函数中,我们将检测按钮的点击事件,并在点击按钮时将游戏状态切换为playing
# 下方牌堆的位置
DOCK = Rect((90, 564), (T_WIDTH*7, T_HEIGHT))
# 上方的所有牌
tiles = []
# 牌堆里的牌
docks = []
# 初始化牌组,12*12张牌随机打乱
ts = list(range(1, 13))*12
random.shuffle(ts)
n = 0
music.play('misty memoty_piano version')
for k in range(7): # 7层
for i in range(7-k): #每层减1行
for j in range(7-k):
t = ts[n] #获取排种类
n += 1
tile = Actor(f'tile{t}') #使用tileX图片创建Actor对象
tile.pos = 120 + (k * 0.5 + j) * tile.width, 100 + (k * 0.5 + i) * tile.height * 0.9 #设定位置
tile.tag = t #记录种类
tile.layer = k #记录层级
tile.status = 1 if k == 6 else 0
tiles.append(tile)
for i in range(4): #剩余的4张牌放下面(保证通关并给予容错)
t = ts[n]
n += 1
tile = Actor(f'tile{t}')
tile.pos = 210 + i * tile.width, 516
tile.tag = t
tile.layer = 0
tile.status = 1
tiles.append(tile)
搭建游戏绘制函数
def draw():
screen.clear()
if game_state == 'start':
screen.blit('start_screen', (0, 0)) # 绘制开始页面背景
screen.draw.filled_rect(start_button, 'white') # 绘制按钮背景
screen.draw.text('START', center=start_button.center, color='black') # 绘制按钮文字
screen.draw.filled_rect(mute_button, 'white') # 绘制静音按钮背景
screen.draw.text('MUTE' if not is_muted else 'UNMUTE', center=mute_button.center, color='black') # 绘制静音按钮文字
screen.draw.filled_rect(leaderboard_button, 'white') # 绘制排行榜按钮背景
screen.draw.text('LEADERBOARD', center=leaderboard_button.center, color='black') # 绘制排行榜按钮文字
elif game_state == 'playing':
screen.blit('back', (0, 0)) #背景图
undo_button = Rect((WIDTH - 100, HEIGHT - 50), (80, 30)) #撤销按钮
screen.draw.filled_rect(undo_button, 'white')
screen.draw.text('REMOVE', center=undo_button.center, color='gray')
for tile in tiles:
#绘制上方牌组
tile.draw()
if tile.status == 0:
screen.blit('mask', tile.topleft) #给不可点击的块添加遮罩
for i, tile in enumerate(docks):
#绘制排队,先调整一下位置(因为可能有牌被消掉)
tile.left = (DOCK.x + i * T_WIDTH)
tile.top = DOCK.y
tile.draw()
# 绘制计时器
elapsed_time = time.time() - start_time
remaining_time = max(0, time_limit - elapsed_time)
screen.draw.text(f'Time: {int(remaining_time)}', (25, 25), color='red', fontsize=32)#fontsize是字体大小参数
# 超过7张,失败
if len(docks) >= 7 or remaining_time <= 0:
screen.blit('gameover', (0, 0))
screen.draw.filled_rect(return_button, 'white') # 绘制返回按钮背景
screen.draw.text('Click twice to return', center=return_button.center, color='black') # 绘制返回按钮文字
elif game_state == 'victory':
screen.blit('win', (0, 0))
screen.draw.filled_rect(return_button, 'white') # 绘制返回按钮背景
screen.draw.text('Congrets!!\nClick twice to return', center=return_button.center, color='blue')
elif game_state == 'leaderboard': #可以显示从小到大排序的历史记录表,记录玩家的最快速度
screen.clear()
screen.draw.text('Leaderboard', (WIDTH // 2 - 75, 50), color='white', fontsize=40)
for i, record in enumerate(sorted(completion_times)):
screen.draw.text(f'{i + 1}. {record:.2f} seconds', (WIDTH // 2 - 75, 100 + i * 30), color='white',fontsize=30)
screen.draw.filled_rect(return_button, 'white') # 绘制返回按钮背景
screen.draw.text('RETURN', center=return_button.center, color='black') # 绘制返回按钮文字
screen.draw.text
对胜利与否做出判断
# 超过7张,失败
if len(docks) >= 7:
screen.blit('gameover', (0, 0))
# 没有剩牌,胜利
if len(tiles) == 0:
screen.blit('win', (0, 0))
游戏主体函数
def on_mouse_down(pos):
global docks, game_state, is_muted, start_time
if game_state == 'start':
if start_button.collidepoint(pos):
reset_game()
game_state = 'playing' # 点击开始按钮时,进入游戏
start_time = time.time() # 开始计时
elif mute_button.collidepoint(pos):
is_muted = not is_muted
if is_muted:
music.pause()
else:
music.unpause()
elif leaderboard_button.collidepoint(pos):
game_state = 'leaderboard'
return
elif game_state == 'playing':
undo_button = Rect((WIDTH - 100, HEIGHT - 50), (80, 30))
if undo_button.collidepoint(pos): #加入撤销功能
restore_last_removed_tile()
elif len(tiles) == 0: # 游戏结束,成功
game_state == 'victory'
return
elif len(docks) >= 7: ##游戏失败. remaining_time<=0的情况不需要考虑,因为每时每刻都在自动更新
game_state = 'gameover'
return
for tile in reversed(tiles): # 逆序循环是为了先判断上方的牌,如果点击了就直接跳出,避免重复判定
if tile.status == 1 and tile.collidepoint(pos):
# 状态1可点,并且鼠标在范围内
tile.status = 2
tiles.remove(tile)
diff = [t for t in docks if t.tag != tile.tag] # 获取牌堆内不相同的牌
if len(docks) - len(diff) < 2: # 如果相同的牌数量<2,就加进牌堆
remove_tile(tile) #储存这个加进牌堆的东西
docks.append(tile)
else: # 否则用不相同的牌替换牌堆(即消除相同的牌)
remove_tile(None) #因为发生了消去操作,所以不允许返回
docks = diff
for down in tiles: # 遍历所有的牌
if down.layer == tile.layer - 1 and down.colliderect(tile): # 如果在此牌的下一层,并且有重叠
for up in tiles: # 那就再反过来判断这种被覆盖的牌,是否还有其他牌覆盖它
if up.layer == down.layer + 1 and up.colliderect(down): # 如果有就跳出
break
else: # 如果全都没有,说明它变成了可点状态
down.status = 1
return
elif game_state == 'leaderboard':
if return_button.collidepoint(pos):
game_state = 'start'
elif game_state == 'gameover':
if pos: #这里不知道为什么要点击两次才能返回,于是直接把判定范围改成了全屏,这样点按两次也能够自然返回
reset_game()
elif game_state == 'victory':
completion_time = time.time() - start_time
completion_times.append(completion_time)
if pos:
reset_game()
文件尾
pgzrun.go()