Python專案實戰(一)《Python程式設計 從入門到實踐》

Eve12345678發表於2020-12-18

專案一、外星人入侵:使用Python開發遊戲

一、武裝飛船

1、規劃專案

開發大型專案時,制定好規劃後再動手編寫程式碼很重要。規劃可確保你不偏離軌道,從而提高專案成功的可能性。
《外星人入侵》遊戲的描述
在遊戲《外星人入侵》中,玩家控制一艘最初出現在螢幕底部中央的飛船。玩家可以使用箭頭鍵左右移動飛船,還可使用空格鍵射擊。遊戲開始時,一群外星人出現在天空中,並向螢幕下方移動。玩家的任務是射殺這些外星人。玩家將所有外星人都消滅乾淨後,將出現一群新的外星人,其移動速度更快。只要有外星人撞到玩家的飛船或到達螢幕底部,玩家就損失一艘飛船。玩家損失三艘飛船後,遊戲結束。

開發的第一個階段就是船艦一艘飛船,它可左右移動,並且能在使用者按空格鍵時開火。

2、開始遊戲專案(已經安裝了Pygame)

  • 建立Pygame視窗及響應使用者輸入。
import sys

import pygame

class AlienInvasion:
	def __init__(self):
		"""管理遊戲資源和行為的類"""
		pygame.init()
		#建立遊戲視窗,指定視窗尺寸為1200X800畫素,並將顯示視窗賦給屬性self.screen
		self.screen = pygame.display.set_mode((1200,800))
		pygame.display.set_caption("Alien Invasion")

	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			#監視鍵盤和滑鼠事件
			for event in pygame.event.get():
				if event.type==pygame.QUIT:
					sys.exit()

			#讓最近繪製的螢幕可見
			pygame.display.flip()

if __name__=='__main__':
	#建立遊戲例項並執行遊戲
	ai = AlienInvasion()
	ai.run_game()

首先匯入模組sys和pygame。模組pygame包含開發遊戲所需的功能,玩家退出時,使用工具sys來退出遊戲。
賦給屬性self.screen的物件是一個surface。在Pygame中,surface是螢幕的一部分,用於顯示遊戲元素。在這個遊戲中每個元素(如外星人或飛船)都是一個surface。pygame.display.set_mode()返回的surface表示整個遊戲的視窗。啟用遊戲動畫迴圈後,每經過一次迴圈都將自動重繪這個surface,將使用者觸發的所有變化都反映出來。
這個遊戲由方法run_game()控制。該方法包含一個不斷執行的while迴圈,而這個迴圈包含一個事件迴圈以及管理螢幕更新的程式碼。事件是使用者玩遊戲時執行的操作,如按鍵或移動滑鼠。為程式響應事件,可編寫一個事件迴圈,以偵聽事件並根據發生的事件型別執行合適的任務。
函式pygame.event.get()用於訪問Pygame檢測到的事件,這個函式返回一個列表,其中包含它在上一次被呼叫後發生的所有之間。所有鍵盤或滑鼠事件都將導致這個for迴圈執行。
函式pygame.display.flip(),命令Pygame讓最近繪製的螢幕可見。它在每次執行while迴圈時都繪製一個空螢幕,並擦去就螢幕,使得只有新螢幕可見。

  • 設定背景色。在alien_invasion.py中設定背景色。
  • 建立設定類。setting類用於將所有設定都儲存在一個地方,以免在程式碼中到處新增設定。這樣,每當需要訪問設定時,只需使用一個設定物件。另外,在專案增大時,這使得修改優秀的外觀和行為更容易:要修改遊戲,只需要修改settings.py中的一些值。
class Settings:
	"""儲存遊戲《外星人入侵》中所有設定的類"""

	def __init__(self):
		"""初始化遊戲的設定"""
		#螢幕設定
		self.screen_width = 1200
		self.screen_height = 800
		self.bg_color = (230,230,230)

修改alien_invasion.py:

import sys

import pygame

from settings import Settings

class AlienInvasion:
	def __init__(self):
		"""管理遊戲資源和行為的類"""
		pygame.init()
		self.settings=Settings()
		self.screen = pygame.display.set_mode(
			(self.settings.screen_width, self.settings.screen_height))
		pygame.display.set_caption("Alien Invasion")

	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			#監視鍵盤和滑鼠事件
			for event in pygame.event.get():
				if event.type==pygame.QUIT:
					sys.exit()

			#每次迴圈時都重繪螢幕
			"""
			fill方法使用背景色填充螢幕,用於處理surface,只接受一種顏色
			"""
			self.screen.fill(self.settings.bg_color)

			#讓最近繪製的螢幕可見
			pygame.display.flip()

if __name__=='__main__':
	#建立遊戲例項並執行遊戲
	ai = AlienInvasion()
	ai.run_game()

3、新增飛船影像

為了在螢幕上繪製玩家的飛船,我們將載入一幅影像,再使用Pygame方法blit()繪製它。
可以使用Pixabay網站提供的免費圖形,無需授權許可即可使用並修改。
在遊戲中機會可以使用任何型別的影像檔案,但使用點陣圖(.bmp)檔案最為簡單,因為Pygame預設載入點陣圖。
選擇影像時,要特別注意背景色,儘可能選擇背景與透明或純色的影像,便於使用影像編輯器將其背景替換為任意顏色。影像的背景色與遊戲的背景色匹配時,遊戲看起來最漂亮。

  • 建立Ship類。負責管理飛船的大部分行為。
import pygame

class Ship:
	"""管理飛船的類"""

	# ai_game是當前AlienInvasion的例項的引用
	def __init__(self, ai_game):
		"""初始化飛船並設定其初始位置"""
		self.screen = ai_game.screen
		self.screen_rect = ai_game.screen.get_rect()

		# 載入飛船影像並獲取其外接矩形
		"""
		pygame.image.load()載入影像,並將飛船影像的位置傳遞給它
		該函式返回一個表示飛船的surface
		"""
		self.image = pygame.image.load('images/ship.bmp')
		"""
		使用get_rect()函式獲取相應surface的屬性rect
		以便後面能夠使用它來指定飛船的位置
		"""
		self.rect = self.image.get_rect()

		# 對於每艘新飛船,都將其放在螢幕底部的中央
		self.rect.midbottom = self.screen_rect.midbottom

	def blitme(self):
		"""在指定位置繪製飛船"""
		self.screen.blit(self.image, self.rect)

Pygame之所以高效,是因為它讓你能夠像處理矩形(rect物件)一樣處理所有的遊戲元素,即便其形狀並非矩形。像處理矩形一樣處理遊戲元素之所以高效,是因為矩形是簡單的幾何形狀。例如,將遊戲元素視為矩形,Pygame能夠更快地判斷出它們是否發生了碰撞。這種做法的效果通常很好,遊戲玩家機會注意不到我們處理的並不是遊戲元素的實際形狀。在這個類中,我們將把飛船和螢幕作為矩形進行處理。
處理rect物件時,可使用矩形四角和中心的x座標和y座標。可以通過設定這些值來指定矩形的位置。要讓遊戲元素居中,可設定相應的rect物件的屬性center、centerx或centery;要讓遊戲元素與螢幕邊緣對齊,可使用屬性top、bottom、left或right。除此之外,還有一些組合屬性,如midbottom、midtop、midleft和midright。要調整遊戲元素的水平或垂直位置,可使用屬性x和y,分別時相應矩形左上角的x座標和y座標。
Notice:在Pygame中,原點(0,0)位於螢幕左上角,向右下方移動時,座標值將增大。在1200x800的螢幕上,原點位於左上角,而右下角的座標為(1200,800)。這些座標對應的是遊戲視窗而不是物理螢幕。

  • 在螢幕上繪製飛船。
import sys

import pygame

from settings import Settings
from ship import Ship

class AlienInvasion:
	def __init__(self):
		"""管理遊戲資源和行為的類"""
		pygame.init()
		self.settings=Settings()
		self.screen = pygame.display.set_mode(
			(self.settings.screen_width, self.settings.screen_height))
		pygame.display.set_caption("Alien Invasion")
		self.ship=Ship(self)

	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			#監視鍵盤和滑鼠事件
			for event in pygame.event.get():
				if event.type==pygame.QUIT:
					sys.exit()

			#每次迴圈時都重繪螢幕
			"""
			fill方法使用背景色填充螢幕,用於處理surface,只接受一種顏色
			"""
			self.screen.fill(self.settings.bg_color)
			self.ship.blitme()

			#讓最近繪製的螢幕可見
			pygame.display.flip()

if __name__=='__main__':
	#建立遊戲例項並執行遊戲
	ai = AlienInvasion()
	ai.run_game()

匯入ship類,並在建立螢幕後建立一個Ship例項。呼叫Ship()時,必須提供一個引數:一個AlienInvasion例項,這裡self指向的是當前AlienInvasion例項。這個引數讓Ship能夠訪問遊戲資源,如物件screen。

4、重構:方法_check_events()和_update_screen()。

在大型專案中,經常需要在新增新程式碼前重構既有程式碼。重構旨在簡化既有程式碼的結構,使其更容易擴充。將越來越長的方法run_game()拆分成兩個輔助方法(helper method)。輔助方法在類中執行任務,但並非是通過例項呼叫的。在Python中,輔助方法的名稱以單個下劃線打頭。

  • 方法_check_events()。將管理事件的程式碼移到名為_check_events()的方法中,以簡化run_game()並隔離事件管理迴圈。通過隔離事件迴圈,可將事件管理與遊戲的其他方面(如更新螢幕)分離。增加check_events()並更改run_game()的程式碼後如下:
	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			self._check_events()

			#每次迴圈時都重繪螢幕
			"""
			fill方法使用背景色填充螢幕,用於處理surface,只接受一種顏色
			"""
			self.screen.fill(self.settings.bg_color)
			self.ship.blitme()

			#讓最近繪製的螢幕可見
			pygame.display.flip()

	def _check_events(self):
		#監視鍵盤和滑鼠事件
		for event in pygame.event.get():
			if event.type==pygame.QUIT:
				sys.exit()
  • 方法_update_screen()。為進一步簡化run_game(),將更新螢幕的程式碼移到一個名為_update_screen()的方法中:
	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			self._check_events()
			self._update_screen()
			
	def _update_screen(self):
		#每次迴圈時都重繪螢幕
		"""
		fill方法使用背景色填充螢幕,用於處理surface,只接受一種顏色
		"""
		self.screen.fill(self.settings.bg_color)
		self.ship.blitme()
		#讓最近繪製的螢幕可見
		pygame.display.flip()

此時已將繪製背景和飛船以及切換螢幕的程式碼移到了方法_update_screen()中。現在很容易看出在每次迴圈中都檢測到了新發生的事件並更新了螢幕。
新手可以先編寫儘可能簡單的程式碼,等專案越來約複雜後對其進行重構。

5、駕駛飛船

編寫程式碼,在使用者按左或右箭頭時做出相應。

  • 響應按鍵。每當使用者按鍵時,都將在Pygame中註冊一個事件。事件都是通過方法pygame.event.get()獲取的,因此需要在方法_check_events()中指定要檢查哪些型別的事件。每次按鍵都被註冊為一個KEYDOWN事件。Pygame檢測到KEYDOWN事件時,需要檢查按下的是否是觸發行動的鍵。
	def _check_events(self):
		#監視鍵盤和滑鼠事件
		for event in pygame.event.get():
			if event.type==pygame.QUIT:
				sys.exit()
			elif event.type == pygame.KEYDOWN:
				if event.key == pygame.K_RIGHT:
					#向右移動飛船
					self.ship.rect.x+=1
  • 允許持續移動。玩家按住右箭頭鍵不放時,我們希望飛船不斷向右移動,直到玩家鬆開為止。結合使用KEYDOWN和KEYUP以及一個名為moving_right的標誌來實現持續移動。當標誌moving_right 為False時,飛船不會移動。玩家按下右箭頭時,將該標誌設為True,在玩家鬆開時將該標誌重新設定為False。
    飛船的屬性都由Ship類控制,因此給這個類新增一個名為moving_right 的屬性和一個update()的方法:
import pygame

class Ship:
	"""管理飛船的類"""

	def __init__(self, ai_game):
		"""初始化飛船並設定其初始位置"""
		self.screen = ai_game.screen
		self.screen_rect = ai_game.screen.get_rect()

		# 載入飛船影像並獲取其外接矩形
		self.image = pygame.image.load('images/ship.bmp')
		self.rect = self.image.get_rect()

		# 對於每艘新飛船,都將其放在螢幕底部的中央
		self.rect.midbottom = self.screen_rect.midbottom

		#移動標誌
		self.moving_right=False

	def update(self):
		if self.moving_right:
			self.rect.x+=1

	def blitme(self):
		"""在指定位置繪製飛船"""
		self.screen.blit(self.image, self.rect)

同時修改_check_events以及run_game:

	def run_game(self):
		"""開始遊戲的主迴圈"""
		while True:
			self._check_events()
			self.ship.update()
			self._update_screen()

	def _check_events(self):
		#監視鍵盤和滑鼠事件
		for event in pygame.event.get():
			if event.type==pygame.QUIT:
				sys.exit()
			elif event.type == pygame.KEYDOWN:
				if event.key == pygame.K_RIGHT:
					#向右移動飛船
					self.ship.moving_right = True
					#玩家鬆開右箭頭
				elif event.type == pygame.KEYUP:
					if event.key == pygame.K_RIGHT:
						self.ship.moving_right = False
  • 左右移動。與右移類似,新增左移邏輯。
  • 調整飛船速度。在Settings類中新增屬性ship_speed,用於控制飛船速度。
  • 限制飛船的活動範圍。飛船不能飛出螢幕之外(0,screen_rect.right)。
  • 重構_check_events()。隨著遊戲的開發,_check_events()方法將越來越長,因此將其部分達目放在兩個方法中,一個處理KEYDOWN事件,一個處理KEYUP事件。
	def _check_events(self):
		#監視鍵盤和滑鼠事件
		for event in pygame.event.get():
			if event.type==pygame.QUIT:
				sys.exit()
			elif event.type == pygame.KEYDOWN:
				self._check_keydown_enents(event)
			elif event.type == pygame.KEYUP:
				self._check_keyup_enents(event)

	def _check_keydown_events(self,event):
		#玩家按下箭頭
		if event.key == pygame.K_RIGHT:
			#向右移動飛船
			self.ship.moving_right = True
		elif event.key == pygame.K_LEFT:
			#向左移動飛船
			self.ship.moving_left = True

	def _check_keyup_events(self,event):
		#玩家鬆開箭頭
		if event.key == pygame.K_RIGHT:
			self.ship.moving_right = False
		elif event.key == pygame.K_LEFT:
			self.ship.moving_left = False
  • 按Q鍵退出。在_check_keydown_events中新增一個程式碼塊,用於在玩家按Q鍵時結束遊戲。
	def _check_keydown_events(self,event):
		#玩家按下箭頭
		if event.key == pygame.K_RIGHT:
			#向右移動飛船
			self.ship.moving_right = True
		elif event.key == pygame.K_LEFT:
			#向左移動飛船
			self.ship.moving_left = True
		elif event.key == pygame.K_q:
			sys.exit()
  • 在全屏模式下執行遊戲。更改__init__()函式如下:
	def __init__(self):
		"""管理遊戲資源和行為的類"""
		pygame.init()
		self.settings=Settings()
		#在全屏模式下執行遊戲
		self.screen = pygame.display.set_mode((0,0),pygame.FULLSCREEN)
		self.settings.screen_width = self.screen.get_rect().width
		self.settings.screen_height = self.screen.get_rect().height
		pygame.display.set_caption("Alien Invasion")
		self.ship=Ship(self)

Notice:在全屏模式下執行這款遊戲之前,請確認能夠按Q鍵退出,因為Pygame預設不提供在全屏模式下退出遊戲的方式。

6、射擊

射擊功能:在玩家按空格鍵時發射子彈(用小矩形表示),子彈將在螢幕中向上飛行,抵達螢幕邊緣後消失。

  • 新增子彈設定。更新Settings類。
		#子彈設定
		self.bullet_speed = 1.0
		self.bullet_width = 3
		self.bullet_height = 15
		self.bullet_color = (60,60,60)
  • 建立bullet類。
import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
	#管理飛船所發射子彈的類

	def __init__(self, ai_game):
		#在飛船當前位置建立一個子彈物件
		super().__init__()
		self.screen = ai_game.screen
		self.settings = ai_game.settings
		self.color = self.settings.bullet_color

		#在(0,0)處建立一個表示子彈的矩形,再設定正確的位置
		self.rect =  pygame.Rect(0,0,self.settings.bullet_width,
			self.settings.bullet_height)
		#將子彈的初始位置設定為飛船的初始位置,這樣子彈就是從飛船頂部出發的
		self.rect.midtop=ai_game.ship.rect.midtop

		#儲存用小數表示的子彈位置
		self.y = float(self.rect.y)

繼承了從模組pygame.sprite匯入的Sprite類。通過使用精靈(sprite),可將遊戲中相關的元素編組,進而同時操作編組中的所有元素。為建立子彈例項,init()還需要當前的AlienInvasion例項,還呼叫了super()來繼承Sprite。
接下來寫好管理子彈位置以及繪製子彈的方法:

	def update(self):
		self.y-=self.settings.bullet_speed
		self.rect.y=self.y

	def draw_bullet(self):
		pygame.draw.rect(self.screen,self.color,self.rect)
  • 將子彈儲存到編組中。定義Bullet類和必要的設定後,便可編寫在玩家每次按空格鍵時都射出一發子彈了。在AlienInvasion中建立一個編組(group),用於儲存所有有效的子彈,以便管理髮射出去的所有子彈。
    這個編組是pygame.sprite.Group的一個例項,它類似於列表,但提供了有助於開發遊戲的額外功能。在主迴圈中,將使用這個編組在螢幕上繪製子彈以及更新每顆子彈的位置。
    在AlienInvasion中為子彈建立編組,並在run_game()的迴圈中更新子彈的位置。當編組呼叫update()時,編組自動對其中的每個精靈呼叫update()。
  • 開火。在AlienInvasion中,需要修改_check_keydown_events(),以便玩家按空格鍵時發射一顆子彈。
  • 刪除消失的子彈。檢測子彈的bottom屬性是否為零,如果是,則表明子彈已飛過螢幕頂端,將其刪除。
  • 限制子彈的數量。為了鼓勵玩家有目標地進行設計,可以限制螢幕上子彈出現的數量。
  • 建立方法_update_bullets()。編寫並檢查子彈管理程式碼後,將其移動到一個獨立的方法中。

二、外星人來了

開發大型專案時,要在進入每個階段之前回顧以下開發計劃,搞清楚接下來要通過程式碼來完成哪些任務。
在專案中新增新功能,還應稽核既有程式碼。每進入一個新階段,專案通常會更復雜,因此,最好對混亂或低效的程式碼進行清理(重構)。

1、建立第一個外星人

像建立Ship類那樣建立Alien類,出於簡化考慮,也將使用點陣圖來表示外星人。

import pygame
from pygame.sprite import Sprite

class Alien(Sprite):
	def __init__( self, ai_game):
		#初始化為行人並設定其起始位置
		super().__init__()
		self.screen = ai_game.screen

		#載入外星人影像並設定其rect屬性
		self.image = pygame.image.load('images/alien.bmp')
		self.rect = self.image.get_rect()

		#每個外星人最初都在螢幕左上角附近
		self.rect.x = self.rect.width
		self.rect.y = self.rect.height

		#儲存外星人的精確水平位置
		self.x = float(self.rect.x)
  • 建立Alien例項。為了讓第一個外星人在螢幕上現身,需要建立一個Alien例項。這屬於設定工作,因此將把這些程式碼放在AlienInvasion類的方法__init__()末尾。最終會建立一群外星人,涉及的工作量不少,因此將新建一個名為create_fleet()的輔助方法。
	def _create_fleet(self):
		#建立外星人群
		#首先建立一個外星人
		alien = Alien(self)
		self.aliens.add(alien)

2、建立一群外星人

要繪製一群外星人,需要確定一行能容納多少外星人以及要繪製多少行。首先計算外星人的水平間距並建立一行外星人,再確定可用的垂直空間並建立一群外星人。

  • 確定一行可容納多少外星人。螢幕兩邊要有邊距(外星人的寬度),外星人之間的間距也是外星人的寬度,//為整除運算子,結果得到整數。
avaliable_space_x = self.settings.screen_width - (2*alien_width)
number_aliens_x = avaliable_space_x // (2*alien_width)
  • 建立一行外星人。重寫_create_fleet()函式。

相關文章