• 完整数字华容道04:游戏主体逻辑



    本节涉及到的内容比较多,而且是在 简版华容道基础上修改的,设计思路和算法请看简版华容道,本节只讲改进部分, 请一定要认真仔细阅读。

    1、整体框架修改

    在做页面切换的时候遇到了问题:MainForm 为 QWidget ,设置一个 Layout 简单,给它切换 Layout 后第一个 Layout并没有消失,导致两个 Layout 重叠。

    修改方案:HomePage 和 GamePage 继承自 QWidget,MainForm 中定义 QStackedLayout 属性 sltMain,sltMain 添加 HomePage 和 GamePage ,可以方便切换和删除。

    2、页面框架

    游戏页面框架如下图所示:

    01_页面框架

    3、Switch 自定义控件

    背景音乐开关是一个 Switch Button,但是QT没有该控件,所以需要自己实现,该部分参考了网上的资料:https://www.cnblogs.com/zhangxuan/p/9152513.html。

    大致思路就是:如果点击该控件,启动定时器,滑块向另一方向移动直到边缘,然后设置颜色和显示的文字。实现的效果如图所示:

    02_switchButton

    4、GamePage 实现

    GamePage 是该游戏比较重量级的部分,逻辑也是最复杂的。

    4.1 新增属性

    GamePage 类中要添加几个属性:

    • 游戏等级:设置游戏的等级,也就是行和列中数字方块的个数;
    • 游戏时间:记录游戏所用时间;
    • 游戏步数:记录游戏所走步数;
    • 方块边长:间隔已经固定,根据游戏等级计算方块的边长;
    self.level = level  # 游戏等级
    self.time = 0       # 游戏时间
    self.steps = 0      # 游戏步数
    self.spacing = 10   # 数字方块的间隔
    self.blockWidth = (380 - self.spacing * (self.level + 1)) / self.level  # 计算方块的边长
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.2 初始化布局

    该页面采用的布局如下图所示:

    03_布局结构

    代码实现:

    def initUI(self): 
            # 启动定时器
            self.timer.start(1000)
            self.timer.timeout.connect(self.updateTime)
    
            font = QFont('Microsoft YaHei')
            font.setPointSize(20)
            # 返回按钮
            self.btnBack = QPushButton()
            pic_dir = os.path.abspath('.') + '\\src\\images\\back.png'
            self.btnBack.setFixedSize(40, 40)
            self.btnBack.setIcon(QIcon(pic_dir))
            self.btnBack.setIconSize(QSize(40, 40))
            self.btnBack.clicked.connect(self.back)
    
            # switch 按钮连接槽
            self.swithcBtn.checkedChanged.connect(self.musicControl)
    
            # 音乐
            lbMusic = QLabel("音乐")
            lbMusic.setFont(font)
            self.swithcBtn.setFixedSize(80, 40)
    
            self.hltTop = QHBoxLayout()
            self.hltTop.addWidget(self.btnBack)
            self.hltTop.addStretch()
            self.hltTop.addWidget(lbMusic)
            self.hltTop.addWidget(self.swithcBtn)
    
            # 记录步数和时间
            self.lbTime = QLabel('时间:{}s'.format(self.time))
            self.lbStep = QLabel('步数:{}'.format(self.steps))
            self.lbTime.setFont(font)
            self.lbStep.setFont(font)
    
            self.hltMedium = QHBoxLayout()
            self.hltMedium.addWidget(self.lbTime)
            self.hltMedium.addStretch()
            self.hltMedium.addWidget(self.lbStep)
    
            self.gltPannel.setSizeConstraint(QLayout.SetFixedSize)
            # 设置方块间隔
            self.gltPannel.setSpacing(self.spacing)
    
            game_background_path = os.path.abspath('.') + '\\src\\images\\game_background.png'
            self.onInit()
            self.wdgGame.setFixedSize(400, 400)
            self.wdgGame.setLayout(self.gltPannel)
    
            self.wdgGame.setStyleSheet("""
                background-color:rgb(242, 242, 242);
                """)
    
            # 重新开始按钮
            self.btnRestart = QPushButton('重新开始')
            self.btnRestart.setFixedSize(120, 40)
            self.btnRestart.setStyleSheet("""
                border-radius:5px;
                padding:2px 4px;
                color: white;
                font: bold;
                font-family: "Microsoft YaHei";
                background-color:rgb(22, 155, 213);
                font-size: 20px;""")
            self.btnRestart.clicked.connect(self.restart)
    
            self.vltMain.setSpacing(20)
    
            self.vltMain.addLayout(self.hltTop)
            self.vltMain.addLayout(self.hltMedium)
            self.vltMain.addWidget(self.wdgGame, 0, Qt.AlignHCenter)
            self.vltMain.addWidget(self.btnRestart, 0, Qt.AlignRight)
    
            self.setLayout(self.vltMain)
            self.setFixedSize(400, 600)
            self.setStyleSheet("background-color:lightblue;")
    
        # 初始化布局
        def onInit(self):
            # 清空二维数组
            self.blocks = []
            # 产生顺序数组
            self.numbers = list(range(1, self.level ** 2))
            self.numbers.append(0)
    
            # 将数字添加到二维数组
            for row in range(self.level):
                self.blocks.append([])
                for column in range(self.level):
                    temp = self.numbers[row * self.level + column]
    
                    if temp == 0:
                        self.zero_row = row
                        self.zero_column = column
                    self.blocks[row].append(temp)
    
            # 打乱数组
            for i in range(500):
                random_num = random.randint(0, 3)
                self.move(Direction(random_num))
    
            # 初始化步数和时间,并更新显示
            self.steps = 0
            self.time = 0
            self.lbTime.setText('时间:{}s'.format(self.time))
            self.updatePanel()
            self.updateSteps()
    
    • 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

    4.3 返回首页

    由于返回首页是由 GamePage 的上一级(NumberHuaRong) 控制的,所以需要定义一个信号:

    • backToHome:返回首页;
    backToHome = pyqtSignal()
    
    • 1

    对应的,定义一个槽:

    # 返回首页
    def back(self):
        self.backToHome.emit()
    
    • 1
    • 2
    • 3

    back() 槽和返回按钮相连接:

    self.btnBack.clicked.connect(self.back)
    
    • 1

    也就是点击返回按钮的时候发送 backToHome 信号。

    4.4 背景音乐控制

    背景音乐的控制权同样交给 GamePage 的上一级(NumberHuaRong) ,因此定义两个信号:

    • musicStart:播放背景音乐;
    • musicStop:停止播放背景音乐。

    对应的定义槽函数:

    # 背景音乐控制
    def musicControl(self, control):
        if control:
            self.musicStart.emit()
        else:
            self.musicStop.emit()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    musicControl() 槽和 swithcBtn 连接:

    self.swithcBtn.checkedChanged.connect(self.musicControl)
    
    • 1

    根据 switchBtn 的状态分别发送不同的控制信号。

    4.5 更新步数

    步数的更新在 move() 方法中发生,注意:由于使用方向键会导致组件焦点的变化,程序没有进行相应的移动,所以我们只检测WSAD 来表示方向。检测到 WSAD 后步数增加并更新显示:

        # 更新步数
        def updateSteps(self):
            self.lbStep.setText('步数:{}'.format(self.steps))
    
        # 方块移动算法
        def move(self, direction):
            if(direction == Direction.UP): # 上
                if self.zero_row != self.level - 1:
                    self.blocks[self.zero_row][self.zero_column] = self.blocks[self.zero_row + 1][self.zero_column]
                    self.blocks[self.zero_row + 1][self.zero_column] = 0
                    self.zero_row += 1
                    self.steps += 1
                    self.updateSteps()
             if(...)  # 此处省略其它三个方向的判断
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.6 更新时间

    利用定时器,每过1s,self.time 增加1并更新显示:

    self.timer = QTimer()   # 定时器
    ...
    
    # 启动定时器
    self.timer.start(1000)  # 设置超时时间为1s
    self.timer.timeout.connect(self.updateTime) # 更新显示
    ...
    
    # 更新时间
    def updateTime(self):
        self.time += 1
        self.lbTime.setText('时间:{}s'.format(self.time))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.7 重新开始

    由于程序的初始化操作是在 self.onInit() 中进行的,所以重新开始只要执行以下该方法即可。定义槽函数并与btnRestart 的 clicked 信号连接即可:

    self.btnRestart.clicked.connect(self.restart)
    ...
    
    # 重新开始
    def restart(self):
        self.onInit()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5、HomePage 修改

    HomePage 里面选择游戏难度后,NumberHuaRong 会根据难度实例化对应的 GamePage,所以游戏难度需要传递给 NumberHuaRong ,为了简单我们直接对 StyleButton 进行改造以达到“一劳永逸”的目的。定义信号:

    btnText = pyqtSignal(int)
    
    • 1

    定义一个槽函数,便于发送信号:

    def sendNumber(self):
        self.btnText.emit(int(self.txt[0]))
    
    • 1
    • 2

    然后在初始化时进行连接:

    self.clicked.connect(self.sendNumber)
    
    • 1

    也就是点击 StyleButton 时会发送一个信号,该信号带有一个参数(游戏的难度),该参数取自按钮上的文本第一个字符并将其转成 int 类型。

    6、NumberHuaRong 修改

    6.1 根据游戏难度实例化 GamePage

    定义槽函数 setLevel() 根据游戏的难度设置 GamePage 每行、每列数字方块的个数,并将其与 StyleButton 的 btnText 信号相连接:

    # 游戏难度已选定
    self.hp.btn3_3.btnText.connect(self.setLevel)
    self.hp.btn4_4.btnText.connect(self.setLevel)
    self.hp.btn5_5.btnText.connect(self.setLevel)
    self.hp.btn6_6.btnText.connect(self.setLevel)
    
    def setLevel(self, level):
        # 如果 sltMain 的 item 个数为2,即已经包含了一个 GamePage,
        # 需要将其删除,因为我们要添加一个新的 GamePage 进来
        if self.sltMain.count() == 2:
            self.sltMain.removeItem(self.sltMain.itemAt(1))
    
        self.gp = GamePage(level)  
        self.sltMain.addWidget(self.gp)
        self.sltMain.setCurrentIndex(1)  # 设置 GamePage 为当前页面
        
        # 背景音乐控制
        self.gp.musicStart.connect(self.start)
        self.gp.musicStop.connect(self.stop)
    
        # 返回首页
        self.gp.backToHome.connect(self.backToHomePage)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    6.2 背景音乐控制

    定义两个信号:

    • musStart:播放背景音乐;
    • musStop:暂停播放背景音乐。
    musStart = pyqtSignal()
    musStop = pyqtSignal()
    
    • 1
    • 2

    对应的定义两个槽函数:

    def start(self):
        self.musStart.emit()
    
    def stop(self):
        self.musStop.emit()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后这两个槽函数在 setLevel 中与 GamePage 的两个信号连接:

    # 背景音乐控制
    self.gp.musicStart.connect(self.start)
    self.gp.musicStop.connect(self.stop)
    
    • 1
    • 2
    • 3

    6.3 返回首页

    定义槽函数:

    def backToHomePage(self):
        self.sltMain.removeItem(self.sltMain.itemAt(1))	# 删除 GamePage 
        self.sltMain.setCurrentIndex(0)
    
    • 1
    • 2
    • 3

    然后与 GamePage 的 backToHome 信号连接即可:

    # 返回首页
    self.gp.backToHome.connect(self.backToHomePage)
    
    • 1
    • 2

    6.4 媒体播放器

    在 main 函数中创建媒体播放器:

    # 媒体播放器
    player = QMediaPlayer()
    # 播放列表
    play_list = QMediaPlaylist()
    # 媒体资源
    media_content = QMediaContent(QUrl.fromLocalFile(os.path.abspath('.') + '\\src\\sounds\\background_music.mp3'))
    play_list.addMedia(media_content)
    # 设置当前要播放的资源
    play_list.setCurrentIndex(0)
    # 循环播放
    play_list.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)    
    player.setPlaylist(play_list) 
    player.setVolume(50);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    至此,游戏页面功能大致完成,来看一下效果图:04_各个难度展示

    待完成功能:

    • 保存成绩
    • 查看排行
  • 相关阅读:
    【AI理论学习】语言模型:掌握BERT和GPT模型
    虚拟互动展会沉浸式体验方案设计优势
    IntelliJ IDEA 安装及创建Java项目
    Linux socket编程(4):服务端fork之僵尸进程的处理
    JVM - 你们垃圾回收器用的什么? G1有哪些特点?G1如何实现可预测的停顿时间?漏标问题如何解决的?介绍下三色标记?说说STAB 算法 ?
    【LeetCode刷题-树】--1367.二叉树中的链表
    2.9.5 Ext JS的Object类型处理及便捷方法
    Spring Data JPA 学习笔记
    2022年暑假ACM热身练习3(详细)
    SpringBootCMS漏洞复现分析
  • 原文地址:https://blog.csdn.net/yaoyefengchen/article/details/125490934