圖形用戶界面和游戲開發
基于tkinter模塊的GUI
GUI是圖形用戶界面的縮寫,圖形化的用戶界面對使用過計算機的人來說應該都不陌生,在此也無需進行贅述。Python默認的GUI開發模塊是tkinter(在Python 3以前的版本中名為Tkinter),從這個名字就可以看出它是基于Tk的,Tk是一個工具包,最初是為Tcl設計的,后來被移植到很多其他的腳本語言中,它提供了跨平臺的GUI控件。當然Tk并不是最新和最好的選擇,也沒有功能特別強大的GUI控件,事實上,開發GUI應用并不是Python最擅長的工作,如果真的需要使用Python開發GUI應用,wxPython、PyQt、PyGTK等模塊都是不錯的選擇。
基本上使用tkinter來開發GUI應用需要以下5個步驟:
- 導入tkinter模塊中我們需要的東西。
- 創建一個頂層窗口對象并用它來承載整個GUI應用。
- 在頂層窗口對象上添加GUI組件。
- 通過代碼將這些GUI組件的功能組織起來。
- 進入主事件循環(main loop)。
下面的代碼演示了如何使用tkinter做一個簡單的GUI應用。
import tkinter
import tkinter.messagebox
def main():
flag = True
# 修改標簽上的文字
def change_label_text():
nonlocal flag
flag = not flag
color, msg = ('red', 'Hello, world!')\
if flag else ('blue', 'Goodbye, world!')
label.config(text=msg, fg=color)
# 確認退出
def confirm_to_quit():
if tkinter.messagebox.askokcancel('溫馨提示', '確定要退出嗎?'):
top.quit()
# 創建頂層窗口
top = tkinter.Tk()
# 設置窗口大小
top.geometry('240x160')
# 設置窗口標題
top.title('小游戲')
# 創建標簽對象并添加到頂層窗口
label = tkinter.Label(top, text='Hello, world!', font='Arial -32', fg='red')
label.pack(expand=1)
# 創建一個裝按鈕的容器
panel = tkinter.Frame(top)
# 創建按鈕對象 指定添加到哪個容器中 通過command參數綁定事件回調函數
button1 = tkinter.Button(panel, text='修改', command=change_label_text)
button1.pack(side='left')
button2 = tkinter.Button(panel, text='退出', command=confirm_to_quit)
button2.pack(side='right')
panel.pack(side='bottom')
# 開啟主事件循環
tkinter.mainloop()
if __name__ == '__main__':
main()
需要說明的是,GUI應用通常是事件驅動式的,之所以要進入主事件循環就是要監聽鼠標、鍵盤等各種事件的發生并執行對應的代碼對事件進行處理,因為事件會持續的發生,所以需要這樣的一個循環一直運行著等待下一個事件的發生。另一方面,Tk為控件的擺放提供了三種布局管理器,通過布局管理器可以對控件進行定位,這三種布局管理器分別是:Placer(開發者提供控件的大小和擺放位置)、Packer(自動將控件填充到合適的位置)和Grid(基于網格坐標來擺放控件),此處不進行贅述。
使用Pygame進行游戲開發
Pygame是一個開源的Python模塊,專門用于多媒體應用(如電子游戲)的開發,其中包含對圖像、聲音、視頻、事件、碰撞等的支持。Pygame建立在SDL的基礎上,SDL是一套跨平臺的多媒體開發庫,用C語言實現,被廣泛的應用于游戲、模擬器、播放器等的開發。而Pygame讓游戲開發者不再被底層語言束縛,可以更多的關注游戲的功能和邏輯。
下面我們來完成一個簡單的小游戲,游戲的名字叫“大球吃小球”,當然完成這個游戲并不是重點,學會使用Pygame也不是重點,最重要的我們要在這個過程中體會如何使用前面講解的面向對象程序設計,學會用這種編程思想去解決現實中的問題。
制作游戲窗口
import pygame
def main():
# 初始化導入的pygame中的模塊
pygame.init()
# 初始化用于顯示的窗口并設置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 設置當前窗口的標題
pygame.display.set_caption('大球吃小球')
running = True
# 開啟一個事件循環處理發生的事件
while running:
# 從消息隊列中獲取事件并對事件進行處理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
在窗口中繪圖
可以通過pygame中draw模塊的函數在窗口上繪圖,可以繪制的圖形包括:線條、矩形、多邊形、圓、橢圓、圓弧等。需要說明的是,屏幕坐標系是將屏幕左上角設置為坐標原點(0, 0)
,向右是x軸的正向,向下是y軸的正向,在表示位置或者設置尺寸的時候,我們默認的單位都是像素。所謂像素就是屏幕上的一個點,你可以用瀏覽圖片的軟件試著將一張圖片放大若干倍,就可以看到這些點。pygame中表示顏色用的是色光三原色表示法,即通過一個元組或列表來指定顏色的RGB值,每個值都在0~255之間,因為是每種原色都用一個8位(bit)的值來表示,三種顏色相當于一共由24位構成,這也就是常說的“24位顏色表示法”。
import pygame
def main():
# 初始化導入的pygame中的模塊
pygame.init()
# 初始化用于顯示的窗口并設置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 設置當前窗口的標題
pygame.display.set_caption('大球吃小球')
# 設置窗口的背景色(顏色是由紅綠藍三原色構成的元組)
screen.fill((242, 242, 242))
# 繪制一個圓(參數分別是: 屏幕, 顏色, 圓心位置, 半徑, 0表示填充圓)
pygame.draw.circle(screen, (255, 0, 0,), (100, 100), 30, 0)
# 刷新當前窗口(渲染窗口將繪制的圖像呈現出來)
pygame.display.flip()
running = True
# 開啟一個事件循環處理發生的事件
while running:
# 從消息隊列中獲取事件并對事件進行處理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
加載圖像
如果需要直接加載圖像到窗口上,可以使用pygame中image模塊的函數來加載圖像,再通過之前獲得的窗口對象的blit
方法渲染圖像,代碼如下所示。
import pygame
def main():
# 初始化導入的pygame中的模塊
pygame.init()
# 初始化用于顯示的窗口并設置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 設置當前窗口的標題
pygame.display.set_caption('大球吃小球')
# 設置窗口的背景色(顏色是由紅綠藍三原色構成的元組)
screen.fill((255, 255, 255))
# 通過指定的文件名加載圖像
ball_image = pygame.image.load('./res/ball.png')
# 在窗口上渲染圖像
screen.blit(ball_image, (50, 50))
# 刷新當前窗口(渲染窗口將繪制的圖像呈現出來)
pygame.display.flip()
running = True
# 開啟一個事件循環處理發生的事件
while running:
# 從消息隊列中獲取事件并對事件進行處理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
實現動畫效果
說到動畫這個詞大家都不會陌生,事實上要實現動畫效果,本身的原理也非常簡單,就是將不連續的圖片連續的播放,只要每秒鐘達到了一定的幀數,那么就可以做出比較流暢的動畫效果。如果要讓上面代碼中的小球動起來,可以將小球的位置用變量來表示,并在循環中修改小球的位置再刷新整個窗口即可。
import pygame
def main():
# 初始化導入的pygame中的模塊
pygame.init()
# 初始化用于顯示的窗口并設置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 設置當前窗口的標題
pygame.display.set_caption('大球吃小球')
# 定義變量來表示小球在屏幕上的位置
x, y = 50, 50
running = True
# 開啟一個事件循環處理發生的事件
while running:
# 從消息隊列中獲取事件并對事件進行處理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((255, 255, 255))
pygame.draw.circle(screen, (255, 0, 0,), (x, y), 30, 0)
pygame.display.flip()
# 每隔50毫秒就改變小球的位置再刷新窗口
pygame.time.delay(50)
x, y = x + 5, y + 5
if __name__ == '__main__':
main()
碰撞檢測
通常一個游戲中會有很多對象出現,而這些對象之間的“碰撞”在所難免,比如炮彈擊中了飛機、箱子撞到了地面等。碰撞檢測在絕大多數的游戲中都是一個必須得處理的至關重要的問題,pygame的sprite(動畫精靈)模塊就提供了對碰撞檢測的支持,這里我們暫時不介紹sprite模塊提供的功能,因為要檢測兩個小球有沒有碰撞其實非常簡單,只需要檢查球心的距離有沒有小于兩個球的半徑之和。為了制造出更多的小球,我們可以通過對鼠標事件的處理,在點擊鼠標的位置創建顏色、大小和移動速度都隨機的小球,當然要做到這一點,我們可以把之前學習到的面向對象的知識應用起來。
from enum import Enum, unique
from math import sqrt
from random import randint
import pygame
@unique
class Color(Enum):
"""顏色"""
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (242, 242, 242)
@staticmethod
def random_color():
"""獲得隨機顏色"""
r = randint(0, 255)
g = randint(0, 255)
b = randint(0, 255)
return (r, g, b)
class Ball(object):
"""球"""
def __init__(self, x, y, radius, sx, sy, color=Color.RED):
"""初始化方法"""
self.x = x
self.y = y
self.radius = radius
self.sx = sx
self.sy = sy
self.color = color
self.alive = True
def move(self, screen):
"""移動"""
self.x += self.sx
self.y += self.sy
if self.x - self.radius <= 0 or \
self.x + self.radius >= screen.get_width():
self.sx = -self.sx
if self.y - self.radius <= 0 or \
self.y + self.radius >= screen.get_height():
self.sy = -self.sy
def eat(self, other):
"""吃其他球"""
if self.alive and other.alive and self != other:
dx, dy = self.x - other.x, self.y - other.y
distance = sqrt(dx ** 2 + dy ** 2)
if distance < self.radius + other.radius \
and self.radius > other.radius:
other.alive = False
self.radius = self.radius + int(other.radius * 0.146)
def draw(self, screen):
"""在窗口上繪制球"""
pygame.draw.circle(screen, self.color,
(self.x, self.y), self.radius, 0)
事件處理
可以在事件循環中對鼠標事件進行處理,通過事件對象的type
屬性可以判定事件類型,再通過pos
屬性就可以獲得鼠標點擊的位置。如果要處理鍵盤事件也是在這個地方,做法與處理鼠標事件類似。
def main():
# 定義用來裝所有球的容器
balls = []
# 初始化導入的pygame中的模塊
pygame.init()
# 初始化用于顯示的窗口并設置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 設置當前窗口的標題
pygame.display.set_caption('大球吃小球')
running = True
# 開啟一個事件循環處理發生的事件
while running:
# 從消息隊列中獲取事件并對事件進行處理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 處理鼠標事件的代碼
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# 獲得點擊鼠標的位置
x, y = event.pos
radius = randint(10, 100)
sx, sy = randint(-10, 10), randint(-10, 10)
color = Color.random_color()
# 在點擊鼠標的位置創建一個球(大小、速度和顏色隨機)
ball = Ball(x, y, radius, sx, sy, color)
# 將球添加到列表容器中
balls.append(ball)
screen.fill((255, 255, 255))
# 取出容器中的球 如果沒被吃掉就繪制 被吃掉了就移除
for ball in balls:
if ball.alive:
ball.draw(screen)
else:
balls.remove(ball)
pygame.display.flip()
# 每隔50毫秒就改變球的位置再刷新窗口
pygame.time.delay(50)
for ball in balls:
ball.move(screen)
# 檢查球有沒有吃到其他的球
for other in balls:
ball.eat(other)
if __name__ == '__main__':
main()
上面的兩段代碼合在一起,我們就完成了“大球吃小球”的游戲(如下圖所示),準確的說它算不上一個游戲,但是做一個小游戲的基本知識我們已經通過這個例子告訴大家了,有了這些知識已經可以開始你的小游戲開發之旅了。其實上面的代碼中還有很多值得改進的地方,比如刷新窗口以及讓球移動起來的代碼并不應該放在事件循環中,等學習了多線程的知識后,用一個后臺線程來處理這些事可能是更好的選擇。如果希望獲得更好的用戶體驗,我們還可以在游戲中加入背景音樂以及在球與球發生碰撞時播放音效,利用pygame的mixer和music模塊,我們可以很容易的做到這一點,大家可以自行了解這方面的知識。事實上,想了解更多的關于pygame的知識,最好的教程是pygame的官方網站,如果英語沒毛病就可以趕緊去看看啦。 如果想開發3D游戲,pygame就顯得力不從心了,對3D游戲開發如果有興趣的讀者不妨看看Panda3D。