源码请看我的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
from pygameWidgets import *
from GameEngine import *
import time
class singleGUI:
def __init__(self, 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))
self.Game = Game
self.Game.p2 = AI(self.Game.p2.name)
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}
self.player = self.Game.p1
self.objs = []
self.cardDict = {}
self.selectedCards = []
self.chosenLandlord = False
self.prevPlayTime = time.time()
pygame.init()
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 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)
self.selectedCards = []
mod = self.Game.checkWin()
if mod == 1:
tkinter.Tk().wm_withdraw()
tkinter.messagebox.showinfo(
f'Winner is: {self.Game.prevPlayer}', 'CONGRATS PROFESSOR! KEEP OPPRESSING YOUR STUDENTS!')
pygame.quit()
elif mod == 2:
tkinter.Tk().wm_withdraw()
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.messagebox.showwarning('Warning', 'Invalid play!')
def passCard(self):
if self.selectedCards == []:
self.Game.makePlay([])
self.prevPlayTime = time.time()
self.updateScreen()
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:
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
xStart = 230
cardCnt = len(self.Game.prevPlay[1])
if cardCnt != 0:
for card in self.Game.prevPlay[1]:
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
text = f"Current playing: {self.Game.currentPlayer}"
currentPlayerTextObj = Text(self.screen, text, 230, 120, 350, 50)
self.objs.append(currentPlayerTextObj)
myPos = self.Game.playOrder.index(self.name)
if myPos == 0:
self.prevPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.prevPlayer)
self.nextPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 70, 60, 20, self.chosenLandlord)
self.objs.append(self.nextPlayer)
self.myPlayer = Player(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 570, 60, 20, self.chosenLandlord)
self.objs.append(self.myPlayer)
self.prevImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 10, 60, 60)
self.objs.append(self.prevImg)
self.afterImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 10, 60, 60)
self.objs.append(self.afterImg)
self.myImg = Img(
self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 510, 60, 60)
self.objs.append(self.myImg)
self.prevCardCnt = Text(
self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 70, 90, 20, 20)
self.objs.append(self.prevCardCnt)
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:
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:
self.passCardButton = Button(
self.screen, 100, 350, 100, 50, 'Pass turn', self.passCard)
self.objs.append(self.passCardButton)
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:
self.passButton = Button(
self.screen, 100, 350, 100, 50, 'Pass', self.passIdentity)
self.objs.append(self.passButton)
self.confirmButton = Button(
self.screen, 600, 350, 100, 50, 'Be Professor', self.confirmIdentity)
self.objs.append(self.confirmButton)
def initGUI(self):
self.clock = pygame.time.Clock()
self.prevPlayTime = time.time()
self.screen = pygame.display.set_mode((self.width, self.height))
self.screen.fill(self.bgColor)
pygame.display.set_caption(self.title)
self.Game.assignPlayOrder()
self.Game.shuffleDeck()
self.Game.dealCard()
self.updateScreen()
def run(self):
self.initGUI()
playing = True
while playing:
self.screen.fill(self.bgColor)
self.screen.blit(self.bg, (0, 0))
self.clock.tick(self.fps)
self.updateScreen()
if time.time() - self.prevPlayTime > 3:
if self.Game.currentPlayer == 'AI1':
if self.chosenLandlord:
self.Game.AIMakePlay('AI1', self.chosenLandlord)
else:
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()
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:
obj.process()
pygame.display.flip()
pygame.quit()
exit()
以上就是前端代码的主要部分了。
可以在前端代码中加入以下部分来在运行代码时运行程序:
if __name__ == "__main__":
wnd = tkinter.Tk()
wnd.geometry("800x600")
wnd.title("Fight the Professor!")
wnd.resizable(0, 0)
game = Game('human', 'AI1', 'AI2')
singleGUIObj = singleGUI(game)
wnd.mainloop()
刚才还有提到过很多次博主自己写的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):
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))
self.buttonRect = pygame.Rect(self.x, self.y, self.width, self.height)
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):
self.buttonSurface.fill(self.fillColors['hover'])
if pygame.mouse.get_pressed(num_buttons=3)[0]:
self.buttonSurface.fill(self.fillColors['pressed'])
if self.onePress:
self.onclickFunction()
elif not self.alreadyPressed:
self.onclickFunction()
self.alreadyPressed = True
else:
self.alreadyPressed = False
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
])
self.screen.blit(self.buttonSurface, self.buttonRect)
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,
}
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))
self.cardRect = pygame.Rect(self.x, self.y, self.width, self.height)
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
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)
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
])
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游戏的引擎。感兴趣的小伙伴可以去了解一下。
不出意外的话本博客就是该系列的最后一篇博客了,希望对各位有帮助!有各种问题和见解欢迎评论或者私信!
Original: https://blog.csdn.net/EricFrenzy/article/details/128132574
Author: EricFrenzy
Title: 用python写一个有AI的斗地主游戏(三)——简述前端设计和实现
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/783554/
转载文章受原作者版权保护。转载请注明原作者出处!