源码请看我的Github页面。
这是我一个课程的学术项目,请不要抄袭,引用时请注明出处。
本专栏系列旨在帮助小白从零开始开发一个项目,同时分享自己写代码时的感想。
请大佬们为我的拙见留情,有不规范之处烦请多多包涵!
本专栏上一篇博客里介绍了游戏后端/游戏引擎的实现方法。本篇博客讲简要介绍python游戏开发中前端设计和实现的方法。当然,以下内容还是博主自己琢磨的,有遗漏或不足之处请指教!
和人一样, 一个游戏受欢迎与否有两个主要因素:一个就是它的灵魂(后端),另一个就是它的脸(前端)。前端对于游戏可玩性、美感、可宣传性、市场竞争力等方面有着重大作用。对于斗地主游戏来说,它的游戏玩法经过历史与文化的堆积,已经非常完美。前端的作用就是要以特别的方式展现游戏内部的价值观。于是,作为一个学术项目,我把我敬爱的教授和热爱的学校放到了游戏里,将斗地主变成了外国友人和苦闷的大学生都易于共情的“斗教授”,并以此为主题设计了游戏的前端。
除了保存基本的游戏玩法外,博主对游戏内装饰性元素如背景和玩家头像进行了基本的更换(换成了教授的头和校徽),把地主改成了教授,把农民换成了学生,等等,以向教授展现学生们在学业压力下的反抗和奋斗精神。
不扯没用的了,下面介绍下tkinter
和pygame
在该项目的一些基础用法。
上篇博客讲到,后端已经为游戏逻辑提供了工具,前端负责用这些工具展示一个完整的游戏。博主的单人模式前端代码放在single.py
的singleGUI
类里。我们需要以下功能:
'''
singleGUI Class: 用来创建、维护、更新游戏窗口并从其中获得并处理输入的类
__init__: 用Game对象初始化singleGUI类
confirmIdentity: 叫地主(和按钮对象绑定)
passIdentity: 不叫地主(和按钮对象绑定)
selectCard: 选择手牌(和卡牌对象绑定)
deselectCard: 取消选择手牌(和卡牌对象绑定)
confirmCard: 确认出牌(和按钮对象绑定)
passCard: 过牌(和按钮对象绑定)
updateScreen: 用Game对象更新游戏界面内容
initGUI: 初始化pygame相关对象和Game对象
run: 类似于tkinter里的mainloop,以循环的方式运行游戏
我们还可以用一个if __name__ == "__main__" 来在运行该文件时运行程序
'''
代码的主要逻辑是:实时根据当前游戏状态创建/更新屏幕上的可互动内容(放到一个列表里),并在主循环中遍历并调用每个对象的process
函数把它们渲染到屏幕上并可以获取相关输入(即根据其初始化参数和用户操作进行动态更新,后面会介绍这些对象是怎么写的)。代码比较长,博主就不挨个拎出来介绍了,对代码的说明和介绍写在了注释里,建议挑最感兴趣的部分阅读。以下是主要的代码:
# 这三个库用来处理图形窗口
import tkinter
import pygame
import tkinter.messagebox
# pygameWidgets库是博主自己写的一些pygame可以互动的对象,比如按钮和卡牌等,之后会详细介绍
from pygameWidgets import *
# 游戏引擎(后端)
from GameEngine import *
import time
class singleGUI:
def __init__(self, Game): # 需要传入游戏引擎中的Game对象
# 初始化变量
self.name = Game.p1.name # 人类玩家的名字
self.width = 800 # 窗口宽度
self.height = 600 # 窗口高度
self.fps = 20 # 帧率
self.title = "Fight the Professor! By Eric Gao" # 游戏窗口标题
self.bgColor = (255, 255, 255) # 窗口默认背景色
self.bg = pygame.image.load('./imgs/bg/tartanbg.png') # 加载窗口背景图片
self.bg = pygame.transform.scale(self.bg, (self.width, self.height)) # 预处理窗口背景图片
# 初始化Game对象
self.Game = Game # self.Game作为类内变量共享
self.Game.p2 = AI(self.Game.p2.name) # 两个AI玩家
self.Game.p3 = AI(self.Game.p3.name)
self.Game.playerDict = {self.Game.p1.name: self.Game.p1,
self.Game.p2.name: self.Game.p2, self.Game.p3.name: self.Game.p3} # 用名字/id找到相应玩家对象
self.player = self.Game.p1 # 人类玩家
self.objs = [] # 记录窗口里的对象,比如按钮、头像、卡牌、文字等等
self.cardDict = {} # 卡牌字典,由卡牌的值指向卡牌对象
self.selectedCards = [] # 已选择的卡牌列表
self.chosenLandlord = False # 是否已经叫好地主
self.prevPlayTime = time.time() # 初始化出牌时间
pygame.init() # 初始化pygame
self.run() # 运行游戏
# 叫地主(和按钮对象绑定)
def confirmIdentity(self):
self.Game.chooseLandlord(self.name) # 自己叫地主
self.chosenLandlord = True # 游戏已经产生地主
self.Game.assignPlayOrder() # 分配游玩顺序
self.prevPlayTime = time.time() # 记录叫地主的时间
self.updateScreen() # 更新窗口内容
# 不叫地主(和按钮对象绑定)
def passIdentity(self):
self.Game.makePlay([]) # 相当于过牌
self.prevPlayTime = time.time() # 记录不叫地主的时间
self.updateScreen() # 更新窗口内容
# 选择手牌(和卡牌对象绑定)
def selectCard(self, cardVal): # 输入卡片的值
# 这个if是用来防止玩家再次选择已选择的卡或者选择其它的卡
if cardVal not in self.selectedCards and cardVal in self.player.cards:
self.selectedCards.append(cardVal) # 放到列表里
# 取消选择手牌(和卡牌对象绑定)
def deSelect(self, cardVal):
if cardVal in self.selectedCards and cardVal in self.player.cards:
self.selectedCards.remove(cardVal)
# 确认出牌(和按钮对象绑定)
def confirmCard(self):
# 这里设置了确定按钮和过牌按钮分开,所以不能不选择牌直接确认
if self.selectedCards != [] and self.Game.isValidPlay(self.selectedCards):
self.Game.makePlay(self.selectedCards) # Game对象内出牌
self.selectedCards = [] # 清空已选择的牌
mod = self.Game.checkWin() # 看游戏结没结束
if mod == 1: # 当前地主玩家没有手牌,地主获胜
tkinter.Tk().wm_withdraw() # 隐藏tkinter主窗口
tkinter.messagebox.showinfo(
f'Winner is: {self.Game.prevPlayer}', 'CONGRATS PROFESSOR! KEEP OPPRESSING YOUR STUDENTS!')
pygame.quit() # 结束游戏
elif mod == 2: # 当前农民玩家没有手牌,农民获胜
tkinter.Tk().wm_withdraw() # to hide the main window
tkinter.messagebox.showinfo(
f'Winner is: {self.Game.prevPlayer}', 'CONGRATS STUDENTS! KILL MORE PROFESSORS!')
pygame.quit()
self.prevPlayTime = time.time()
self.updateScreen()
elif self.Game.prevPlay == []: # 不能这么出,显示错误
tkinter.Tk().wm_withdraw() # 隐藏tkinter主窗口
tkinter.messagebox.showwarning('Warning', 'Invalid play!')
# 空过(和按钮对象绑定)
def passCard(self):
# 必须不选择牌才能空过(不是很人性化,但是解决了bug)
if self.selectedCards == []:
self.Game.makePlay([])
self.prevPlayTime = time.time()
self.updateScreen()
# 用Game对象更新屏幕
def updateScreen(self):
# 清空当前所有屏幕上的对象
self.objs.clear()
################ 更新手牌 ################
for i in self.cardDict: # 删掉手牌里没有的牌
if i not in self.player.cards:
self.cardDict[i] = None
xStart = 50 # 从屏幕这个位置开始放卡牌对象
cardCnt = len(self.player.cards)
for card in self.player.cards: # 遍历自己的卡牌
if card not in self.cardDict: # 如果看到了新的牌,那就创建它
# 这里的Card是博主自己写的pygame组件对象,过后会更详细地介绍
# 参数分别是主屏幕对象,卡面数值,x坐标,y坐标,宽,高,被点击时调用的函数
cardObj = Card(self.screen, card, xStart, 430, 50, 70, lambda x=card: self.selectCard(
x), lambda x=card: self.deSelect(x))
self.cardDict[card] = cardObj
else: # 如果是已经有的牌,那么只更新它的位置
cardObj = self.cardDict[card]
cardObj.x = xStart
if cardObj not in self.objs: # 如果更新过的牌不在已有对象里,把它放到已有对象里
self.objs.append(cardObj)
xStart += 700/cardCnt # 下一张牌的x坐标
################ 更新上一个玩家打的牌 ################
xStart = 230 # 从这个位置开始放牌
cardCnt = len(self.Game.prevPlay[1]) # 上一个出牌记录牌的数量
if cardCnt != 0: # 有牌就放牌
for card in self.Game.prevPlay[1]:
# 同样的Card对象,是博主自己写的,稍后会介绍
prevPlayObj = Card(self.screen, card, xStart, 180, 50, 70)
self.objs.append(prevPlayObj)
xStart += 350/cardCnt
################ 更新展示的地主牌 ################
if self.chosenLandlord == True: # 如果已经叫过地主,那就显示地主牌
xStart = 320
for card in self.Game.landLordCards:
landlordCardObj = Card(self.screen, card, xStart, 40, 50, 70)
self.objs.append(landlordCardObj)
xStart += 200/3 # 三张牌平均分配到200像素宽
################ 更新“当前某某在出牌”文字提示 ################
text = f"Current playing: {self.Game.currentPlayer}"
# Text也是博主自己写的pygame组件,稍后会介绍
# 需要参数分别是:显示到的窗口,文本内容,x坐标,y坐标,宽,高
currentPlayerTextObj = Text(self.screen, text, 230, 120, 350, 50)
self.objs.append(currentPlayerTextObj)
################ 更新玩家头像、名称、位置、手牌数量 ################
myPos = self.Game.playOrder.index(self.name) # 自己的位置
# if的三种可能性实现原理一样,看一个就好
if myPos == 0:
# 创建上家的Player对象(一个有背景和颜色的名字),Player也是博主自己写的
# 需要参数:显示到的窗口,GameEngine里的player对象,x坐标,y坐标,宽,高,是否已叫地主
self.prevPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.prevPlayer)
# 创建下家的Player对象
self.nextPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.nextPlayer)
# 创建自己的Player对象
self.myPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 570, 60, 20, self.chosenLandlord)
self.objs.append(self.myPlayer)
# 创建上家的Img对象(图片头像),是博主自己写的pygame组件
# 需要参数:显示到的屏幕,player对象,x坐标,y坐标,宽,高
self.prevImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 10, 60, 60)
self.objs.append(self.prevImg)
# 创建下家的Img对象
self.afterImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 10, 60, 60)
self.objs.append(self.afterImg)
# 创建自己的Img对象
self.myImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 510, 60, 60)
self.objs.append(self.myImg)
# 创建上家的Text对象(用来记录手牌数量的文字),也是自己写的,后面会详细介绍
# 需要参数:显示到的屏幕,手牌数量,x坐标,y坐标,宽,高
self.prevCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 70, 90, 20, 20)
self.objs.append(self.prevCardCnt)
# 创建下家的Text对象
self.afterCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[1]].cards)), 720, 90, 20, 20)
self.objs.append(self.afterCardCnt)
elif myPos == 1: # 这个elif和下面的else类似,只是玩家对象根据顺序进行了变化
self.prevPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 50, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.prevPlayer)
self.nextPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 700, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.nextPlayer)
self.myPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 370, 570, 60, 20, self.chosenLandlord)
self.objs.append(self.myPlayer)
self.prevImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 50, 10, 60, 60)
self.objs.append(self.prevImg)
self.afterImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 700, 10, 60, 60)
self.objs.append(self.afterImg)
self.myImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 370, 510, 60, 60)
self.objs.append(self.myImg)
self.prevCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[0]].cards)), 70, 90, 20, 20)
self.objs.append(self.prevCardCnt)
self.afterCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 720, 90, 20, 20)
self.objs.append(self.afterCardCnt)
else:
self.prevPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 50, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.prevPlayer)
self.nextPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 700, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.nextPlayer)
self.myPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 370, 570, 60, 20, self.chosenLandlord)
self.objs.append(self.myPlayer)
self.prevImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 50, 10, 60, 60)
self.objs.append(self.prevImg)
self.afterImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 700, 10, 60, 60)
self.objs.append(self.afterImg)
self.myImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 370, 510, 60, 60)
self.objs.append(self.myImg)
self.prevCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[1]].cards)), 70, 90, 20, 20)
self.objs.append(self.prevCardCnt)
self.afterCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[0]].cards)), 720, 90, 20, 20)
self.objs.append(self.afterCardCnt)
################ 更新按钮对象 ################
# 如果是自己的回合:
## 如果没叫地主,那就显示叫地主和不叫地主的按钮
## 否则显示出牌或者过牌的按钮
if self.chosenLandlord and self.Game.currentPlayer == self.name:
# 创建过牌的Button对象(自己写的后面会解释)
# 需要参数:显示到的屏幕,x坐标,y坐标,宽,高,显示文字,点击时调用的函数
self.passCardButton = Button(
self.screen, 100, 350, 100, 50, 'Pass turn', self.passCard)
self.objs.append(self.passCardButton)
# 创建确认出牌的Button对象
self.confirmCardButton = Button(
self.screen, 600, 350, 100, 50, 'Confirm Play', self.confirmCard)
self.objs.append(self.confirmCardButton)
elif not self.chosenLandlord and self.Game.currentPlayer == self.name:
# 创建不叫地主的Button对象
self.passButton = Button(
self.screen, 100, 350, 100, 50, 'Pass', self.passIdentity)
self.objs.append(self.passButton)
# 创建叫地主的Button对象
self.confirmButton = Button(
self.screen, 600, 350, 100, 50, 'Be Professor', self.confirmIdentity)
self.objs.append(self.confirmButton)
# 初始化游戏GUI
def initGUI(self):
# 设置一些基础变量
self.clock = pygame.time.Clock() # pygame的计时器
self.prevPlayTime = time.time() # 上一次出牌的时间
self.screen = pygame.display.set_mode((self.width, self.height)) # pygame屏幕对象
self.screen.fill(self.bgColor) # 给屏幕填充初始颜色
pygame.display.set_caption(self.title) # 给pygame窗口添加标题
# 初始化Game对象
self.Game.assignPlayOrder() # 分配叫地主顺序
self.Game.shuffleDeck() # 创建牌堆并洗牌
self.Game.dealCard() # 发牌
# 第一次更新屏幕
self.updateScreen()
# 包含主循环的函数
def run(self):
self.initGUI() # 初始化游戏GUI
playing = True # 表示正在游玩的状态
while playing:
# 在每一帧上覆盖前一帧的内容
self.screen.fill(self.bgColor) # 先用颜色覆盖
self.screen.blit(self.bg, (0, 0)) # 再用背景图覆盖
self.clock.tick(self.fps) # 按照帧率跑这个循环(比如每秒这个循环跑20次)
self.updateScreen() # 每一个循环里要更新屏幕内容
if time.time() - self.prevPlayTime > 3: # 控制AI出牌,在上一步操作三秒后AI行动
if self.Game.currentPlayer == 'AI1':
if self.chosenLandlord: # AI出牌
self.Game.AIMakePlay('AI1', self.chosenLandlord)
else: # AI叫地主
self.Game.AIMakePlay('AI1', self.chosenLandlord)
self.chosenLandlord = True
self.prevPlayTime = time.time()
elif self.Game.currentPlayer == 'AI2':
if self.chosenLandlord:
self.Game.AIMakePlay('AI2', self.chosenLandlord)
else:
self.Game.AIMakePlay('AI2', self.chosenLandlord)
self.chosenLandlord = True
self.prevPlayTime = time.time()
# 处理用户输入
for obj in self.objs:
obj.process() # 每个博主自己写的pygame组件都有一个process函数,调用时可以更新显示并获得用户互动
for event in pygame.event.get():
if event.type in [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP]:
for obj in self.objs: # 在用户按下和抬起鼠标的时候额外处理一次
obj.process()
if event.type == pygame.QUIT: # 用户退出游戏时结束主循环
playing = False
for obj in self.objs: # 这里多写了一遍为了避免用户一次点击时经过了多个帧出现bug
obj.process()
pygame.display.flip() # 把之前生成好的新内容更新到屏幕上
pygame.quit() # 循环结束后退出游戏
exit()
以上就是前端代码的主要部分了。
可以在前端代码中加入以下部分来在运行代码时运行程序:
if __name__ == "__main__":
wnd = tkinter.Tk() # 创建tkinter窗口
wnd.geometry("800x600") # 设置窗口大小
wnd.title("Fight the Professor!") # 设置窗口标题
wnd.resizable(0, 0) # 设置窗口大小为不可更改
game = Game('human', 'AI1', 'AI2') # 创建Game对象
singleGUIObj = singleGUI(game) # 创建pygameGUI对象
wnd.mainloop() # tkinter的主循环
刚才还有提到过很多次博主自己写的pygame组件,放到了pygameWidgets.py
里。我们有以下组件:
'''
pygameWidgets.py描述
Button Class: 用来创建可互动按钮的类
__init__: 需要:显示到的屏幕, x和y坐标, 宽和高, 按钮文字, 被点击时调用的函数, 是否按钮只能被点一次(这个功能没用到)
process: 在屏幕上渲染按钮, 检测用户点击, 被点击后调用函数
Card Class: 用来创建可选中/取消选中卡牌的类
__init__: 需要:显示到的屏幕, x和y坐标, 宽和高, 被选择时调用的函数, 被取消选择(点击第二次)时调用的函数
process: 在屏幕上渲染卡牌, 检测用户点击, 被选择/取消选择后调用函数
Player Class: 创建可以区分地主和农民身份玩家的类
__init__: 需要:显示到的屏幕, player对象, x和y坐标, 宽和高, 是否已经叫地主
process: 根据player对象信息把玩家名字渲染到屏幕上
Text Class: 用来在屏幕上添加文字的类
__init__: 需要:显示到的屏幕, 要显示的文字, x和y坐标, 宽和高
process: 把文字渲染到屏幕上
Img Class: 用来在屏幕上显示图片/玩家头像的类
__init__: 需要:显示到的屏幕, player对象, x和y坐标, 宽和高
process: 根据玩家身份把头像渲染到屏幕上
'''
以下是pygameWidgets.py
代码的主要部分:
import pygame
# 按钮类
class Button():
def __init__(self, screen, x, y, width, height, buttonText='Button', onclickFunction=lambda: print("Not assigned function"), onePress=False):
# 上面的lambda是作为默认函数使用的
# 初始化一些变量
self.screen = screen
self.x = x
self.y = y
self.width = width
self.height = height
self.onclickFunction = onclickFunction
self.onePress = onePress
self.alreadyPressed = False
font = pygame.font.SysFont('Arial', 16)
self.fillColors = {
'normal': '#ffffff',
'hover': '#666666',
'pressed': '#333333',
}
self.buttonSurface = pygame.Surface((self.width, self.height)) # 按钮的Surface
self.buttonRect = pygame.Rect(self.x, self.y, self.width, self.height) # 按钮的Rect
self.buttonSurf = font.render(buttonText, True, (20, 20, 20)) # 按钮文字
def process(self):
mousePos = pygame.mouse.get_pos() # 获取用户当前鼠标位置
self.buttonSurface.fill(self.fillColors['normal']) # 填充为“未点击”颜色
if self.buttonRect.collidepoint(mousePos): # 如果用户鼠标和按钮Rect碰撞
self.buttonSurface.fill(self.fillColors['hover']) # 填充为“悬停”颜色
if pygame.mouse.get_pressed(num_buttons=3)[0]: # 如果鼠标左键被点击
self.buttonSurface.fill(self.fillColors['pressed']) # 填充为“已点击”颜色
if self.onePress: # 如果只能按一次(这个if没用到)
self.onclickFunction() # 那就调用函数一次
elif not self.alreadyPressed: # 如果没有已经点过
self.onclickFunction() # 调用函数一次
self.alreadyPressed = True # 已经点击过了(没用到)
else: # 如果没被点击
self.alreadyPressed = False # 那就记录没点击过(没用到)
# 把按钮文字渲染到按钮Surface上
self.buttonSurface.blit(self.buttonSurf, [
self.buttonRect.width/2 - self.buttonSurf.get_rect().width/2,
self.buttonRect.height/2 - self.buttonSurf.get_rect().height/2
])
# 把按钮Surface渲染到屏幕上按钮Rect的位置
self.screen.blit(self.buttonSurface, self.buttonRect)
# 在按钮Rect周围画黑色实心边界线
pygame.draw.rect(self.screen, (0, 0, 0), self.buttonRect, 2)
# 下面几个类非常类似,只会在有区别的地方进行注释说明
# 卡牌类
class Card:
def __init__(self, screen, cardType, x, y, width, height, onclickFunction=lambda: print("Not assigned function"), cancelFuction=lambda: print("Not assigned function")):
self.screen = screen
self.cardType = cardType
self.x = x
self.y = y
self.width = width
self.height = height
self.onclickFunction = onclickFunction
self.cancelFunction = cancelFuction
self.pressedCnt = 0 # 点击计数
self.newY = {
'normal': self.y,
'hover': self.y-3,
'pressed': self.y-5,
} # 未选中、悬停、选中的y坐标进行上下移动
self.cardImg = pygame.image.load(f"./imgs/pokers/{self.cardType}.png") # 加载卡片图片
self.cardImg = pygame.transform.scale(
self.cardImg, (self.width, self.height)) # 改变图片大小
self.cardImg.convert() # 处理图片格式
self.cardSurface = pygame.Surface((self.width, self.height)) # 卡牌Surface
self.cardRect = pygame.Rect(self.x, self.y, self.width, self.height) # 卡牌Rect
def process(self):
condition = 'normal' # 卡牌目前未选择
mousePos = pygame.mouse.get_pos()
if self.cardRect.collidepoint(mousePos):
condition = 'hover' # 悬停状态
if pygame.mouse.get_pressed(num_buttons=3)[0]:
self.pressedCnt = (self.pressedCnt+1) % 2 # 一个小窍门,让计数在0-1-0-1这样循环
if self.pressedCnt == 1: # 如果从未选择变成已选择,调用选择时的函数
self.onclickFunction()
else: # 否则调用取消选择时的函数
self.cancelFunction()
if self.pressedCnt == 1: # 卡牌已经选择
condition = 'pressed'
else: # 卡牌未被选择
condition = 'normal'
self.cardRect = pygame.Rect(
self.x, self.newY[condition], self.width, self.height) # 卡牌Rect
# 把卡牌图片渲染到卡牌Surface上
self.cardSurface.blit(self.cardImg, [
self.cardRect.width/2 - self.cardSurface.get_rect().width/2,
self.cardRect.height/2 - self.cardSurface.get_rect().height/2
])
# 把卡牌Surface渲染到屏幕上的卡牌Rect位置
self.screen.blit(self.cardSurface, self.cardRect)
# 在卡牌周围画边框
pygame.draw.rect(self.screen, (0, 0, 0), self.cardRect, 2)
# 玩家(名称)类
class Player:
def __init__(self, screen, playerObj, x, y, width, height, assignedIdentity=False):
self.player = playerObj
self.screen = screen
self.x = x
self.y = y
self.width = width
self.height = height
font = pygame.font.SysFont('Arial', 18)
self.playerSurface = pygame.Surface((self.width, self.height))
self.playerSurface.fill((225, 225, 225))
self.playerRect = pygame.Rect(self.x, self.y, self.width, self.height)
if assignedIdentity: # 根据是否选过地主对名字颜色进行更改
if self.player.identity == 's':
self.playerSurf = font.render(
self.player.name, True, (81, 4, 0))
else:
self.playerSurf = font.render(
self.player.name, True, (2, 7, 93))
else:
self.playerSurf = font.render(self.player.name, True, (80, 80, 80))
def process(self):
self.playerSurface.blit(self.playerSurf, [
self.playerRect.width/2 - self.playerSurf.get_rect().width/2,
self.playerRect.height/2 - self.playerSurf.get_rect().height/2
])
self.screen.blit(self.playerSurface, self.playerRect)
# 文字类
class Text:
def __init__(self, screen, text, x, y, width, height):
self.text = text
self.screen = screen
self.x = x
self.y = y
self.width = width
self.height = height
font = pygame.font.SysFont('Arial', 18)
self.textSurface = pygame.Surface((self.width, self.height))
self.textRect = pygame.Rect(self.x, self.y, self.width, self.height)
self.textSurf = font.render(self.text, True, (255, 255, 255))
def process(self):
self.textSurface.blit(self.textSurf, [
self.textRect.width/2 - self.textSurf.get_rect().width/2,
self.textRect.height/2 - self.textSurf.get_rect().height/2
])
self.screen.blit(self.textSurface, self.textRect)
# 图片/玩家头像类
class Img:
def __init__(self, screen, player, x, y, width, height):
self.screen = screen
self.x = x
self.y = y
self.width = width
self.height = height
self.identity = player.identity
if self.identity == 'p': # 我敬爱的教授的图片(代表地主/教授)
self.image = pygame.image.load(f"./imgs/professors/saquib.jpg")
else: # 校徽图片(代表农民/学生)
self.image = pygame.image.load(f"./imgs/students/tartan.png")
self.image = pygame.transform.scale(
self.image, (self.width, self.height))
self.image.convert()
self.imgSurface = pygame.Surface((self.width, self.height))
self.imgRect = pygame.Rect(self.x, self.y, self.width, self.height)
def process(self):
self.imgSurface.blit(self.image, [
self.imgRect.width/2 - self.imgSurface.get_rect().width/2,
self.imgRect.height/2 - self.imgSurface.get_rect().height/2
])
self.screen.blit(self.imgSurface, self.imgRect)
到这里,游戏的前端部分基本上就完成啦!
博主没学过设计,游戏界面可能比较丑陋,但是已经按照强迫症标准努力进行了左右对称。
pygame
也有一些内置的组件(比如Spire
这种功能比较齐全的)。同时,斗地主游戏好像也能只用tkinter
实现,因为主要用到的功能基本只有按钮、图片这种tkinter
里很强大的部分。pygame
强就强在做有镜头感的冒险类游戏(比如上帝视角射击游戏,冰与火之歌游戏等等)。市面上还有很多其它很强大的游戏库,比如Ursina
这种能做出很华丽3D游戏的引擎。感兴趣的小伙伴可以去了解一下。
不出意外的话本博客就是该系列的最后一篇博客了,希望对各位有帮助!有各种问题和见解欢迎评论或者私信!