Python開發接水果小游戲


我研發的Python游戲引擎Pylash已經更新到1.4了。現在我們就來使用它完成一個極其簡單的小游戲:接水果。以下是游戲截圖:

接水果 游戲截圖

游戲操作說明:點擊屏幕左右兩邊或者使用鍵盤方向鍵控制人物移動,使人物與水果接觸得分,碰到非水果的物品,如碎玻璃,就會game over。

接下來是詳盡的開發過程,篇幅較長,請看官耐心閱讀。

Pylash項目地址

由於本次開發用到了pylash,大家可以先去Github上對引擎進行了解。
https://github.com/yuehaowang/pylash_engine

創建項目

首先在工作目錄創建一個名為get_fruits的目錄。然后到Github下載Pylash。引擎是基於Python3和PyQt4構建的,所以在使用前請確保你使用的是Python3並且安裝了PyQt4。如果沒有,可以在上述項目地址中找到他們的相關網頁鏈接進行下載安裝,安裝和配置步驟都十分簡單。這里不再贅述。

下載完Pylash后,我們得到這樣的目錄結構:

+- pylash_engine/
|
+- pylash/
|
+- demo/
|
+- examples/

大家可以在demo/examples/兩個目錄下查看示例。本文的源代碼可以在examples/get_fruits中找到。

pylash目錄就是引擎源代碼。接下來把這個目錄復制到我們創建的get_fruits目錄下,再在get_fruits目錄下創建一個images目錄,用於儲存圖片。最后創建一個Main.py文件。這時,我們的get_fruits目錄結構如下:

+- get_fruits/
|
+- pylash/
|
+- images/
|
+- Main.py

然后將引擎目錄plash_engine/examples/get_fruits/images/下圖片復制到項目目錄get_fruits/images/下,用作游戲素材。

images/目錄 截圖

這樣一來,我們的項目就創建好了,接下來只用往Main.py里填寫代碼,然后運行即可。

編寫Hello World小程序

用代碼編輯器(推薦Sublime Text)打開Main.py文件,寫入以下代碼:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from pylash.utils import init, addChild
from pylash.text import TextField

def main():
# 創建文本顯示對象
txt = TextField()
# 設置文本內容
txt.text = "Hello World"
# 設置文本顏色
txt.textColor = "red"
# 設置文本位置
txt.x = 50
txt.y = 100
# 設置文本字體大小
txt.size = 50
# 將文本對象加入到最底層
addChild(txt)

# 初始化窗口,參數:界面刷新時間(單位:毫秒),窗口標題,窗口寬度,窗口高度,初始化完畢后回調函數
init(1000 / 60, "Hello World", 800, 600, main)

運行Main.py,如果得到了如下圖所示的界面,說明程序正常運轉起來了。

Hello World 演示截圖

大家可以結合注釋初步認識Pylash。熟悉flash的同學不難發現,TextField就是flash里顯示文本的類,而且用法十分相近。
我們從代碼的第4行看起,這里我們引入了pylash中的一些函數和類。pylash提供了很多模塊,大家可以到這里查看它們的簡介。
再往下看,我們會發現,pylash提供了一個用於顯示文本的類,通過設置這個類的不同屬性來設定文本樣式。最后使用addChild將文本顯示對象加入到界面中。我們可以把游戲看作分為很多層:地圖層、人物層、UI層……,通過分層我們就能實現層次化顯示效果。比如人物一直是在地圖上方顯示的,那么人物層就在地圖層上方。addChild函數就是把一個顯示對象加到最底層。
最后,我們使用init函數初始化窗口。

Pylash提供了許多基礎顯示對象,除了TextField文本顯示類,還有Bitmap圖片顯示類,Sprite精靈類等。下文會提及。

編寫游戲

有了上述對pylash的大致了解,我們就可以開始編寫游戲了。首先,刪除第四行以后所有代碼。

引入所需

首先引入我們所需的所有類和函數,修改Main.py

from pylash.utils import stage, init, addChild, KeyCode
from pylash.system import LoadManage
from pylash.display import Sprite, BitmapData, Bitmap, FPS
from pylash.text import TextField, TextFormatWeight
from pylash.events import MouseEvent, Event, KeyboardEvent
from pylash.ui import LoadingSample1

這些類和函數在下面的代碼中都會被用到。由於我是提前寫好了游戲,所以在這里把這部分代碼一塊兒貼出來了,大家使用的時候可以根據自己使用情況,每用一個引入一個。

全局變量

游戲中需要用到一些全局變量,大家可以先瀏覽一遍,不同知道它們是干什么的,后文會用到它們:

dataList = {}

stageLayer = None
player = None
itemLayer = None
scoreTxt = None
addItemSpeed = 40
addItemSpeedIndex = 0
score = 0
keyboardEnabled = False

加載資源

我們的游戲中要用到圖片,所以要提前加載圖片(存儲於images/目錄下)。加載圖片我們用到LoadManage靜態類和LoadingSample1進度條類(還有LoadingSample2LoadingSample3這兩款不同樣式的進度條。或者大家深入學習后,可以自己寫一個進度條類)。修改main函數:

def main():
# 資源列表,一個list對象,格式:{"name" : 資源名稱, "path" : 資源路徑}
loadList = [
{"name" : "player", "path" : "./images/player.png"},
{"name" : "bg", "path" : "./images/bg.jpg"},
{"name" : "item0", "path" : "./images/item0.png"},
{"name" : "item1", "path" : "./images/item1.png"},
{"name" : "item2", "path" : "./images/item2.png"},
{"name" : "item3", "path" : "./images/item3.png"},
{"name" : "item4", "path" : "./images/item4.png"},
{"name" : "item5", "path" : "./images/item5.png"},
{"name" : "item6", "path" : "./images/item6.png"},
{"name" : "item7", "path" : "./images/item7.png"}
]

# 創建進度條
loadingPage = LoadingSample1()
addChild(loadingPage)

# 加載完成后調用的函數,接受一個參數,該參數是一個dict對象,通過result[資源名稱]來獲取加載完成的資源
def loadComplete(result):
# 調用remove方法從界面上移除自身
loadingPage.remove()

# 調用初始化游戲函數
gameInit(result)

# 加載文件,參數:資源列表,每加載完一個資源回調函數(多用於顯示進度),加載完所有資源回調函數
LoadManage.load(loadList, loadingPage.setProgress, loadComplete)

上述代碼含有詳細注釋,理解起來應該不算困難。可以看到,我們使用LoadManage.load實現加載。LoadingSample1.setProgress用於設置顯示進度。

創建開始界面

我們在main函數中調用了gameInit函數,所以添加該函數:

def gameInit(result):
global dataList, stageLayer

# 保存加載完成的資源,這樣一來,就可以使用dataList[資源名稱]來獲取加載完成的資源
dataList = result

# 創建舞台層
stageLayer = Sprite()
addChild(stageLayer)

# 加入FPS,方便查看游戲效率
fps = FPS()
addChild(fps)

# 加入背景圖片
bg = Bitmap(BitmapData(dataList["bg"]))
stageLayer.addChild(bg)

# 加入文本
titleTxt = TextField()
titleTxt.text = "Get Furit"
titleTxt.size = 70
titleTxt.textColor = "red"
titleTxt.x = (stage.width - titleTxt.width) / 2
titleTxt.y = 100
stageLayer.addChild(titleTxt)

hintTxt = TextField()
hintTxt.text = "Tap to Start the Game!~"
hintTxt.textColor = "red"
hintTxt.size = 40
hintTxt.x = (stage.width - hintTxt.width) / 2
hintTxt.y = 300
stageLayer.addChild(hintTxt)

engineTxt = TextField()
engineTxt.text = "- Powered by Pylash -"
engineTxt.textColor = "red"
engineTxt.size = 20
engineTxt.weight = TextFormatWeight.BOLD
engineTxt.italic = True
engineTxt.x = (stage.width - engineTxt.width) / 2
engineTxt.y = 500
stageLayer.addChild(engineTxt)

# 加入鼠標點擊事件:點擊舞台層后,開始游戲
stageLayer.addEventListener(MouseEvent.MOUSE_UP, startGame)

# 加入鍵盤事件:用於控制游戲中的人物
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown)
stage.addEventListener(KeyboardEvent.KEY_UP, keyUp)

def startGame(e):
print("start game")

def keyDown(e):
print("key down")

def keyUp(e):
print("key up")

上述代碼中,我們需要突破以下幾個難點:

  1. Sprite精靈類。Sprite是一個精靈類。可是什么是精靈?其實你可以把它理解為一個層。它擁有addChild方法,用於把顯示對象加到自身這個層上(和全局的addChild函數類似)。當然Sprite不只是有層的功能,不過你姑且把它看作一個層吧。

  2. BitmapBitmapData類的使用。Bitmap在上文中提到是一個用於顯示圖片的類。和TextField一樣,使用addChild將它加入界面。BitmapData類是用於儲存圖像數據的,它接收的參數就是加載完成的圖片資源。將他作為參數傳給Bitmap類的構造器,就能創建出圖片。BitmapData還可以進行像素操作,不過這是較高級的功能,目前不用了解。

  3. 事件。在pylash中,使用addEventListener統一接口加入事件,該方法參數:事件類型,事件監聽器(即事件回調函數)。什么是事件呢?類似於一個信號,這個信號在某種情況下被發送后,指定的信號監聽器就會被觸發。這里添加鼠標事件的addEventListener是在EventDispatcher中定義的,DisplayObject類繼承自EventDispatcher,所以繼承自DisplayObject的所有類,都能加入事件。不過只有Sprite才有觸發鼠標事件的能力。所以我們給stageLayer(舞台層,一個Sprite對象)加入了鼠標點擊事件(MouseEvent.MOUSE_UP)。對應addEventListener方法的有removeEventListener(移除事件,參數相同)。鼠標事件除了MouseEvent.MOUSE_UP(鼠標彈起),還有MouseEvent.MOUSE_DOWN(鼠標按下),MouseEvent.MOUSE_MOVE(鼠標移動),MouseEvent.MOUSE_OUT(鼠標移出)等事件。后文會用到一些。事件的監聽器是一個函數,startGamekeyDownkeyUp它們都是事件監聽器。監聽器在事件觸發時被調用,並接受一個事件數據參數(通常寫為e),通過這個參數可以獲取事件的一些信息,如鼠標事件的監聽器可以通過該參數獲取鼠標位置。

  4. stage全局類。這里的stage是一個全局類,用於管理整個窗口,比如設置窗口刷新速度、獲取窗口尺寸(stage.width,stage.height),有點類似於JavaScript里的window。鍵盤事件總不能加到某個對象上吧,所以stage還能加入鍵盤事件。加入鍵盤事件同樣使用addEventListener這個的統一接口。

最后加入init函數初始化窗口:

init(1000 / 60, "Get Fruits", 800, 600, main)

init函數中,值得注意的是第一個參數,上文代碼的注釋中解釋的是“界面刷新時間”,也就是說我們的界面是在不斷刷新重繪的。這個參數就是用來決定刷新的時間。參數值越小,刷新得越快,游戲越流暢,不過也不用設置得太小,太小了話,刷新速度過快,設備會跟不上這個節奏的。玩過游戲的朋友可以這么理解這個參數,用1000除以這個參數,得到的就是FPS。

運行Main.py,得到如下界面:

接水果開始界面 截圖

可以看到,我們的界面上有圖片也有文本。點擊界面輸出“start game”,按下鍵盤輸出“key down”,釋放鍵盤輸出“key up”。這樣一來,我們就成功地添加了顯示對象和鼠標&鍵盤事件。

開始游戲

舞台層鼠標點擊事件的監聽器是startGame函數,也就是說,我們點擊開始界面就開始游戲。修改startGame函數:

def startGame(e):
global player, itemLayer, scoreTxt, addItemSpeedIndex, score, keyboardEnabled

# 初始一些全局變量
addItemSpeedIndex = 0
score = 0

keyboardEnabled = True

# 清空舞台層和舞台事件
stageLayer.removeAllChildren()
stageLayer.removeAllEventListeners()

# 加入背景
bg = Bitmap(BitmapData(dataList["bg"]))
stageLayer.addChild(bg)

# 創建角色
player = Player(dataList["player"])
player.x = (stage.width - player.width) / 2
player.y = 450
stageLayer.addChild(player)

# 創建下落物品層
itemLayer = Sprite()
stageLayer.addChild(itemLayer)
# 將人物對象保存到itemLayer中,用於檢測碰撞
itemLayer.hitTarget = player

# 加入分數文本
scoreTxt = TextField()
scoreTxt.text = "Score: 0"
scoreTxt.textColor = "red"
scoreTxt.size = 30
scoreTxt.x = scoreTxt.y = 30
scoreTxt.weight = TextFormatWeight.BOLDER
stageLayer.addChild(scoreTxt)

# 加入事件
stageLayer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown)
stageLayer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp)
stageLayer.addEventListener(Event.ENTER_FRAME, loop)

def onMouseDown(e):
print("mouse down")

def onMouseUp(e):
print("mouse up")

def loop(e):
print("loop")

對應addChildSprite提供了removeChild方法用於移除顯示對象。除此之外還有removeAllChildren移除所有對象方法。removeAllEventListeners顧名思義就是移除所有事件。上面的代碼會讓人一頭霧水,同樣的,我們需要突破以下難關:

  1. 全局變量。addItemSpeedIndex是用於控制添加下落物品的時間間隔,后文會提及。score是保存分數的變量。由於游戲開始后,這些變量要回到初始值,所以在startGame函數中添加了這些代碼來完成這項任務。keyboardEnabled = True這行代碼是用於打開鍵盤事件,鍵盤事件是加到stage對象上的(見上文),但是是用於操作游戲中主角的,所以只有在游戲開始后才有用,所以加入keyboardEnabled變量作為能否使用鍵盤的開關,后文修改鍵盤事件監聽器時會用到它。

  2. Player類。這個類是我們要自己創建的人物類,后文會展示其代碼。

  3. 時間軸事件ENTER_FRAME。我們了解了鼠標事件,認識MouseEvent.MOUSE_DOWNMouseEvent.MOUSE_UP,可是Event.ENTER_FRAME是什么東西-_-#?這個事件就是時間軸事件。時間軸事件類似於一個計時器。這個事件的監聽器每隔段事件就會觸發。事件觸發的時間間隔取決於init函數的第一個參數。

運行代碼,點擊開始界面開始游戲,你可以發現控制台在不停地輸出“loop”,代表時間軸事件運轉了。

Player人物類

上文提到了這個類,在寫這個類之前,我們重新在get_fruits/目錄下創建一個名為Player.py的python文件。創建完成后,打開這個文件,加入以下代碼:

from pylash.utils import stage
from pylash.display import Sprite, Animation, BitmapData

# 創建Player類,並使其繼承自Sprite類
class Player(Sprite):
def __init__(self, playerImage):
super(Player, self).__init__()

# 移動方向,【right向右,left向左,None不移動】
self.direction = None
# 移動速度
self.step = 5

# 創建圖片數據
bmpd = BitmapData(playerImage)
# 創建動畫幀列表
frames = Animation.divideUniformSizeFrames(bmpd.width, bmpd.height, 4, 4)

# 創建動畫
self.animation = Animation(bmpd, frames)
# 設置動畫播放速度
self.animation.speed = 5
# 播放動畫
self.animation.play()
# 將動畫加入界面
self.addChild(self.animation)

def loop(self):
# 向右移動
if self.direction == "right":
self.x += self.step
# 播放向右移動時的動畫
self.animation.currentRow = 2
# 向左移動
elif self.direction == "left":
self.x -= self.step
# 播放向左移動時的動畫
self.animation.currentRow = 1
# 不移動
else:
# 播放不移動時的動畫
self.animation.currentRow = 0

# 限制人物位置
if self.x < 0:
self.x = 0
elif self.x > stage.width - self.width:
self.x = stage.width - self.width

這個Player類需要繼承自Sprite,使其成為一個顯示對象。也就是說繼承自Sprite后,就可以被addChild到界面上去了,並可以顯示出來。除此之外,還可以使用Player對象的addChild方法來向人物類添加顯示元件。Player類的構造器接收一個人物圖片參數。
代碼中用到了Animation類。它由pylash提供,用於創建簡單的基於圖片的動畫。Animation構造器接收兩個參數:動畫位圖數據,動畫幀列表。

一般而言,我們的動畫用的圖片都是這樣的:

動畫圖片

所以我們播放動畫的時候,只需要控制位圖顯示區域的大小和位置就能實現播放動畫。類似於放映機放映電影。如下兩幅圖所示,顯示區域就是空白部分,不被顯示的區域就是被半透明黑色幕布遮住的部分。動畫中的每個小圖叫幀,移動顯示區域就實現切換幀,達到播放動畫的目的。

動畫播放示例之一

動畫播放示例之二

代碼中的Animation.divideUniformSizeFrames(bmpd.width, bmpd.height, 4, 4)就是用於獲取每幀的位置和大小。divideUniformSizeFrames靜態方法接收四個參數:動畫圖片寬度,動畫圖片高度,動畫列數,動畫行數。該方法只適合得到每幀分布和大小都是均勻的幀列表。

Animation有個speed屬性,用於控制動畫播放速度。如果不設置這個屬性,動畫中每幀的切換速度就和init中設置的刷新速度一樣。設置后,切換速度變為speed * 刷新速度。

Animation類默認只播放第一排第一行動畫,要指定動畫播放的位置,需要設置currentRowcurrentColumn屬性來控制播放的行和列。

下落物品類:Item

這個類在前面沒出現過,不過我們先寫好放在這里,下文要用到。同樣的,新建一個名為Item.py的文件,打開它,寫入代碼:

from pylash.utils import stage
from pylash.display import Sprite, Bitmap, BitmapData

class Item(Sprite):
# 定義自定義事件
EVENT_ADD_SCORE = "event_add_score"
EVENT_GAME_OVER = "event_game_over"

def __init__(self, image):
super(Item, self).__init__()

bmp = Bitmap(BitmapData(image))
self.addChild(bmp)

self.index = 0
self.y = -bmp.height / 2

def loop(self):
player = None

# 獲取人物對象
if self.parent:
player = self.parent.hitTarget

if player is None:
return

# 向下移動
self.y += 5

# 碰撞檢測
if (abs(self.x + self.width / 2 - player.x - player.width / 2) <= (self.width + player.width) / 2) and (abs(self.y + self.height / 2 - player.y - player.height / 2) <= (self.height + player.height) / 2):
# 如果index <= 3,代表物品是水果
if self.index <= 3:
# 觸發自定義事件:加分事件
self.dispatchEvent(Item.EVENT_ADD_SCORE)

self.remove()
# 如果物品是非水果
else:
# 觸發自定義事件:游戲結束
self.dispatchEvent(Item.EVENT_GAME_OVER)

# 移除自身,當自身移出了屏幕
if self.y >= stage.height:
self.remove()

Item類的構造器和Player構造器一樣,接受一個圖片參數。
我們這里用到了一個比較高級的功能:自定義事件。自定的事件可以是一個字符串,作為該事件的標識。使用dispatchEvent方法觸發事件。dispatchEvent方法在EventDispatcher中定義,通過繼承使Item也能使用這個方法。
值得關注的還有檢測碰撞部分。目前處理簡單的矩形碰撞即可。首先來看張圖:

矩形碰撞檢測演示圖

如果要橫向判斷碰撞的話,判斷(x1-x2)的絕對值是否小於或者等於w1/2+w2/2,如果是則橫向則有碰撞。縱向判斷是一樣的,判斷(y1-y2)的絕對值是否小於或等於h1/2+h2/2即可。

修改事件監聽器

上面的代碼中我們雖然添加了事件,但是沒有添加有效的事件監聽器,所以修改這些函數:

def keyDown(e):
global player

if not keyboardEnabled or not player:
return

if e.keyCode == KeyCode.KEY_RIGHT:
player.direction = "right"
elif e.keyCode == KeyCode.KEY_LEFT:
player.direction = "left"

def keyUp(e):
global player

if not keyboardEnabled or not player:
return

player.direction = None

def onMouseDown(e):
global player

if e.offsetX > (stage.width / 2):
player.direction = "right"
else:
player.direction = "left"

def onMouseUp(e):
global player

player.direction = None

def loop(e):
global player, itemLayer, addItemSpeed, addItemSpeedIndex

player.loop()

for o in itemLayer.childList:
o.loop()

# 控制添加下落物品時間間隔
if addItemSpeedIndex < addItemSpeed:
addItemSpeedIndex += 1

return

addItemSpeedIndex = 0

# 獲得隨機下落物品
randomNum = random.randint(0, 7)

# 加入下落物品
item = Item(dataList["item" + str(randomNum)])
item.index = randomNum
item.x = int(random.randint(30, stage.width - 100))
itemLayer.addChild(item)
# 加入自定義的事件
item.addEventListener(Item.EVENT_ADD_SCORE, addScore)
item.addEventListener(Item.EVENT_GAME_OVER, gameOver)

keyDownkeyUponMouseDownonMouseUp這四個監聽器用於操作人物(player)。

接下來看監聽器loop。該函數中,首先調用了人物的loop方法(見Player類的loop)。我們在上文定義的itemLayer是一個Sprite對象,Sprite對象有一個childList屬性,是一個list對象,保存了所有的子對象。所以我們通過遍歷itemLayer的這個列表,獲取每個下落物品,調用它們的loop方法。接下來使用addItemSpeedIndexaddItemSpeed兩個全局變量控制加入下落物品的速度。接下來的代碼就是來構造Item類創建下落物品。

加分和Game Over

我們給Item對象加入了自定義事件,分別觸發addScoregameOver監聽器,加入這兩個監聽器:

def addScore(e):
global score, scoreTxt

score += 1

scoreTxt.text = "Score: %s" % score

def gameOver(e):
global player, scoreTxt, stageLayer, keyboardEnabled

keyboardEnabled = False

stageLayer.removeAllEventListeners()

scoreTxt.remove()
player.animation.stop()

resultTxt = TextField()
resultTxt.text = "Final Score: %s" % score
resultTxt.size = 40
resultTxt.weight = TextFormatWeight.BOLD
resultTxt.textColor = "orangered"
resultTxt.x = (stage.width - resultTxt.width) / 2
resultTxt.y = 250
stageLayer.addChild(resultTxt)

hintTxt = TextField()
hintTxt.text = "Double Click to Restart"
hintTxt.size = 35
hintTxt.textColor = "red"
hintTxt.x = (stage.width - hintTxt.width) / 2
hintTxt.y = 320
stageLayer.addChild(hintTxt)

# 加入雙擊事件,點擊后重新開始游戲
stageLayer.addEventListener(MouseEvent.DOUBLE_CLICK, startGame)

運行Main.py,開始游戲后,得到本文開篇圖片所示效果。移動人物,接觸下落的物品。如果碰到碎玻璃等非水果物品就會game over:

接水果游戲Game Over 截圖

Ok,我們的接水果小游戲就完成了。可見使用python+pylash開發小游戲還是很方便的。

源代碼

本文的源代碼可以在引擎目錄的examples/get_fruits中找到。或者到這里在線查看。

文中有任何不妥之處或者讀者有疑問的話,歡迎大家交流~


歡迎大家繼續關注我的博客

轉載請注明出處:Yorhom’s Game Box

http://blog.csdn.net/yorhomwang


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com