pygame

用Python和Pygame寫遊戲-從入門到精通(1)

 

Pygame的歷史css

 

Pygame是一個利用SDL庫的寫就的遊戲庫,SDL呢,全名Simple DirectMedia Layer,是一位叫作Sam Lantinga的大牛寫的,聽說他爲了讓Loki(致力於向Linux上移植Windows的遊戲的一家大好人公司,惋惜已經倒閉,唉好人不長命啊……)更有效的工做,創造了這個東東。html

SDL是用C寫的,不過它也能夠使用C++進行開發,固然還有不少其它的語言,Pygame就是Python中使用它的一個庫。Pygame已經存在不少時間了,許多優秀的程序員加入其中,把Pygame作得愈來愈好。python

安裝Pygame程序員

你能夠從www.pygame.org下載Pygame,選擇合適你的操做系統和合適的版本,而後安裝就能夠了(什麼,你連Python都沒有?您多是不適合看這個系列了,不過若是執意要學,很好!快去www.python.org下載吧!)。 一旦你安裝好,你能夠用下面的方法確認下有沒有安裝成功:算法

Python編程

>>> import pygame >>> print pygame.ver 1.9.1release 小程序

 

使用Pygamewindows

Pygame有不少的模塊,下面是一張一覽表:後端

模塊名數組

功能

pygame.cdrom

訪問光驅

pygame.cursors

加載光標

pygame.display

訪問顯示設備

pygame.draw

繪製形狀、線和點

pygame.event

管理事件

pygame.font

使用字體

pygame.image

加載和存儲圖片

pygame.joystick

使用遊戲手柄或者 相似的東西

pygame.key

讀取鍵盤按鍵

pygame.mixer

聲音

pygame.mouse

鼠標

pygame.movie

播放視頻

pygame.music

播放音頻

pygame.overlay

訪問高級視頻疊加

pygame

就是咱們在學的這個東西了……

pygame.rect

管理矩形區域

pygame.sndarray

操做聲音數據

pygame.sprite

操做移動圖像

pygame.surface

管理圖像和屏幕

pygame.surfarray

管理點陣圖像數據

pygame.time

管理時間和幀信息

pygame.transform

縮放和移動圖像

有些模塊可能在某些平臺上不存在,你能夠用None來測試一下。

Python

if pygame.font is None: print "The font module is not available!" exit()

新的Hello World

學程序一開始咱們總會寫一個Hello world程序,但那只是在屏幕上寫了兩個字,如今咱們來點更帥的!寫好之後會是這樣的效果:

 

 

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

#!/usr/bin/env python

 

background_image_filename = 'sushiplate.jpg'

mouse_image_filename = 'fugu.png'

#指定圖像文件名稱

 

import pygame

#導入pygame庫

from pygame.locals import *

#導入一些經常使用的函數和常量

from sys import exit

#向sys模塊借一個exit函數用來退出程序

 

pygame.init()

#初始化pygame,爲使用硬件作準備

 

screen = pygame.display.set_mode((640, 480), 0, 32)

#建立了一個窗口

pygame.display.set_caption("Hello, World!")

#設置窗口標題

 

background = pygame.image.load(background_image_filename).convert()

mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

#加載並轉換圖像

 

while True:

#遊戲主循環

 

    for event in pygame.event.get():

        if event.type == QUIT:

            #接收到退出事件後退出程序

            exit()

 

    screen.blit(background, (0,0))

    #將背景圖畫上去

 

    x, y = pygame.mouse.get_pos()

    #得到鼠標位置

    x-= mouse_cursor.get_width() / 2

    y-= mouse_cursor.get_height() / 2

    #計算光標的左上角位置

    screen.blit(mouse_cursor, (x, y))

    #把光標畫上去

 

    pygame.display.update()

    #刷新一下畫面

這個程序須要兩張圖片,你能夠在這篇文章最後的地方找到下載地址,雖然你也能夠隨便找兩張。爲了達到最佳效果,背景的 sushiplate.jpg應要有640×480的分辨率,而光標的fugu.png大約應爲80×80,並且要有Alpha通道(若是你不知道這是什麼,仍是下載吧……)。
注意:代碼中的註釋我使用的是中文,若是執行報錯,能夠直接刪除。

遊戲中我已經爲每一行寫了註釋,另外若是打算學習,強烈建議本身動手輸入一遍而不是複製粘貼!

稍微講解一下比較重要的幾個部分:

set_mode會返回一個Surface對象,表明了在桌面上出現的那個窗口,三個參數第一個爲元祖,表明分 辨率(必須);第二個是一個標誌位,具體意思見下表,若是不用什麼特性,就指定0;第三個爲色深。

標誌位

功能

FULLSCREEN

建立一個全屏窗口

DOUBLEBUF

建立一個「雙緩衝」窗口,建議在HWSURFACE或者OPENGL時使用

HWSURFACE

建立一個硬件加速的窗口,必須和FULLSCREEN同時使用

OPENGL

建立一個OPENGL渲染的窗口

RESIZABLE

建立一個能夠改變大小的窗口

NOFRAME

建立一個沒有邊框的窗口

convert函數是將圖像數據都轉化爲Surface對象,每次加載完圖像之後就應該作這件事件(事實上由於 它太經常使用了,若是你不寫pygame也會幫你作);convert_alpha相比convert,保留了Alpha 通道信息(能夠簡單理解爲透明的部分),這樣咱們的光標才能夠是不規則的形狀。

遊戲的主循環是一個無限循環,直到用戶跳出。在這個主循環裏作的事情就是不停地畫背景和更新光標位置,雖然背景是不動的,咱們仍是須要每次都畫它, 不然鼠標覆蓋過的位置就不能恢復正常了。

blit是個重要函數,第一個參數爲一個Surface對象,第二個爲左上角位置。畫完之後必定記得用update更新一下,不然畫面一片漆黑。

 

 

理解事件

事件是什麼,其實從名稱來看咱們就能想到些什麼,並且你所想到的基本就是事件的真正意思了。咱們上一個程序,會一直運行下去,直到你關閉窗口而產生了一個QUIT事件,Pygame會接受用戶的各類操做(好比按鍵盤,移動鼠標等)產生事件。事件隨時可能發生,並且量也可能會很大,Pygame的作法是把一系列的事件存放一個隊列裏,逐個的處理。

事件檢索

上個程序中,使用了pygame.event.get()來處理全部的事件,這好像打開大門讓全部的人進入。若是咱們使用pygame.event.wait(),Pygame就會等到發生一個事件才繼續下去,就好像你在門的貓眼上盯着外面同樣,來一個放一個……通常遊戲中不太實用,由於遊戲每每是須要動態運做的;而另一個方法pygame.event.poll()就好一些,一旦調用,它會根據如今的情形返回一個真實的事件,或者一個「什麼都沒有」。下表是一個經常使用事件集:

事件

產生途徑

參數

QUIT

用戶按下關閉按鈕

none

ATIVEEVENT

Pygame被激活或者隱藏

gain, state

KEYDOWN

鍵盤被按下

unicode, key, mod

KEYUP

鍵盤被放開

key, mod

MOUSEMOTION

鼠標移動

pos, rel, buttons

MOUSEBUTTONDOWN

鼠標按下

pos, button

MOUSEBUTTONUP

鼠標放開

pos, button

JOYAXISMOTION

遊戲手柄(Joystick or pad)移動

joy, axis, value

JOYBALLMOTION

遊戲球(Joy ball)?移動

joy, axis, value

JOYHATMOTION

遊戲手柄(Joystick)?移動

joy, axis, value

JOYBUTTONDOWN

遊戲手柄按下

joy, button

JOYBUTTONUP

遊戲手柄放開

joy, button

VIDEORESIZE

Pygame窗口縮放

size, w, h

VIDEOEXPOSE

Pygame窗口部分公開(expose)?

none

USEREVENT

觸發了一個用戶事件

code

若是你想把這個表如今就背下來,固然我不會阻止你,但實在不是個好主意,在實際的使用中,天然而然的就會記住。咱們先來寫一個能夠把全部方法輸出的程序,它的結果是這樣的。

咱們這裏使用了wait(),由於這個程序在有事件發生的時候動彈就能夠了。還用了font模塊來顯示文字(後面會講的),下面是源代碼:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

SCREEN_SIZE = (640, 480)

screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

 

font = pygame.font.SysFont("arial", 16);

font_height = font.get_linesize()

event_text = []

 

while True:

 

    event = pygame.event.wait()

    event_text.append(str(event))

    #得到時間的名稱

    event_text = event_text[-SCREEN_SIZE[1]/font_height:]

    #這個切片操做保證了event_text裏面只保留一個屏幕的文字

 

    if event.type == QUIT:

        exit()

 

    screen.fill((255, 255, 255))

 

    y = SCREEN_SIZE[1]-font_height

    #找一個合適的起筆位置,最下面開始可是要留一行的空

    for text in reversed(event_text):

        screen.blit( font.render(text, True, (0, 0, 0)), (0, y) )

        #之後會講

        y-=font_height

        #把筆提一行

 

    pygame.display.update()


書上說,若是你把填充色的(0, 0, 0)改成(0, 255, 0),效果會想黑客帝國的字幕雨同樣,我得說,實際試一下並不太像……不過之後你徹底能夠寫一個以假亂真甚至更酷的!

這個程序在你移動鼠標的時候產生了海量的信息,讓咱們知道了Pygame是多麼的繁忙……咱們第一個程序那樣是調用pygame.mouse.get_pos()來獲得當前鼠標的位置,而如今利用事件能夠直接得到!

處理鼠標事件

MOUSEMOTION事件會在鼠標動做的時候發生,它有三個參數:

  • buttons – 一個含有三個數字的元組,三個值分別表明左鍵、中鍵和右鍵,1就是按下了。
  • pos – 就是位置了……
  • rel – 表明瞭如今距離上次產生鼠標事件時的距離

和MOUSEMOTION相似的,咱們還有MOUSEBUTTONDOWNMOUSEBUTTONUP兩個事件,看名字就明白是什麼意思了。不少時候,你只須要知道鼠標點下就能夠了,那就能夠不用上面那個比較強大(也比較複雜)的事件了。它們的參數爲:

  • button – 看清楚少了個s,這個值表明了哪一個按鍵被操做
  • pos – 和上面同樣

處理鍵盤事件

鍵盤和遊戲手柄的事件比較相似,爲KEYDOWNKEYUP,下面有一個例子來演示使用方向鍵移動一些東西。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()

 

x, y = 0, 0

move_x, move_y = 0, 0

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

           exit()

        if event.type == KEYDOWN:

            #鍵盤有按下?

            if event.key == K_LEFT:

                #按下的是左方向鍵的話,把x座標減一

                move_x = -1

            elif event.key == K_RIGHT:

                #右方向鍵則加一

                move_x = 1

            elif event.key == K_UP:

                #相似了

                move_y = -1

            elif event.key == K_DOWN:

                move_y = 1

        elif event.type == KEYUP:

            #若是用戶放開了鍵盤,圖就不要動了

            move_x = 0

            move_y = 0

 

        #計算出新的座標

        x+= move_x

        y+= move_y

 

        screen.fill((0,0,0))

        screen.blit(background, (x,y))

        #在新的位置上畫圖

        pygame.display.update()

當咱們運行這個程序的時候,按下方向鍵就能夠把背景圖移動,可是等等!爲何我只能按一下動一下啊……太很差試了吧?!用腳掌考慮下就應該按着就一直動下去纔是啊!?Pygame這麼垃圾麼……

哦,真是抱歉上面的代碼有點小bug,可是真的很小,你都不須要更改代碼自己,只要改一下縮進就能夠了,你能夠發現麼?Python自己是縮進編排來表現層次,有些時候可能會出現一點小麻煩,要咱們本身注意才能夠。

KEYDOWN和KEYUP的參數描述以下:

  • key – 按下或者放開的鍵值,是一個數字,估計地球上不多有人能夠記住,因此Pygame中你能夠使用K_xxx來表示,好比字母a就是K_a,還有K_SPACEK_RETURN等。
  • mod – 包含了組合鍵信息,若是mod & KMOD_CTRL是真的話,表示用戶同時按下了Ctrl鍵。相似的還有KMOD_SHIFTKMOD_ALT
  • unicode – 表明了按下鍵的Unicode值,這個有點很差理解,真正說清楚又太麻煩,遊戲中也不太經常使用,說明暫時省略,何時須要再講吧。

事件過濾

並非全部的事件都須要處理的,就好像不是全部登門造訪的人都是咱們歡迎的同樣。好比,俄羅斯方塊就無視你的鼠標,而在遊戲場景切換的時候,你按什麼都是徒勞的。咱們應該有一個方法來過濾掉一些咱們不感興趣的事件(固然咱們能夠不處理這些沒興趣的事件,但最好的方法仍是讓它們根本不進入咱們的事件隊列,就好像在門上貼着「XXX免進」同樣),咱們使用pygame.event.set_blocked(事件名)來完成。若是有好多事件須要過濾,能夠傳遞一個列表,好比pygame.event.set_blocked([KEYDOWN, KEYUP]),若是你設置參數None,那麼全部的事件有被打開了。與之相對的,咱們使用pygame.event.set_allowed()來設定容許的事件。

產生事件

一般玩家作什麼,Pygame就產生對應的事件就能夠了,不過有的時候咱們須要模擬出一些事件來,好比錄像回放的時候,咱們就要把用戶的操做再現一遍。

爲了產生事件,必須先造一個出來,而後再傳遞它:

Python

 

1

2

3

4

my_event = pygame.event.Event(KEYDOWN, key=K_SPACE, mod=0, unicode=u' ')

#你也能夠像下面這樣寫,看起來比較清晰(但字變多了……)

my_event = pygame.event.Event(KEYDOWN, {"key":K_SPACE, "mod":0, "unicode":u' '})

pygame.event.post(my_event)

你甚至能夠產生一個徹底自定義的全新事件,有些高級的話題,暫時不詳細說,僅用代碼演示一下:

Python

 

1

2

3

4

5

6

7

8

CATONKEYBOARD = USEREVENT+1

my_event = pygame.event.Event(CATONKEYBOARD, message="Bad cat!")

pgame.event.post(my_event)

 

#而後得到它

for event in pygame.event.get():

    if event.type == CATONKEYBOARD:

        print event.message

此次的內容不少,又很重要,一遍看下來雲裏霧裏或者看的時候明白看完了全忘了什麼的估計不少,慢慢學習吧~~多看看動手寫寫,其實都很簡單。

下次講解顯示的部分。

用Python和Pygame寫遊戲-從入門到精通(3)

 

全屏顯示

咱們在第一個程序裏使用了以下的語句

Python

 

1

screen = pygame.display.set_mode((640, 480), 0, 32)

也講述了各個參數的意思,當咱們把第二個參數設置爲FULLSCREEN時,就能獲得一個全屏窗口了

Python

 

1

screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)

注意:若是你的程序有什麼問題,極可能進入了全屏模式就不太容易退出來了,因此最好先用窗口模式調試好,再改成全屏模式。

在全屏模式下,顯卡可能就切換了一種模式,你能夠用以下代碼得到您的機器支持的顯示模式:

Python

 

1

2

3

>>> import pygame

>>> pygame.init()

>>> pygame.display.list_modes()

看一下一個實例:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

background = pygame.image.load(background_image_filename).convert()

 

Fullscreen = False

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

    if event.type == KEYDOWN:

        if event.key == K_f:

            Fullscreen = not Fullscreen

            if Fullscreen:

                screen = pygame.display.set_mode((640, 480), FULLSCREEN, 32)

            else:

                screen = pygame.display.set_mode((640, 480), 0, 32)

 

    screen.blit(background, (0,0))

    pygame.display.update()

運行這個程序,默認仍是窗口的,按「f 」,顯示模式會在窗口和全屏之間切換。程序也沒有什麼難度,應該都能看明白。

可變尺寸的顯示

雖然通常的程序窗口都能拖邊框來改變大小,pygame的默認顯示窗口是不行的,而事實上,不少遊戲確實也不能改變顯示窗口的大小,咱們能夠使用一個參數來改變這個默認行爲。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

background_image_filename = 'sushiplate.jpg'

 

import pygame

from pygame.locals import *

from sys import exit

 

SCREEN_SIZE = (640, 480)

 

pygame.init()

screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

 

background = pygame.image.load(background_image_filename).convert()

 

while True:

 

    event = pygame.event.wait()

    if event.type == QUIT:

        exit()

    if event.type == VIDEORESIZE:

        SCREEN_SIZE = event.size

        screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 32)

        pygame.display.set_caption("Window resized to "+str(event.size))

 

    screen_width, screen_height = SCREEN_SIZE

    # 這裏須要從新填滿窗口

    for y in range(0, screen_height, background.get_height()):

        for x in range(0, screen_width, background.get_width()):

            screen.blit(background, (x, y))

 

    pygame.display.update()

當你更改大小的時候,後端控制檯會顯示出新的尺寸,這裏咱們學習到一個新的事件VIDEORESIZE,它包含以下內容:

  • size  —  一個二維元組,值爲更改後的窗口尺寸,size[0]爲寬,size[1]爲高
  • w  —  寬
  • h  —  一目瞭然,高;之因此多出這兩個,無非是爲了方便

 

其餘、複合模式

咱們還有一些其餘的顯示模式,但未必全部的操做系統都支持(放心windows、各類比較流行的Linux發行版都是沒問題的),通常來講窗口就用0全屏就用FULLSCREEN,這兩個老是OK的。

若是你想建立一個硬件顯示(surface會存放在顯存裏,從而有着更高的速度),你必須和全屏一塊兒使用:

Python

 

1

screen = pygame.display.set_mode(SCREEN_SIZE, HWSURFACE | FULLSCREEN, 32)

固然你徹底能夠把雙緩衝(更快)DOUBLEBUF也加上,這就是一個很棒的遊戲顯示了,不過記得你要使用pygame.display.flip()來刷新顯示。pygame.display.update()是將數據畫到前面顯示,而這個是交替顯示的意思。

稍微說一下雙緩衝的意思,能夠作一個比喻:個人任務就是出黑板報,若是隻有一塊黑板,那我得不停的寫,所有寫完了稍微Show一下就要擦掉重寫,這樣一來別人看的基本都是我在寫黑板報的過程,看到的都是不完整的黑板報;若是我有兩塊黑板,那麼能夠掛一塊給別人看,我本身在底下寫另外一塊,寫好了把原來的換下來換上新的,這樣一來別人基本老是看到完整的內容了。雙緩衝就是這樣維護兩個顯示區域,快速的往屏幕上換內容,而不是每次都慢慢地重畫。

 

用Python和Pygame寫遊戲-從入門到精通(4)

 

使用字體模塊

就像上一次說的,一個遊戲,再怎麼寒磣也得有文字,俄羅斯方塊還有個記分數的呢;印象中沒有文字的電子遊戲只有電腦剛剛誕生的那種打乒乓的了。Pygame能夠直接調用系統字體,或者也能夠使用TTF字體,稍有點電腦知識的都知道這是什麼。爲了使用字體,你得先建立一個Font對象,對於系統自帶的字體:

Python

 

1

my_font = pygame.font.SysFont("arial", 16)

第一個參數是字體名,第二個天然就是大小,通常來講「Arial」字體在不少系統都是存在的,若是找不到的話,就會使用一個默認的字體,這個默認的字體和每一個操做系統相關,你也能夠使用pygame.font.get_fonts()來得到當前系統全部可用字體。還有一個更好的方法的,使用TTF的方法:

Python

 

1

my_font = pygame.font.Font("my_font.ttf", 16)

這個語句使用了一個叫作「my_font.ttf」,這個方法之因此好是由於你能夠把字體文件隨遊戲一塊兒分發,避免用戶機器上沒有須要的字體。。一旦你建立了一個font對象,你就能夠使用render方法來寫字了,而後就能blit到屏幕上:

Python

 

1

text_surface = my_font.render("Pygame is cool!", True, (0,0,0), (255, 255, 255))

第一個參數是寫的文字;第二個參數是個布爾值,覺得這是否開啓抗鋸齒,就是說True的話字體會比較平滑,不過相應的速度有一點點影響;第三個參數是字體的顏色;第四個是背景色,若是你想沒有背景色(也就是透明),那麼能夠不加這第四個參數。

下面是一個小例子演示下文字的使用,不過並非顯示在屏幕上,而是存成一個圖片文件

Python

 

1

2

3

4

5

6

my_name = "Will McGugan"

import pygame

pygame.init()

my_font = pygame.font.SysFont("arial", 64)

name_surface = my_font.render(my_name, True, (0, 0, 0), (255, 255, 255))

pygame.image.save(name_surface, "name.png")

追加說明一下如何顯示中文,這在原書但是沒有的哦:) 簡單來講,首先你得用一個能夠使用中文的字體,宋體、黑體什麼的,或者你直接用中文TTF文件,而後文字使用unicode,即u」中文的文字」這種,最後不要忘了源文件里加上一句關於文件編碼的「魔法註釋」,具體的能夠查一下Python的編碼方面的文章。舉一個這樣的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

# -*- coding: utf-8 -*-

# 記住上面這行是必須的,並且保存文件的編碼要一致!

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

#font = pygame.font.SysFont("宋體", 40)

#上句在Linux可行,在個人Windows 7 64bit上不行,XP不知道行不行

#font = pygame.font.SysFont("simsunnsimsun", 40)

#用get_fonts()查看後看到了這個字體名,在個人機器上能夠正常顯示了

font = pygame.font.Font("simsun.ttc", 40)

#這句話老是能夠的,因此仍是TTF文件保險啊

text_surface = font.render(u"你好", True, (0, 0, 255))

 

x = 0

y = (480 - text_surface.get_height())/2

 

background = pygame.image.load("sushiplate.jpg").convert()

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0, 0))

 

    x -= 2  # 文字滾動太快的話,改改這個數字

    if x < -text_surface.get_width():

        x = 640 - text_surface.get_width()

 

    screen.blit(text_surface, (x, y))

 

    pygame.display.update()

Pygame的錯誤處理

程序總會出錯的,好比當內存用盡的時候Pygame就沒法再加載圖片,或者文件根本就不存在。再好比下例:

Python

 

1

2

3

4

5

6

7

>>> import pygame

>>> screen = pygame.display.set_mode((640, -1))

---------------------------------

Traceback (most recent call last):

  File "<interactive input>", line 1, in ?

pygame.error: Cannot set 0 sized display mode

----------------------------------

對付這種錯誤一個比較好的方法:

Python

 

1

2

3

4

5

6

try:

    screen = pygame.display.set_mode(SCREEN_SIZE)

except pygame.error, e:

    print "Can't create the display :-("

    print e

    exit()

其實就是Python的標準的錯誤捕捉方法就是了,實際的遊戲(或者程序)中,錯誤捕捉實在過重要了,若是你寫過比較大的應用,應該不用我來講明這一點,Pygame中也是同樣的。

Pygame的基礎就到這裏,後面咱們會進行一些高級的介紹,下一次的話,就開始講畫東西了~

用Python和Pygame寫遊戲-從入門到精通(5)

像素的威力

湊近顯示器,你能看到圖像是由一個一個點構成,這就是像素。至於屏幕分辨率的意義,也就不用多說了吧,一個1280×1024的顯示器,有着1310720個像素,通常的32爲RGB系統,每一個像素能夠顯示16.7百萬種顏色(能夠看個人另外一篇一張白紙能夠承載多少重的文章),咱們能夠寫一個小程序來顯示這麼多的顏色~

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import pygame

pygame.init()

 

screen = pygame.display.set_mode((640, 480))

 

all_colors = pygame.Surface((4096,4096), depth=24)

 

for r in xrange(256):

    print r+1, "out of 256"

    x = (r&15)*256

    y = (r>>4)*256

    for g in xrange(256):

        for b in xrange(256):

            all_colors.set_at((x+g, y+b), (r, g, b))

 

pygame.image.save(all_colors, "allcolors.bmp")

運行可能有些慢,你應該等生成bmp圖像文件,打開看看效果吧

色彩是一個頗有趣的話題,好比把藍色和黃色混合產生綠色,事實上你能夠用紅黃藍混合出全部的顏色(光學三原色),電腦屏幕上的三原色是紅綠藍(RGB),要想更深入的理解這個東西,你得學習一下(就看看李濤的PhotoShop講座吧,VeryCD上有下的,講的仍是很清楚的)~

稍有點經驗的圖像設計者應該看到RGB的數值就能想象出大概的顏色,咱們來用一個Python腳本增強這個認識。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

#!/usr/bin/env python

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

def create_scales(height):

    red_scale_surface = pygame.surface.Surface((640, height))

    green_scale_surface = pygame.surface.Surface((640, height))

    blue_scale_surface = pygame.surface.Surface((640, height))

    for x in range(640):

        c = int((x/640.)*255.)

        red = (c, 0, 0)

        green = (0, c, 0)

        blue = (0, 0, c)

        line_rect = Rect(x, 0, 1, height)

        pygame.draw.rect(red_scale_surface, red, line_rect)

        pygame.draw.rect(green_scale_surface, green, line_rect)

        pygame.draw.rect(blue_scale_surface, blue, line_rect)

    return red_scale_surface, green_scale_surface, blue_scale_surface

 

red_scale, green_scale, blue_scale = create_scales(80)

 

color = [127, 127, 127]

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.fill((0, 0, 0))

 

    screen.blit(red_scale, (0, 00))

    screen.blit(green_scale, (0, 80))

    screen.blit(blue_scale, (0, 160))

 

    x, y = pygame.mouse.get_pos()

 

    if pygame.mouse.get_pressed()[0]:

        for component in range(3):

            if y > component*80 and y < (component+1)*80:

                color[component] = int((x/639.)*255.)

        pygame.display.set_caption("PyGame Color Test - "+str(tuple(color)))

 

    for component in range(3):

        pos = ( int((color[component]/255.)*639), component*80+40 )

        pygame.draw.circle(screen, (255, 255, 255), pos, 20)

 

    pygame.draw.rect(screen, tuple(color), (0, 240, 640, 240))

 

    pygame.display.update()

 

顏色的縮放

「縮放顏色」並非一種合適的說法,它的準確意義就是上面所說的把顏色變亮或者變暗。通常來講,把顏色的RGB每個數值乘以一個小於1的正小數,顏色看起來就會變暗了(記住RGB都是整數因此可能須要取整一下)。咱們很容易能夠寫一個縮放顏色的函數出來,我就不贅述了。

很天然的能夠想到,若是乘以一個大於1的數,顏色就會變亮,不過一樣要記住每一個數值最多255,因此一旦超過,你得把它歸爲255!使用Python的內置函數min,你能夠方便的作到這事情,也很少說了。若是你乘的數字偏大,顏色很容易就爲變成純白色,就失去了原來的色調。並且RGB也不多是負數,因此謹慎選擇你的縮放係數!

顏色的混合

不少時候咱們還須要混合顏色,好比一個殭屍在路過一個火山熔岩坑的時候,它會由綠色變成橙紅色,再變爲正常的綠色,這個過程必須表現的很平滑,這時候咱們就須要混合顏色。

咱們用一種叫作「線性插值(linear interpolation)」的方法來作這件事情。爲了找到兩種顏色的中間色,咱們將這第二種顏色與第一種顏色的差乘以一個0~1之間的小數,而後再加上第一種顏色就好了。若是這個數爲0,結果就徹底是第一種顏色;是1,結果就只剩下第二種顏色;中間的小數則會皆有二者的特點。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#!/usr/bin/env python

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

color1 = (221, 99, 20)

color2 = (96, 130, 51)

factor = 0.

 

def blend_color(color1, color2, blend_factor):

    r1, g1, b1 = color1

    r2, g2, b2 = color2

    r = r1 + (r2 - r1) * blend_factor

    g = g1 + (g2 - g1) * blend_factor

    b = b1 + (b2 - b1) * blend_factor

    return int(r), int(g), int(b)

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.fill((255,255,255))

 

    tri = [ (0, 120), (639, 100), (639, 140) ]

    pygame.draw.polygon(screen, (0, 255, 0), tri)

    pygame.draw.circle(screen, (0, 0, 0), (int(factor * 639.0), 120), 10)

 

    x, y = pygame.mouse.get_pos()

    if pygame.mouse.get_pressed()[0]:

        factor = x / 639.0

        pygame.display.set_caption("Pygame Color Blend Test - %.3f" % factor)

 

    color = blend_color(color1, color2 , factor)

    pygame.draw.rect(screen, color, (0, 240, 640, 240))

 

    pygame.display.update()

 

用Python和Pygame寫遊戲-從入門到精通(6)

這個世界上有不少存儲圖像的方式(也就是有不少圖片格式),好比JPEG、PNG等,Pygmae都能很好的支持,具體支持的格式以下:

  • JPEG(Join Photograhpic Exper Group),極爲經常使用,通常後綴名爲.jpg或者.jpeg。數碼相機、網上的圖片基本都是這種格式。這是一種有損壓縮方式,儘管對圖片質量有些損壞,但對於減少文件尺寸很是棒。優勢不少只是不支持透明。
  • PNG(Portable Network Graphics)將會大行其道的一種格式,支持透明,無損壓縮。對於網頁設計,軟件界面設計等等都是很是棒的選擇!
  • GIF 網上使用的不少,支持透明和動畫,只是只能有256種顏色,軟件和遊戲中使用不多
  • BMP Windows上的標準圖像格式,無壓縮,質量很高但尺寸很大,通常不使用
  • PCX
  • TGA
  • TIF
  • LBM, PBM
  • XPM

使用Surface對象

對於Pygame而已,加載圖片就是pygame.image.load,給它一個文件名而後就還給你一個surface對象。儘管讀入的圖像格式各不相同,surface對象隱藏了這些不一樣。你能夠對一個Surface對象進行塗畫、變形、複製等各類操做。事實上,屏幕也只是一個surface,pygame.display.set_mode就返回了一個屏幕surface對象。

建立Surfaces對象

一種方法就是剛剛說的pygame.image.load,這個surface有着和圖像相同的尺寸和顏色;另一種方法是指定尺寸建立一個空的surface,下面的語句建立一個256×256像素的surface:

Python

 

1

bland_surface = pygame.Surface((256, 256))

若是不指定尺寸,那麼就建立一個和屏幕同樣大小的。

你還有兩個參數可選,第一個是flags:

  • HWSURFACE – 相似於前面講的,更快!不過最好不設定,Pygmae能夠本身優化。
  • SRCALPHA – 有Alpha通道的surface,若是你須要透明,就要這個選項。這個選項的使用須要第二個參數爲32~

第二個參數是depth,和pygame.display.set_mode中的同樣,你能夠不設定,Pygame會自動設的和display一致。不過若是你使用了SRCALPHA,仍是設爲32吧:

Python

 

1

bland_alpha_surface = pygame.Surface((256, 256), flags=SRCALPHA, depth=32)

 

轉換Surfaces

一般你不用在乎surface裏的具體內容,不過也許須要把這些surface轉換一下以得到更高的性能,還記得一開始的程序中的兩句話嗎:

Python

 

1

2

background = pygame.image.load(background_image_filename).convert()

mouse_cursor = pygame.image.load(mouse_image_filename).convert_alpha()

第一句是普通的轉換,相同於display;第二句是帶alpha通道的轉換。若是你給convert或者conver_alpha一個surface對象做爲參數,那麼這個會被做爲目標來轉換。

矩形對象(Rectangle Objects)

通常來講在制定一個區域的時候,矩形是必須的,好比在屏幕的一部分畫東西。在pygame中矩形對象極爲經常使用,它的指定方法能夠用一個四元素的元組,或者兩個二元素的元組,前兩個數爲左上座標,後兩位爲右下座標。

Pygame中有一個Rect類,用來存儲和處理矩形對象(包含在pygame.locals中,因此若是你寫了from pygame.locals import *就能夠直接用這個對象了),好比:

Python

 

1

2

3

4

5

my_rect1 = (100, 100, 200, 150)

my_rect2 = ((100, 100), (200, 150))

#上兩種爲基礎方法,表示的矩形也是同樣的

my_rect3 = Rect(100, 100, 200, 150)

my_rect4 = Rect((100, 100), (200, 150))

一旦有了Rect對象,咱們就能夠對其作不少操做,好比調整位置和大小,判斷一個點是否在其中等等。之後會慢慢接觸到,求知慾旺盛的能夠在http://www.pygame.org/docs/ref/rect.html中找到Rect的詳細信息。

剪裁(Clipping)

一般遊戲的時候你只須要繪製屏幕的一部分。好比魔獸上面是菜單,下面是操做面板,中間的小兵和英雄打的不可開交時候,上下的部分也是保持相對不動的。爲了實現這一點,surface就有了一種叫裁剪區域(clipping area)的東西,也是一個矩形,定義了哪部分會被繪製,也就是說一旦定義了這個區域,那麼只有這個區域內的像素會被修改,其餘的位置保持不變,默認狀況下,這個區域是全部地方。咱們能夠使用set_clip來設定,使用get_clip來得到這個區域。

下面幾句話演示瞭如何使用這個技術來繪製不一樣的區域:

Python

 

1

2

3

4

5

6

screen.set_clip(0, 400, 200, 600)

draw_map()

#在左下角畫地圖

screen.set_clip(0, 0, 800, 60)

draw_panel()

#在上方畫菜單面板

 

子表面(Subsurfaces)

Subsurface就是在一個Surface中再提取一個Surface,記住當你往Subsurface上畫東西的時候,同時也向父表面上操做。這能夠用來繪製圖形文字,儘管pygame.font能夠用來寫很不錯的字,但只是單色,遊戲可能須要更豐富的表現,這時候你能夠把每一個字母(中文的話有些吃力了)各自作成一個圖片,不過更好的方法是在一張圖片上畫滿全部的字母。把整張圖讀入,而後再用Subsurface把字母一個一個「摳」出來,就像下面這樣:

Python

 

1

2

3

4

my_font_image = Pygame.load("font.png")

letters = []

letters["a"] = my_font_image.subsurface((0,0), (80,80))

letters["b"] = my_font_image.subsurface((80,0), (80,80))

 

填充Surface

填充有時候能夠做爲一種清屏的操做,把整個surface填上一種顏色:

Python

 

1

screen.fill((0, 0, 0))

一樣能夠提供一個矩形來制定填充哪一個部分(這也能夠做爲一種畫矩形的方法)。

設置Surface的像素

咱們能對Surface作的最基本的操做就是設置一個像素的色彩了,雖然咱們基本不會這麼作,但仍是要了解。set_at方法能夠作到這一點,它的參數是座標和顏色,下面的小腳本會隨機的在屏幕上畫點:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import pygame

from pygame.locals import *

from sys import exit

from random import randint

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

while True:

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    rand_col = (randint(0, 255), randint(0, 255), randint(0, 255))

    #screen.lock()    #很快你就會知道這兩句lock和unlock的意思了

    for _ in xrange(100):

        rand_pos = (randint(0, 639), randint(0, 479))

        screen.set_at(rand_pos, rand_col)

    #screen.unlock()

 

    pygame.display.update()

得到Surface上的像素

set_at的兄弟get_at能夠幫咱們作這件事,它接受一個座標返回指定座標點上的顏色。不過記住get_at在對hardware surface操做的時候很慢,而全屏的時候老是hardware的,因此慎用這個方法!

鎖定Surface

當Pygame往surface上畫東西的時候,首先會把surface鎖住,以保證不會有其它的進程來干擾,畫完以後再解鎖。鎖和解鎖時自動發生的,因此有時候可能不那麼有效率,好比上面的例子,每次畫100個點,那麼就得鎖解鎖100次,如今咱們把兩句註釋去掉,再執行看看是否是更快了(好吧,其實我沒感受出來,由於如今的機器性能都不錯,這麼點的差別還不太感受的出來。不過請相信我~複雜的狀況下會影響效率的)?

當你手動加鎖的時候,必定不要忘記解鎖,不然pygame有可能會失去響應。雖然上面的例子可能沒問題,可是隱含的bug是咱們必定要避免的事情。

Blitting

blit的的中文翻譯給人摸不着頭腦的感受,能夠譯爲位塊傳送(bit block transfer),其意義是將一個平面的一部分或所有圖象整塊從這個平面複製到另外一個平面,下面仍是直接使用英文。

blit是對錶面作的最多的操做,咱們在前面的程序中已經屢次用到,很少說了;blit的還有一種用法,每每用在對動畫的表現上,好比下例經過對frame_no的值的改變,咱們能夠把不一樣的幀(同一副圖的不一樣位置)畫到屏幕上:

Python

 

1

screen.blit(ogre, (300, 200), (100 * frame_no, 0, 100, 100))

此次東西真是很多,打完脖子都酸了……

不少之前的程序中已經出現,看完這部分才能算是真正瞭解。圖像是遊戲相當重要的一部分,值得多花時間,下一次講解繪製圖形~

用Python和Pygame寫遊戲-從入門到精通(7)

pygame.draw中函數的第一個參數老是一個surface,而後是顏色,再後會是一系列的座標等。稍有些計算機繪圖經驗的人就會知道,計算機裏的座標,(0,0)表明左上角。而返回值是一個Rect對象,包含了繪製的領域,這樣你就能夠很方便的更新那個部分了。

函數

做用

rect

繪製矩形

polygon

繪製多邊形(三個及三個以上的邊)

circle

繪製圓

ellipse

繪製橢圓

arc

繪製圓弧

line

繪製線

lines

繪製一系列的線

aaline

繪製一根平滑的線

aalines

繪製一系列平滑的線

咱們下面一個一個詳細說明。

pygame.draw.rect

用法:pygame.draw.rect(Surface, color, Rect, width=0)

pygame.draw.rect在surface上畫一個矩形,除了surface和color,rect接受一個矩形的座標和線寬參數,若是線寬是0或省略,則填充。咱們有一個另外的方法來畫矩形——fill方法,若是你還記得的話。事實上fill可能還會快一點點,由於fill由顯卡來完成。

pygame.draw.polygon

用法:pygame.draw.polygon(Surface, color, pointlist, width=0)

polygon就是多邊形,用法相似rect,第1、第2、第四的參數都是相同的,只不過polygon會接受一系列座標的列表,表明了各個頂點。

pygame.draw.circle

用法:pygame.draw.circle(Surface, color, pos, radius, width=0)

很簡單,畫一個圓。與其餘不一樣的是,它接收一個圓心座標和半徑參數。

pygame.draw.ellipse

用法:pygame.draw.ellipse(Surface, color, Rect, width=0)

你能夠把一個ellipse想象成一個被壓扁的圓,事實上,它是能夠被一個矩形裝起來的。pygame.draw.ellipse的第三個參數就是這個橢圓的外接矩形。

pygame.draw.arc

用法:pygame.draw.arc(Surface, color, Rect, start_angle, stop_angle, width=1)

arc是橢圓的一部分,因此它的參數也就比橢圓多一點。但它是不封閉的,所以沒有fill方法。start_angle和stop_angle爲開始和結束的角度。

pygame.draw.line

用法:pygame.draw.line(Surface, color, start_pos, end_pos, width=1)

我相信全部的人都能看明白。

pygame.draw.lines

用法:pygame.draw.lines(Surface, color, closed, pointlist, width=1)

closed是一個布爾變量,指明是否須要多畫一條線來使這些線條閉合(感受就和polygone同樣了),pointlist是一個點的數組。

上面的表中咱們還有aaline和aalines,玩遊戲的都知道開出「抗鋸齒(antialiasing)」效果會讓畫面更好看一些,模型的邊就不會是鋸齒形的了,這兩個方法就是在畫線的時候作這事情的,參數和上面同樣,省略。

咱們用一個混雜的例子來演示一下上面的各個方法:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

#!/usr/bin/env python

 

import pygame

from pygame.locals import *

from sys import exit

 

from random import *

from math import pi

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

points = []

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

        if event.type == KEYDOWN:

            # 按任意鍵能夠清屏並把點回復到原始狀態

            points = []

            screen.fill((255,255,255))

        if event.type == MOUSEBUTTONDOWN:

            screen.fill((255,255,255))

            # 畫隨機矩形

            rc = (randint(0,255), randint(0,255), randint(0,255))

            rp = (randint(0,639), randint(0,479))

            rs = (639-randint(rp[0], 639), 479-randint(rp[1], 479))

            pygame.draw.rect(screen, rc, Rect(rp, rs))

            # 畫隨機圓形

            rc = (randint(0,255), randint(0,255), randint(0,255))

            rp = (randint(0,639), randint(0,479))

            rr = randint(1, 200)

            pygame.draw.circle(screen, rc, rp, rr)

            # 得到當前鼠標點擊位置

            x, y = pygame.mouse.get_pos()

            points.append((x, y))

            # 根據點擊位置畫弧線

            angle = (x/639.)*pi*2.

            pygame.draw.arc(screen, (0,0,0), (0,0,639,479), 0, angle, 3)

            # 根據點擊位置畫橢圓

            pygame.draw.ellipse(screen, (0, 255, 0), (0, 0, x, y))

            # 從左上和右下畫兩根線鏈接到點擊位置

            pygame.draw.line(screen, (0, 0, 255), (0, 0), (x, y))

            pygame.draw.line(screen, (255, 0, 0), (640, 480), (x, y))

            # 畫點擊軌跡圖

            if len(points) > 1:

                pygame.draw.lines(screen, (155, 155, 0), False, points, 2)

            # 和軌跡圖基本同樣,只不過是閉合的,由於會覆蓋,因此這裏註釋了

            #if len(points) >= 3:

            #    pygame.draw.polygon(screen, (0, 155, 155), points, 2)

            # 把每一個點畫明顯一點

            for p in points:

                pygame.draw.circle(screen, (155, 155, 155), p, 3)

 

    pygame.display.update()

運行這個程序,在上面點鼠標就會有圖形出來了;按任意鍵能夠從新開始。另外這個程序只是各個命令的堆砌,並不見得是一個好的程序代碼。

到此次爲止,文字、顏色、圖像、圖形都講好了,靜態顯示的部分都差很少了。然而多彩的遊戲中只有靜態的東西實在太讓人寒心了(GalGame大多如此),下次開始咱們學習遊戲中的動畫製做。

用Python和Pygame寫遊戲-從入門到精通(8)

 

直線運動

咱們先來看一下初中一開始就學習的直線運動,咱們讓一開始的程序中出現的那條魚本身動起來~

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename)

 

# sprite的起始x座標

x = 0.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, 100))

    x+= 10.     #若是你的機器性能太好以致於看不清,能夠把這個數字改小一些

 

    # 若是移動出屏幕了,就搬到開始位置繼續

    if x > 640.:

        x = 0.    

 

    pygame.display.update()

我想你應該須要調節一下「x += 10.」來讓這條魚遊的天然一點,不過,這個動畫的幀率是多少的?在這個情形下,動畫很簡單,因此應該會很快;而有些時候動畫元素不少,速度就會慢下來。這可不是咱們想看到的!

關於時間

有一個解決上述問題的方法,就是讓咱們的動畫基於時間運做,咱們須要知道上一個畫面到如今通過了多少時間,而後咱們才能決定是否開始繪製下一幅。pygame.time模塊給咱們提供了一個Clock的對象,使咱們能夠輕易作到這一些:

Python

 

1

2

3

clock = pygame.time.Clock()

time_passed = clock.tick()

time_passed = clock.tick(30)

第一行初始化了一個Clock對象;第二行的意識是返回一個上次調用的時間(以毫秒計);第三行很是有用,在每個循環中加上它,那麼給tick方法加上的參數就成爲了遊戲繪製的最大幀率,這樣的話,遊戲就不會用掉你全部的CPU資源了!可是這僅僅是「最大幀率」,並不能表明用戶看到的就是這個數字,有些時候機器性能不足,或者動畫太複雜,實際的幀率達不到這個值,咱們須要一種更有效的手段來控制咱們的動畫效果。

爲了使得在不一樣機器上有着一致的效果,咱們實際上是須要給定物體(咱們把這個物體叫作精靈,Sprite)恆定的速度。這樣的話,從起點到終點的時間點是同樣的,最終的效果也就相同了,所差異的,只是流暢度。看下面的圖試着理解一下~

 

咱們把上面的結論實際試用一下,假設讓咱們的小魚兒每秒遊動250像素,這樣遊動一個屏幕差很少須要2.56秒。咱們就須要知道,從上一幀開始到如今,小魚應該遊動了多少像素,這個算法很簡單,速度*時間就好了,也就是250 * time_passed_second。不過咱們剛剛獲得的time_passed是毫秒,不要忘了除以1000.0,固然咱們也能假設小魚每毫秒遊動0.25像素,這樣就能夠直接乘了,不過這樣的速度單位有些怪怪的……

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename)

 

# Clock對象

clock = pygame.time.Clock()

 

x = 0.

# 速度(像素/秒)

speed = 250.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, 100))    

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    distance_moved = time_passed_seconds * speed

    x += distance_moved

 

    # 想一下,這裏減去640和直接歸零有何不一樣?

    if x > 640.:

        x -= 640.    

 

    pygame.display.update()

好了,這樣無論你的機器是更深的藍仍是打開個記事本都要吼半天的淘汰機,人眼看起來,不一樣屏幕上的魚的速度都是一致的了。請緊緊記住這個方法,在不少狀況下,經過時間控制要比直接調節幀率好用的多。

斜線運動

下面有一個更有趣一些的程序,再也不是單純的直線運動,而是有點像屏保同樣,碰到了壁會反彈。不過也並無新的東西在裏面,原理上來講,反彈只不過是把速度取反了而已~ 能夠先試着本身寫一個,而後與這個對照一下。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

x, y = 100., 100.

speed_x, speed_y = 133., 170.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, (x, y))

 

    time_passed = clock.tick(30)

    time_passed_seconds = time_passed / 1000.0

 

    x += speed_x * time_passed_seconds

    y += speed_y * time_passed_seconds    

 

    # 到達邊界則把速度反向

    if x > 640 - sprite.get_width():

        speed_x = -speed_x

        x = 640 - sprite.get_width()

    elif x < 0:

        speed_x = -speed_x

        x = 0.

 

    if y > 480 - sprite.get_height():

        speed_y = -speed_y

        y = 480 - sprite.get_height()

    elif y < 0:

        speed_y = -speed_y

        y = 0

 

    pygame.display.update()

OK,此次的運動就說到這裏。仔細一看的話,就會明白遊戲中的所謂運動(尤爲是2D遊戲),不過是把一個物體的座標改一下而已。不過老是不停的計算和修改x和y,有些麻煩不是麼,下次咱們引入向量,看看使用數學怎樣能夠幫咱們減輕負擔。

用Python和Pygame寫遊戲-從入門到精通(9)

引入向量

咱們先考慮二維的向量,三維也差很少了,而遊戲中的運動最多隻用獲得三維,更高的留給之後的遊戲吧~

向量的表示和座標很像,(10,20)對座標而言,就是一個固定的點,然而在向量中,它意味着x方向行進10,y方向行進20,因此座標(0,0)加上向量(10,20)後,就到達了點(10,20)。

向量能夠經過兩個點來計算出來,以下圖,A通過向量AB到達了B,則向量AB就是(30, 35) – (10, 20) = (20, 15)。咱們也能猜到向量BA會是(-20, -15),注意向量AB和向量BA,雖然長度同樣,可是方向不一樣。

 

在Python中,咱們能夠建立一個類來存儲和得到向量(雖然向量的寫法很像一個元組,但由於向量有不少種計算,必須使用類來完成):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class Vector2(object):

    def __init__(self, x=0.0, y=0.0):

        self.x = x

        self.y = y

    def __str__(self):

        return "(%s, %s)"%(self.x, self.y)

 

    @classmethod

    def from_points(cls, P1, P2):

        return cls( P2[0] – P1[0], P2[1] – P1[1] )

#咱們能夠使用下面的方法來計算兩個點之間的向量

A = (10.0, 20.0)

B = (30.0, 35.0)

AB = Vector2.from_points(A, B)

print AB

原理上很簡單,函數修飾符@不用我說明了吧?若是不明白的話,能夠參考Python的編程指南。

向量的大小

向量的大小能夠簡單的理解爲那根箭頭的長度,勾股定理熟稔的各位馬上知道怎麼計算了:

Python

 

1

2

    def get_magnitude(self):

        return math.sqrt( self.x**2 + self.y**2 )

把這幾句加入到剛剛的Vector2裏,咱們的向量類就多了計算長度的能力。嗯,別忘了一開始要引入math庫。

單位向量

一開頭說過,向量有着大小和方向兩個要素,經過剛剛的例子,咱們能夠理解這兩個意思了。在向量的你們族裏,有一種比較特殊的向量叫「單位向量」,意思是大小爲1的向量,咱們還能把任意向量方向不變的縮放(體如今數字上就是x和y等比例的縮放)到一個單位向量,這叫向量的規格(正規)化,代碼體現的話:

Python

 

1

2

3

4

    def normalize(self):

        magnitude = self.get_magnitude()

        self.x /= magnitude

        self.y /= magnitude

使用過normalize方法之後,向量就成了一個單位向量。單位向量有什麼用?咱們之後會看到。

向量運算

咱們觀察下圖,點B由A出發,經過向量AB到達,C則有B到達,經過BC到達;C直接由A出發的話,就得經由向量AC。

 

由此咱們獲得一個顯而易見的結論向量AC = 向量AB + 向量BC。向量的加法計算方法呼之欲出:

(20, 15) + (-15, 10) = (20-15, 15+10) = (5, 25)

把各個方向分別相加,咱們就獲得了向量的加法運算法則。很相似的,減法也是一樣,把各個方向分別想減,能夠本身簡單驗證一下。代碼表示的話:

Python

 

1

2

3

4

    def __add__(self, rhs):

        return Vector2(self.x + rhs.x, self.y + rhs.y)

    def __sub__(self, rhs):

        return Vector2(self.x - rhs.x, self.y - rhs.y)

兩個下劃線「__」爲首尾的函數,在Python中通常就是重載的意思,若是不知道的話還須要稍微努力努力:)固然,功力稍深厚一點的,就會知道這裏super來代替Vector2可能會更好一些,確實如此。不過這裏只是示例代碼,講述一下原理而已。

有加減法,那乘除法呢?固然有!不過向量的乘除並非發生在兩個向量直接,而是用一個向量來乘/除一個數,其實際意義就是,向量的方向不變,而大小放大/縮小多少倍。以下圖:

Python

 

1

2

3

4

    def __mul__(self, scalar):

        return Vector2(self.x * scalar, self.y * scalar)

    def __div__(self, scalar):

        return Vector2(self.x / scalar, self.y / scalar)

向量的運算被普遍的用來計算到達某個位置時的中間狀態,好比咱們知道一輛坦克從A到B,中間有10幀,那麼很顯然的,把步進向量經過(B-A)/10計算出來,每次在當前位置加上就能夠了。很簡單吧?

更好的向量類

咱們創造的向量類已經不錯了,不過畢竟只能作一些簡單的運算,別人幫咱們已經寫好了更帥的庫(早點不拿出來?寫了半天…… 原理始終是咱們掌握的,本身動手,印象更深),是發揮拿來主義的時候了(能夠嘗試使用easy_install gameobjects簡單的安裝起來)。若是您沒法打開這個地址,文章最後能夠下載。下面是一個使用的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

from gameobjects.vector2 import *

A = (10.0, 20.0)

B = (30.0, 35.0)

AB = Vector2.from_points(A, B)

print "Vector AB is", AB

print "AB * 2 is", AB * 2

print "AB / 2 is", AB / 2

print "AB + (–10, 5) is", AB + (–10, 5)

print "Magnitude of AB is", AB.get_magnitude()

print "AB normalized is", AB.get_normalized()

 

# 結果是下面

Vector AB is ( 20, 15 )

AB * 2 is ( 40, 30 )

AB / 2 is ( 10, 7.5 )

AB + (-10, 5) is ( 10, 20 )

Magnitude of AB is 25.0

AB normalized is ( 0.8, 0.6 )

 

使用向量的遊戲動畫

終於能夠實幹一番了!這個例子比咱們之前寫的都要帥的多,小魚不停的在咱們的鼠標周圍遊動,若即若離:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

position = Vector2(100.0, 100.0)

heading = Vector2()

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, position)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    # 參數前面加*意味着把列表或元組展開

    destination = Vector2( *pygame.mouse.get_pos() ) - Vector2( *sprite.get_size() )/2

    # 計算魚兒當前位置到鼠標位置的向量

    vector_to_mouse = Vector2.from_points(position, destination)

    # 向量規格化

    vector_to_mouse.normalize()

 

    # 這個heading能夠看作是魚的速度,可是因爲這樣的運算,魚的速度就不斷改變了

    # 在沒有到達鼠標時,加速運動,超過之後則減速。於是魚會在鼠標附近晃動。

    heading = heading + (vector_to_mouse * .6)    

 

    position += heading * time_passed_seconds

    pygame.display.update()

雖然這個例子裏的計算有些讓人看不明白,可是很明顯heading的計算是關鍵,如此複雜的運動,使用向量竟然兩句話就搞定了~看來沒有白學。

動畫總結

  • 正如上一章所說,所謂動畫,不過是在每一幀上,相對前一幀把精靈的座標在加減一些而已;
  • 使用時間來計算加減的量以在不一樣性能的計算機上得到一致的動畫效果;
  • 使用向量來計算運動的過程來減輕咱們的勞動,在3D的狀況下,簡單的使用Vector3即可以了。

現在咱們已經學習到了遊戲動畫製做的精髓,一旦能夠動起來,就能創造無數讓人歎爲觀止的效果,是否是應該寫個程序在朋友們面前炫耀炫耀了?
在下面,咱們要學習接受輸入和遊戲裏的物體互動起來。

gameobjects-0.0.3.win32.exe可運行的安裝文件
gameobjects-0.0.3源碼

 

用Python和Pygame寫遊戲-從入門到精通(10)

 

遊戲設備

玩過遊戲的都知道鼠標和鍵盤是遊戲的不可或缺的輸入設備。鍵盤能夠控制有限的方向和諸多的命令操做,而鼠標更是提供了全方位的方向和位置操做。不過這兩個設備並非爲遊戲而生,專業的遊戲手柄給玩家提供了更好的操做感,加上力反饋等技術,應該說遊戲設備愈來愈豐富,玩家們也是愈來愈幸福。

鍵盤設備

咱們先從最普遍的鍵盤開始講起。

如今使用的鍵盤,基本都是QWERTY鍵盤(看看字幕鍵盤排布的左上就知道了),儘管這個世界上還有其餘種類的鍵盤,好比AZERTY啥的,反正我是沒見過,若是你能在寫遊戲的時候考慮到這些特殊用戶天然是最好,我的感受是問題不大吧。

之前第二部分也稍微使用了一下鍵盤,那時候是用了pygame.event.get()獲取全部的事件,當event.type == KEYDOWN的時候,在判斷event.key的種類,而各個種類也使用K_aK_b……等判斷。這裏再介紹一個pygame.key.get_pressed()來得到全部按下的鍵值,它會返回一個元組。這個元組的索引就是鍵值,對應的就是是否按下,好比說:

Python

 

1

2

3

4

    pressed_keys = pygame.key.get_pressed()

    if pressed_keys[K_SPACE]:

        # Space key has been pressed

        fire()pressed_keys = pygame.key.get_pressed()

固然key模塊下還有不少函數:

  • key.get_focused —— 返回當前的pygame窗口是否激活
  • key.get_pressed —— 剛剛解釋過了
  • key.get_mods —— 按下的組合鍵(Alt, Ctrl, Shift)
  • key.set_mods —— 你也能夠模擬按下組合鍵的效果(KMOD_ALT, KMOD_CTRL, KMOD_SHIFT)
  • key.set_repeat —— 無參數調用設置pygame不產生重複按鍵事件,二參數(delay, interval)調用設置重複事件發生的時間
  • key.name —— 接受鍵值返回鍵名

注:感謝xumaomao朋友的傾情指正!

使用鍵盤控制方向

有了上一章向量的基礎,只需一幅圖就能明白鍵盤如何控制方向:

 

不少遊戲也使用ASDW當作方向鍵來移動,咱們來看一個實際的例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

sprite_pos = Vector2(200, 150)

sprite_speed = 300.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    pressed_keys = pygame.key.get_pressed()

 

    key_direction = Vector2(0, 0)

    if pressed_keys[K_LEFT]:

        key_direction.x = -1

    elif pressed_keys[K_RIGHT]:

        key_direction.x = +1

    if pressed_keys[K_UP]:

        key_direction.y = -1

    elif pressed_keys[K_DOWN]:

        key_direction.y = +1

 

    key_direction.normalize()

 

    screen.blit(background, (0,0))

    screen.blit(sprite, sprite_pos)

 

    time_passed = clock.tick(30)

    time_passed_seconds = time_passed / 1000.0

 

    sprite_pos+= key_direction * sprite_speed * time_passed_seconds

 

    pygame.display.update()

這個例子很簡單,就是使用方向鍵移動小魚。使用的知識也都講過了,相信你們均可以理解。不過這裏並非單純的判斷按下的鍵來得到方向,而是經過對方向的加減來得到最終的效果,這樣可能會更簡短一些,也須要一些技術;若是把方向寫入代碼,效率更高,不過明顯通用性就要低一些。記得把力氣花在刀刃上!固然這個例子也不是那麼完美,看代碼、實踐一下都能看到,左方向鍵的優先級大於右方向鍵,而上則優於下,咱們是否有更好的方法?……有興趣的本身考慮~

這個例子咱們能夠看到,小魚只能在八個方向移動,如何作到全方向?若是你遊戲經驗足一點或許能夠想到,是的,先轉向,再移動,儘管不是那麼快捷,但畢竟達到了目標。咱們看一下這樣的代碼怎麼寫:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

from math import *

 

pygame.init()

 

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

sprite_pos = Vector2(200, 150)   # 初始位置

sprite_speed = 300.     # 每秒前進的像素數(速度)

sprite_rotation = 0.      # 初始角度

sprite_rotation_speed = 360. # 每秒轉動的角度數(轉速)

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

 

    pressed_keys = pygame.key.get_pressed()

 

    rotation_direction = 0.

    movement_direction = 0.

 

    # 更改角度

    if pressed_keys[K_LEFT]:

        rotation_direction = +1.

    if pressed_keys[K_RIGHT]:

        rotation_direction = -1.

    # 前進、後退

    if pressed_keys[K_UP]:

        movement_direction = +1.

    if pressed_keys[K_DOWN]:

        movement_direction = -1.

 

    screen.blit(background, (0,0))

 

    # 得到一條轉向後的魚

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)

    # 轉向後,圖片的長寬會變化,由於圖片永遠是矩形,爲了放得下一個轉向後的矩形,外接的矩形勢必會比較大

    w, h = rotated_sprite.get_size()

    # 得到繪製圖片的左上角(感謝pltc325網友的指正)

    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)

    screen.blit(rotated_sprite, sprite_draw_pos)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    # 圖片的轉向速度也須要和行進速度同樣,經過時間來控制

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

 

    # 得到前進(x方向和y方向),這兩個須要一點點三角的知識

    heading_x = sin(sprite_rotation*pi/180.)

    heading_y = cos(sprite_rotation*pi/180.)

    # 轉換爲單位速度向量

    heading = Vector2(heading_x, heading_y)

    # 轉換爲速度

    heading *= movement_direction

 

    sprite_pos+= heading * sprite_speed * time_passed_seconds

 

    pygame.display.update()

咱們經過上下控制前進/後退,而左右控制轉向。咱們經過pygame.transform.rotate()來得到了轉向後的圖片,具體參數能夠參考代碼。各條語句的做用也能夠參考註釋。

下次講解使用鼠標控制遊戲。

用Python和Pygame寫遊戲-從入門到精通(11)

咱們已經看到如何畫一個光標了,只是簡單的在鼠標座標上畫一個圖像而已,咱們能夠從MOUSEMOTION或者pygame.mouse.get_pos方法來得到座標。但咱們還能夠使用這個座標來控制方向,好比在3D遊戲中,能夠使用鼠標來控制視角。這種時候,咱們不使用鼠標的位置,由於鼠標可能會跑到窗口外面,咱們使用鼠標如今與上一幀的相對偏移量。在下一個例子中,咱們演示使用鼠標的左右移動來轉動咱們熟悉的小魚兒:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

background_image_filename = 'sushiplate.jpg'

sprite_image_filename = 'fugu.png'

 

import pygame

from pygame.locals import *

from sys import exit

from gameobjects.vector2 import Vector2

from math import *

 

pygame.init()

screen = pygame.display.set_mode((640, 480), 0, 32)

 

background = pygame.image.load(background_image_filename).convert()

sprite = pygame.image.load(sprite_image_filename).convert_alpha()

 

clock = pygame.time.Clock()

 

# 讓pygame徹底控制鼠標

pygame.mouse.set_visible(False)

pygame.event.set_grab(True)

 

sprite_pos = Vector2(200, 150)

sprite_speed = 300.

sprite_rotation = 0.

sprite_rotation_speed = 360.

 

while True:

 

    for event in pygame.event.get():

        if event.type == QUIT:

            exit()

        # 按Esc則退出遊戲

        if event.type == KEYDOWN:

            if event.key == K_ESCAPE:

                exit()

 

    pressed_keys = pygame.key.get_pressed()

    # 這裏獲取鼠標的按鍵狀況

    pressed_mouse = pygame.mouse.get_pressed()

 

    rotation_direction = 0.

    movement_direction = 0.

 

    # 經過移動偏移量計算轉動

    rotation_direction = pygame.mouse.get_rel()[0]/5.0

 

    if pressed_keys[K_LEFT]:

        rotation_direction = +1.

    if pressed_keys[K_RIGHT]:

        rotation_direction = -1.

    # 多了一個鼠標左鍵按下的判斷

    if pressed_keys[K_UP] or pressed_mouse[0]:

        movement_direction = +1.

    # 多了一個鼠標右鍵按下的判斷

    if pressed_keys[K_DOWN] or pressed_mouse[2]:

        movement_direction = -1.

 

    screen.blit(background, (0,0))

 

    rotated_sprite = pygame.transform.rotate(sprite, sprite_rotation)

    w, h = rotated_sprite.get_size()

    sprite_draw_pos = Vector2(sprite_pos.x-w/2, sprite_pos.y-h/2)

    screen.blit(rotated_sprite, sprite_draw_pos)

 

    time_passed = clock.tick()

    time_passed_seconds = time_passed / 1000.0

 

    sprite_rotation += rotation_direction * sprite_rotation_speed * time_passed_seconds

 

    heading_x = sin(sprite_rotation*pi/180.)

    heading_y = cos(sprite_rotation*pi/180.)

    heading = Vector2(heading_x, heading_y)

    heading *= movement_direction

 

    sprite_pos+= heading * sprite_speed * time_passed_seconds

 

    pygame.display.update()

一旦打開這個例子,鼠標就看不到了,咱們得使用Esc鍵來退出程序,除了上一次的方向鍵,當鼠標左右移動的時候,小魚轉動,按下鼠標左右鍵的時候,小魚前進/後退。看代碼,基本也是同樣的,就多了幾句帶註釋的。

這裏使用了

 

1

2

pygame.mouse.set_visible(False)

pygame.event.set_grab(True)

來徹底控制鼠標,這樣鼠標的光標看不見,也不會跑到pygame窗口外面去,一個反作用就是沒法使用鼠標關閉窗口了,因此你得準備一句代碼來退出程序。

而後咱們使用

 

1

rotation_direction = pygame.mouse.get_rel()[0] / 5.

來得到x方向上的偏移量,除以5是把動做放慢一點……

還有

 

1

lmb, mmb, rmb = pygame.mouse.get_pressed()

得到了鼠標按鍵的狀況,若是有一個按鍵按下,那麼對應的值就會爲True。

總結一下pygame.mouse的函數:

  • pygame.mouse.get_pressed —— 返回按鍵按下狀況,返回的是一元組,分別爲(左鍵, 中鍵, 右鍵),如按下則爲True
  • pygame.mouse.get_rel —— 返回相對偏移量,(x方向, y方向)的一元組
  • pygame.mouse.get_pos —— 返回當前鼠標位置(x, y)
  • pygame.mouse.set_pos —— 顯而易見,設置鼠標位置
  • pygame.mouse.set_visible —— 設置鼠標光標是否可見
  • pygame.mouse.get_focused —— 若是鼠標在pygame窗口內有效,返回True
  • pygame.mouse.set_cursor —— 設置鼠標的默認光標式樣,是否是感受咱們之前作的事情白費了?哦不會,咱們使用的方法有着更好的效果。
  • pyGame.mouse.get_cursor —— 再也不解釋。

關於使用鼠標

在遊戲中活用鼠標是一門學問,像在FPS中,鼠標用來瞄準,ARPG或RTS中,鼠標用來指定位置和目標。而在不少策略型的小遊戲中,鼠標的威力更是被髮揮的 淋漓盡致,也許是能夠放置一些道具,也許是用來操控蓄力。咱們如今使用的屏幕是二維的,而鼠標也能在2維方向到達任何的位置,因此鼠標相對鍵盤,更適合現代的複雜操做,只有想不到沒有作不到啊。

絕大多數時候,鼠標和鍵盤是合做使用的,好比使用鍵盤轉換視角,使用鍵盤移動,或者鍵盤對應不少快捷鍵,而鍵盤則用來指定位置。開動大腦,創造將來!

 

用Python和Pygame寫遊戲-從入門到精通(13)

 

咱們要學習遊戲的另一個支撐物,智能,或者帥氣一點稱爲AI(Artificial Intelligence,人工智能,由於遊戲裏的智能確定是人賦予的)。玩家操做咱們本身的角色,那麼NPC(nonplayer characters)呢?交由AI去操做,因此若是遊戲中有何你相同地位的角色存在的話,你就是在和AI對壘。智能意味着對抗,「與人鬥其樂無窮」,就是由於人足夠聰明,要想「玩遊戲其樂無窮」,咱們都得賦予遊戲足夠的AI。

爲遊戲建立人工智能

也許你但願能在Pygame中發現一個pygame.ai模塊,不過每一個遊戲中的智能都是不一樣的,很難準備一個通用的模塊。一個簡單的遊戲中並不須要多少AI編程的代碼,好比俄羅斯方塊,你只須要隨機的落下一個方塊組合,而後每次降低完畢掃描一下落下的方塊就行了,這甚至不能稱爲AI。但好比魔獸爭霸,這裏面的AI就很是的複雜,通常人都要學習一段時間才能戰勝電腦,可想而知其高度了。

儘管通常遊戲中的人工智能都是用來對付人類的,不過隨着遊戲發展,AI也多是咱們朋友,甚至AI互相影響從而改變整個遊戲世界,這樣的遊戲就有了更多的深度和未知,沒法預知的東西老是吸引人的不是麼?

遊戲的AI編程不是一件簡單的事情,幸運的是AI代碼每每能夠重用,這個咱們之後再講。

咱們接下來要講述遊戲AI的技術,賦予遊戲角色以生命,應該說人工智能是很高端的技術,花費幾十年都未必能到達怎麼的一個高度,因此這裏的幾章仍是以講解重要概念爲主。做爲參考,我的推薦Mat Buckland的《AI Techniques for Game Programming》,中文版《遊戲編程中的人工智能技術》由清華大學出版社出版,很不錯的一本入門書籍。

什麼是人工智能

出於嚴謹性,咱們應該給人工智能下一個定義。每一個人都會對智能有一個概念,但又很難給它下一個確切的定義。著名的美國斯坦福大學人工智能研究中心尼爾遜教授對人工智能下了這樣一個定義:「人工智能是關於知識的學科――怎樣表示知識以及怎樣得到知識並使用知識的科學。」而另外一個美國麻省理工學院的溫斯頓教授認爲:「人工智能就是研究如何使計算機去作過去只有人才能作的智能工做。」這些說法反映了人工智能學科的基本思想和基本內容。即人工智能是研究人類智能活動的規律,構造具備必定智能的人工系統,研究如何讓計算機去完成以往須要人的智力才能勝任的工做,也就是研究如何應用計算機的軟硬件來模擬人類某些智能行爲的基本理論、方法和技術。但這些說辭太麻煩了,我以爲,人工智能就是自我感知和反應的人造系統,足矣。

智能是一件玄妙的事情,在遊戲中的人工智能更是如此,咱們用程序中的一些數據結構和算法就構築了NPC的大腦,聽起來太酷了!更酷的是,Python很是適合用來編寫人工智能。

人工智能初探

舉超級瑪麗爲一個例子,那些走來走去的老烏龜,咱們控制英雄踩到它們頭上就能殺死它們,而平時,它們就在兩根管子之間走來走去(這樣的人生真可悲……),若是咱們打開它們的腦殼看一下,可能會看到這樣的代碼:

Python

 

1

2

3

self.move_forward()

if self.hit_wall():

    self.change_direction()

無比簡單,向前走,一撞牆就回頭,而後重複。它只能理解一種狀態,就是撞牆,而一旦到達這個狀態,它的反應就是回頭。

在考慮一個會發子彈的小妖怪,它的腦殼多是這麼長的:

Python

 

1

2

3

4

5

6

7

8

9

10

if self.state == "exploring":

    self.random_heading()

    if self.can_see(player):

        self.state = "seeking"

elif self.state == "seeking":

    self.head_towards("player")

    if self.in_range_of(player):

        self.fire_at(player)

    if not self.can_see(player):

        self.state = "exploring"

它就有了兩個狀態,搜尋鎖定。若是正在搜尋,就隨處走動,若是發現目標,就鎖定他,而後靠近並試着向其開火,而一旦丟失目標(目標走出了視線範圍或者被消滅),從新進入搜尋狀態。這個AI也是很簡單的,可是它對玩家來講就有了必定的危險性。若是咱們給它加入更多的狀態,它就會變得更厲害,遊戲的趣味性也就可能直線上揚了。

OK,這就是咱們下一次要講的主題,狀態機

用Python和Pygame寫遊戲-從入門到精通(14)

狀態定義了兩個內容:

  • 當前正在作什麼
  • 轉化到下一件事時候的條件

狀態同時還可能包含進入(entry退出(exit)兩種動做,進入時間是指進入某個狀態時要作的一次性的事情,好比上面的怪,一旦進入攻擊狀態,就得開始計算與玩家的距離,或許還得大吼一聲「我要殺了你」等等;而退出動做則是與之相反的,離開這個狀態要作的事情。

咱們來建立一個更爲複雜的場景來闡述這個概念——一個蟻巢世界。咱們經常使用昆蟲來研究AI,由於昆蟲的行爲很簡單容易建模。在咱們此次的環境裏,有三個實體(entity)登場:葉子、蜘蛛、螞蟻。葉子會隨機的出如今屏幕的任意地方,並由螞蟻回收至蟻穴,而蜘蛛在屏幕上隨便爬,平時螞蟻不會在乎它,而一旦進入蟻穴,就會遭到螞蟻的極力驅趕,直至蜘蛛掛了或遠離蟻穴。

儘管咱們是對昆蟲建模的,這段代碼對不少場景都是合適的。把它們替換爲巨大的機器人守衛(蜘蛛)、坦克(螞蟻)、能源(葉子),這段代碼依然可以很好的工做。

遊戲實體類

這裏出現了三個實體,咱們試着寫一個通用的實體基類,省得寫三遍了,同時若是加入了其餘實體,也能很方便的擴展出來。

一個實體須要存儲它的名字,如今的位置,目標,速度,以及一個圖形。有些實體可能只有一部分屬性(好比葉子不該該在地圖上瞎走,咱們把它的速度設爲0),同時咱們還須要準備進入和退出的函數供調用。下面是一個完整的GameEntity類:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class GameEntity(object):

    def __init__(self, world, name, image):

        self.world = world

        self.name = name

        self.image = image

        self.location = Vector2(0, 0)

        self.destination = Vector2(0, 0)

        self.speed = 0.

        self.brain = StateMachine()

        self.id = 0

    def render(self, surface):

        x, y = self.location

        w, h = self.image.get_size()

        surface.blit(self.image, (x-w/2, y-h/2))

    def process(self, time_passed):

        self.brain.think()

        if self.speed > 0 and self.location != self.destination:

            vec_to_destination = self.destination - self.location

            distance_to_destination = vec_to_destination.get_length()

            heading = vec_to_destination.get_normalized()

            travel_distance = min(distance_to_destination, time_passed * self.speed)

            self.location += travel_distance * heading

觀察這個類,會發現它還保存一個world,這是對外界描述的一個類的引用,不然實體沒法知道外界的信息。這裏類還有一個id,用來標示本身,甚至還有一個brain,就是咱們後面會定義的一個狀態機類。

render函數是用來繪製本身的。

process函數首先調用self.brain.think這個狀態機的方法來作一些事情(好比轉身等)。接下來的代碼用來讓實體走近目標。

世界類

咱們寫了一個GameObject的實體類,這裏再有一個世界類World用來描述外界。這裏的世界不須要多複雜,僅僅須要準備一個蟻穴,和存儲若干的實體位置就足夠了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

class World(object):

    def __init__(self):

        self.entities = {} # Store all the entities

        self.entity_id = 0 # Last entity id assigned

        # 畫一個圈做爲蟻穴

        self.background = pygame.surface.Surface(SCREEN_SIZE).convert()

        self.background.fill((255, 255, 255))

        pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION, int(NEST_SIZE))

    def add_entity(self, entity):

        # 增長一個新的實體

        self.entities[self.entity_id] = entity

        entity.id = self.entity_id

        self.entity_id += 1

    def remove_entity(self, entity):

        del self.entities[entity.id]

    def get(self, entity_id):

        # 經過id給出實體,沒有的話返回None

        if entity_id in self.entities:

            return self.entities[entity_id]

        else:

            return None

    def process(self, time_passed):

        # 處理世界中的每個實體

        time_passed_seconds = time_passed / 1000.0

        for entity in self.entities.itervalues():

            entity.process(time_passed_seconds)

    def render(self, surface):

        # 繪製背景和每個實體

        surface.blit(self.background, (0, 0))

        for entity in self.entities.values():

            entity.render(surface)

    def get_close_entity(self, name, location, range=100.):

        # 經過一個範圍尋找以內的全部實體

        location = Vector2(*location)

        for entity in self.entities.values():

            if entity.name == name:

                distance = location.get_distance_to(entity.location)

                if distance < range:

                    return entity

        return None

由於咱們有着一系列的GameObject,使用一個列表來存儲就是很天然的事情。不過若是實體增長,搜索列表就會變得緩慢,因此咱們使用了字典來存儲。咱們就使用GameObject的id做爲字典的key,實例做爲內容來存放,實際的樣子會是這樣:

 

大多數的方法都用來管理實體,好比add_entity和remove_entity。process方法是用來調用全部試題的process,讓它們更新本身的狀態;而render則用來繪製這個世界;最後get_close_entity用來尋找某個範圍內的實體,這個方法會在實際模擬中用到。

這兩個類還不足以構築咱們的昆蟲世界,可是倒是整個模擬的基礎,下一次咱們就要講述實際的螞蟻類和大腦(狀態機類)。

用Python和Pygame寫遊戲-從入門到精通(15)

螞蟻實例類

在咱們正式建造大腦以前,咱們得先作一個螞蟻類出來,就是下面的這個,從GameEntity繼承而來:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

class Ant(GameEntity):

    def __init__(self, world, image):

        # 執行基類構造方法

        GameEntity.__init__(self, world, "ant", image)

        # 建立各類狀態

        exploring_state = AntStateExploring(self)

        seeking_state = AntStateSeeking(self)

        delivering_state = AntStateDelivering(self)

        hunting_state = AntStateHunting(self)

        self.brain.add_state(exploring_state)

        self.brain.add_state(seeking_state)

        self.brain.add_state(delivering_state)

        self.brain.add_state(hunting_state)

        self.carry_image = None

    def carry(self, image):

        self.carry_image = image

    def drop(self, surface):

        # 放下carry圖像

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

            self.carry_image = None

    def render(self, surface):

        # 先調用基類的render方法

        GameEntity.render(self, surface)

        # 額外繪製carry_image

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

這個Ant類先調用了父類的__init__,都是Python基礎很少說了。下面的代碼就是一些狀態機代碼了,對了還有一個carry_image變量,保持瞭如今螞蟻正在搬運物體的圖像,或許是一片樹葉,或許是一隻死蜘蛛。這裏咱們寫了一個增強的render函數,由於咱們可能還須要畫一下搬的東西。

建造大腦

咱們給每一隻螞蟻賦予四個狀態,這樣才能足夠建造咱們的螞蟻的狀態機。在建造狀態機以前,咱們得先把這些狀態的詳細信息列出來。

狀態

動做

探索(Exploring)

隨機的走向一個點

蒐集(Seeking)

向一篇樹葉前進

搬運(Dellivering)

搬運一個什麼回去

狩獵(Hunting)

攻擊一隻蜘蛛

咱們也須要定義一下各個狀態之間的連接,或者能夠叫轉移條件。這裏舉兩個例子(實際上不止):

條件

轉移狀態

發現樹葉

蒐集

有蜘蛛攻擊

狩獵

咱們仍是最終畫一張圖來表示整個狀態機:


高水平的你也許能夠看着上面的圖寫狀態機了,不過爲了方便先創建一個State類,來保存一個狀態。很簡單,只是一個框子,實際上什麼都不作:

Python

 

1

2

3

4

5

6

7

8

9

10

11

class State():

    def __init__(self, name):

        self.name = name

    def do_actions(self):

        pass

    def check_conditions(self):

        pass

    def entry_actions(self):

        pass

    def exit_actions(self):

        pass

而後能夠創建一個狀態機類來管理這些狀態,這個狀態機但是整個代碼的核心類。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class StateMachine():

    def __init__(self):

        self.states = {}    # 存儲狀態

        self.active_state = None    # 當前有效狀態

    def add_state(self, state):

        # 增長狀態

        self.states[state.name] = state

    def think(self):

        if self.active_state is None:

            return

        # 執行有效狀態的動做,並作轉移檢查

        self.active_state.do_actions()

        new_state_name = self.active_state.check_conditions()

        if new_state_name is not None:

            self.set_state(new_state_name)

    def set_state(self, new_state_name):

        # 更改狀態,執行進入/退出動做

        if self.active_state is not None:

            self.active_state.exit_actions()

        self.active_state = self.states[new_state_name]

        self.active_state.entry_actions()

而後就能夠經過繼承State建立一系列的實際狀態了,這些狀態傳遞給StateMachine保留並運行。StateMachine類的think方法是檢查當前有效狀態並執行其動做的,最後還可能會調用set_state來進入下一個狀態。

 

用Python和Pygame寫遊戲-從入門到精通(16) 

下面給出完整代碼(注意須要gameobjects庫才能夠運行,參考以前的向量篇):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

SCREEN_SIZE = (640, 480)

NEST_POSITION = (320, 240)

ANT_COUNT = 20

NEST_SIZE = 100.

 

import pygame

from pygame.locals import *

 

from random import randint, choice

from gameobjects.vector2 import Vector2

 

class State(object):

    def __init__(self, name):

        self.name = name

    def do_actions(self):

        pass

    def check_conditions(self):

        pass

    def entry_actions(self):

        pass

    def exit_actions(self):

        pass        

 

class StateMachine(object):

    def __init__(self):

        self.states = {}

        self.active_state = None

 

    def add_state(self, state):

        self.states[state.name] = state

 

    def think(self):

        if self.active_state is None:

            return

        self.active_state.do_actions()

        new_state_name = self.active_state.check_conditions()

        if new_state_name is not None:

            self.set_state(new_state_name)

 

    def set_state(self, new_state_name):

        if self.active_state is not None:

            self.active_state.exit_actions()

        self.active_state = self.states[new_state_name]

        self.active_state.entry_actions()

 

class World(object):

    def __init__(self):

        self.entities = {}

        self.entity_id = 0

        self.background = pygame.surface.Surface(SCREEN_SIZE).convert()

        self.background.fill((255, 255, 255))

        pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION, int(NEST_SIZE))

 

    def add_entity(self, entity):

        self.entities[self.entity_id] = entity

        entity.id = self.entity_id

        self.entity_id += 1

 

    def remove_entity(self, entity):

        del self.entities[entity.id]

 

    def get(self, entity_id):

        if entity_id in self.entities:

            return self.entities[entity_id]

        else:

            return None

 

    def process(self, time_passed):

        time_passed_seconds = time_passed / 1000.0

        for entity in self.entities.values():

            entity.process(time_passed_seconds)

 

    def render(self, surface):

        surface.blit(self.background, (0, 0))

        for entity in self.entities.itervalues():

            entity.render(surface)

 

    def get_close_entity(self, name, location, range=100.):

        location = Vector2(*location)

        for entity in self.entities.itervalues():

            if entity.name == name:

                distance = location.get_distance_to(entity.location)

                if distance < range:

                    return entity

        return None

 

class GameEntity(object):

 

    def __init__(self, world, name, image):

 

        self.world = world

        self.name = name

        self.image = image

        self.location = Vector2(0, 0)

        self.destination = Vector2(0, 0)

        self.speed = 0.

        self.brain = StateMachine()

        self.id = 0

 

    def render(self, surface):

        x, y = self.location

        w, h = self.image.get_size()

        surface.blit(self.image, (x-w/2, y-h/2))  

 

    def process(self, time_passed):

        self.brain.think()

        if self.speed > 0. and self.location != self.destination:

            vec_to_destination = self.destination - self.location

            distance_to_destination = vec_to_destination.get_length()

            heading = vec_to_destination.get_normalized()

            travel_distance = min(distance_to_destination, time_passed * self.speed)

            self.location += travel_distance * heading

 

class Leaf(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "leaf", image)

 

class Spider(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "spider", image)

        self.dead_image = pygame.transform.flip(image, 0, 1)

        self.health = 25

        self.speed = 50. + randint(-20, 20)

 

    def bitten(self):

        self.health -= 1

        if self.health <= 0:

            self.speed = 0.

            self.image = self.dead_image

        self.speed = 140.

 

    def render(self, surface):

        GameEntity.render(self, surface)

        x, y = self.location

        w, h = self.image.get_size()

        bar_x = x - 12

        bar_y = y + h/2

        surface.fill( (255, 0, 0), (bar_x, bar_y, 25, 4))

        surface.fill( (0, 255, 0), (bar_x, bar_y, self.health, 4))

 

    def process(self, time_passed):

        x, y = self.location

        if x > SCREEN_SIZE[0] + 2:

            self.world.remove_entity(self)

            return

        GameEntity.process(self, time_passed)

 

class Ant(GameEntity):

    def __init__(self, world, image):

        GameEntity.__init__(self, world, "ant", image)

        exploring_state = AntStateExploring(self)

        seeking_state = AntStateSeeking(self)

        delivering_state = AntStateDelivering(self)

        hunting_state = AntStateHunting(self)

        self.brain.add_state(exploring_state)

        self.brain.add_state(seeking_state)

        self.brain.add_state(delivering_state)

        self.brain.add_state(hunting_state)

        self.carry_image = None

 

    def carry(self, image):

        self.carry_image = image

 

    def drop(self, surface):

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

            self.carry_image = None

 

    def render(self, surface):

        GameEntity.render(self, surface)

        if self.carry_image:

            x, y = self.location

            w, h = self.carry_image.get_size()

            surface.blit(self.carry_image, (x-w, y-h/2))

 

class AntStateExploring(State):

    def __init__(self, ant):

        State.__init__(self, "exploring")

        self.ant = ant

 

    def random_destination(self):

        w, h = SCREEN_SIZE

        self.ant.destination = Vector2(randint(0, w), randint(0, h))    

 

    def do_actions(self):

        if randint(1, 20) == 1:

            self.random_destination()

 

    def check_conditions(self):

        leaf = self.ant.world.get_close_entity("leaf", self.ant.location)

        if leaf is not None:

            self.ant.leaf_id = leaf.id

            return "seeking"

        spider = self.ant.world.get_close_entity("spider", NEST_POSITION, NEST_SIZE)

        if spider is not None:

            if self.ant.location.get_distance_to(spider.location) < 100.:

                self.ant.spider_id = spider.id

                return "hunting"

        return None

 

    def entry_actions(self):

        self.ant.speed = 120. + randint(-30, 30)

        self.random_destination()

 

class AntStateSeeking(State):

    def __init__(self, ant):

        State.__init__(self, "seeking")

        self.ant = ant

        self.leaf_id = None

 

    def check_conditions(self):

        leaf = self.ant.world.get(self.ant.leaf_id)

        if leaf is None:

            return "exploring"

        if self.ant.location.get_distance_to(leaf.location) < 5.0:

            self.ant.carry(leaf.image)

            self.ant.world.remove_entity(leaf)

            return "delivering"

        return None

 

    def entry_actions(self):

        leaf = self.ant.world.get(self.ant.leaf_id)

        if leaf is not None:

            self.ant.destination = leaf.location

            self.ant.speed = 160. + randint(-20, 20)

 

class AntStateDelivering(State):

    def __init__(self, ant):

        State.__init__(self, "delivering")

        self.ant = ant

 

    def check_conditions(self):

        if Vector2(*NEST_POSITION).get_distance_to(self.ant.location) < NEST_SIZE:

            if (randint(1, 10) == 1):

                self.ant.drop(self.ant.world.background)

                return "exploring"

        return None

 

    def entry_actions(self):

        self.ant.speed = 60.

        random_offset = Vector2(randint(-20, 20), randint(-20, 20))

        self.ant.destination = Vector2(*NEST_POSITION) + random_offset      

 

class AntStateHunting(State):

    def __init__(self, ant):

        State.__init__(self, "hunting")

        self.ant = ant

        self.got_kill = False

 

    def do_actions(self):

        spider = self.ant.world.get(self.ant.spider_id)

        if spider is None:

            return

        self.ant.destination = spider.location

        if self.ant.location.get_distance_to(spider.location) < 15.:

            if randint(1, 5) == 1:

                spider.bitten()

                if spider.health <= 0:

                    self.ant.carry(spider.image)

                    self.ant.world.remove_entity(spider)

                    self.got_kill = True

 

    def check_conditions(self):

        if self.got_kill:

            return "delivering"

        spider = self.ant.world.get(self.ant.spider_id)

        if spider is None:

            return "exploring"

        if spider.location.get_distance_to(NEST_POSITION) > NEST_SIZE * 3:

            return "exploring"

        return None

 

    def entry_actions(self):

        self.speed = 160. + randint(0, 50)

 

    def exit_actions(self):

        self.got_kill = False

 

def run():

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

    world = World()

    w, h = SCREEN_SIZE

    clock = pygame.time.Clock()

    ant_image = pygame.image.load("ant.png").convert_alpha()

    leaf_image = pygame.image.load("leaf.png").convert_alpha()

    spider_image = pygame.image.load("spider.png").convert_alpha()

 

    for ant_no in xrange(ANT_COUNT):

        ant = Ant(world, ant_image)

        ant.location = Vector2(randint(0, w), randint(0, h))

        ant.brain.set_state("exploring")

        world.add_entity(ant)

 

    while True:

        for event in pygame.event.get():

            if event.type == QUIT:

                return

        time_passed = clock.tick(30)

 

        if randint(1, 10) == 1:

            leaf = Leaf(world, leaf_image)

            leaf.location = Vector2(randint(0, w), randint(0, h))

            world.add_entity(leaf)

 

        if randint(1, 100) == 1:

            spider = Spider(world, spider_image)

            spider.location = Vector2(-50, randint(0, h))

            spider.destination = Vector2(w+50, randint(0, h))

            world.add_entity(spider)

 

        world.process(time_passed)

        world.render(screen)

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

這個程序的長度超過了以往任何一個,甚至可能比咱們寫的加起來都要長一些。然而它能夠展示給咱們的也史無前例的驚喜。無數勤勞的小螞蟻在整個地圖上處處覓食,隨機出現的葉子一旦被螞蟻發現,就會搬回巢穴,而蜘蛛一旦出如今巢穴範圍以內,就會被螞蟻們羣起而攻之,直到被驅逐出地圖範圍或者掛了,蜘蛛的屍體也會被帶入巢穴。

這個代碼寫的不夠漂亮,沒有用過高級的語法,甚至都沒有註釋天哪……基本代碼都在前面出現了,只是新引入了四個新的狀態,AntStateExploringAntStateSeekingAntStateDeliveringAntStateHunting,意義的話前面已經說明。好比說AntStateExploring,繼承了基本的Stat,這個狀態的動做平時就是讓螞蟻以一個隨機的速度走向屏幕隨機一個點,在此過程當中,check_conditions會不斷檢查周圍的環境,發現了樹葉或蜘蛛都會採起相應的措施(進入另一個狀態)。

用Python和Pygame寫遊戲-從入門到精通(17)

 

距離的魔法

咱們看現實中的東西,和咱們看畫面上的東西,最大差異在於能感覺現實物體的距離。而距離的產生,則是由於咱們雙眼看到的東西是不一樣的,兩眼交替閉合,你會發現眼前的東西左右移動。一隻眼睛則很難正確的判斷距離,雖然比上眼睛仍是能感受到遠近,但更精細一點,好比很難把線穿過針眼。

咱們在3D畫面上繪圖的時候,就要遵循這個規律,看看下面的代碼。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

import pygame

from pygame.locals import *

from random import randint

 

class Star(object):

 

    def __init__(self, x, y, speed):

 

        self.x = x

        self.y = y

        self.speed = speed

 

def run():

 

    pygame.init()

    screen = pygame.display.set_mode((640, 480)) #, FULLSCREEN)

 

    stars = []

 

    # 在第一幀,畫上一些星星

    for n in xrange(200):

 

        x = float(randint(0, 639))

        y = float(randint(0, 479))

        speed = float(randint(10, 300))

        stars.append( Star(x, y, speed) )

 

    clock = pygame.time.Clock()

 

    white = (255, 255, 255)

 

    while True:

 

        for event in pygame.event.get():

            if event.type == QUIT:

                return

            if event.type == KEYDOWN:

                return

 

        # 增長一顆新的星星

        y = float(randint(0, 479))

        speed = float(randint(10, 300))

        star = Star(640., y, speed)

        stars.append(star)

 

        time_passed = clock.tick()

        time_passed_seconds = time_passed / 1000.

 

        screen.fill((0, 0, 0))

 

        # 繪製全部的星

        for star in stars:

 

            new_x = star.x - time_passed_seconds * star.speed

            pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1., star.y))

            star.x = new_x

 

        def on_screen(star):

            return star.x > 0

 

        # 星星跑出了畫面,就刪了它

        stars = filter(on_screen, stars)

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

這裏你還能夠把FULLSCREEN加上,更有感受。

這個程序給個人畫面,發揮一下你的想象,不是一片宇宙麼,無數的星雲穿梭,近的速度更快,遠的則很慢。而實際上看代碼,咱們只是畫了一些長短不一樣的線而已!雖然很簡單,仍是用了很多很多python的技術,特別是函數式編程的(小)技巧。不過強大的你必定沒問題:)可是pygame的代碼,沒有任何沒講過的,爲何這樣就能有3D的效果了?感謝你的大腦,由於它知道遠的看起來更慢,因此這樣的錯覺就產生了。

理解3D空間

3D空間的事情,基本就是立體幾何的問題,高中學一半應該就差很少理解了,這裏很少講了。你能明白下圖的小球在(7, 5, 10)的位置,換句話說,若是你站在原點,面朝Z軸方向。那麼小球就在你左邊7,上面5,前面10的位置。這就夠了~

 

使用3D向量

咱們已經學習了二維向量來表達運動,在三維空間內,固然要使用三維的向量。其實和二維的概念都同樣,加減縮放啥的,這裏就不用三個元素的元組列表先演練一番了,直接祭出咱們的gameobjects神器吧!

Python

 

1

2

3

4

5

6

7

8

9

10

from gameobjects.vector3 import *

A = Vector3(6, 8, 12)

B = Vector3(10, 16, 12)

print "A is", A

print "B is", B

print "Magnitude of A is", A.get_magnitude()

print "A+B is", A+B

print "A-B is", A–B

print "A normalized is", A.get_normalized()

print "A*2 is", A * 2

運行一下看看結果吧,有些無趣?確實,光數字無法展示3D的美啊,下一次,讓咱們把物體在立體空間內運動起來。

用Python和Pygame寫遊戲-從入門到精通(18)

 

基於時間的三維移動

咱們使用Vector3類來進行3D上的移動,與2D很是相似,看下面一個例子:

 

直升機A在(-6, 2, 2)的位置上,目標是直升機B(7, 5, 10),A想摧毀B,因此發射了一枚火箭AB,如今咱們得把火箭的運動軌跡過程給畫出來,不然一點發射敵機就炸了,多沒意思啊~~ 經過計算出二者之間的向量爲(13, 3, 8),而後單位化這個向量,這樣就能夠在運動中用到了,下面的代碼作了這些事情。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

from gameobjects.vector3 import *

A = (–6, 2, 2)

B = (7, 5, 10)

plasma_speed = 100. # meters per second

AB = Vector3.from_points(A, B)

print "Vector to droid is", AB

distance_to_target = AB.get_magnitude()

print "Distance to droid is", distance_to_target, "meters"

plasma_heading = AB.get_normalized()

print "Heading is", plasma_heading

#######輸出結果#########

Vector to droid is (13, 3, 8)

Distance to droid is 15.5563491861 meters

Heading is (0.835672, 0.192847, 0.514259)

而後不停的重繪火箭的位置,用這個語句:
rocket_location += heading * time_passed_seconds * speed

不過咱們還不能直接在pygame中繪製3D物體,得先學習一下下面講的,「如何把3D轉換爲2D」。

3D透視

若是您初中美術認真學了的話,應該會知道這裏要講什麼,還記得當初咱們是如何在紙上畫立方體的?

忘了?OK,從頭開始提及吧,存儲、計算3D座標是很是容易的,可是要把它展示到屏幕上就不那麼簡單了,由於pygame中全部的繪圖函數都只接受2D座標,所以,咱們必須把這些3D的座標投影到2D的圖面上。

平行投影

最簡單的投影方法是——把第三個座標z座標給丟棄,用這樣的一個簡單的函數就能夠作到:

Python

 

1

2

def parallel_project(vector3):

    return (vector3.x, vector3.y)

儘管這樣的轉換簡單又快速,咱們卻不能用它。爲何?效果太糟糕了,咱們甚至沒法在最終的畫面上感覺到一點立體的影子,這個世界看起來仍是平的,沒有那個物體看起來比其餘物體更遠或更近。就好像我右邊這幅圖同樣。

立體投影

在3D遊戲中使用的更爲普遍且合理的技術是立體投影,由於它的結果更爲真實。立體投影把遠處的物體縮小了,也就是使用透視法(foreshortening),如左圖所示,而後下面是咱們的轉換函數,看起來也很簡單:

Python

 

1

2

3

def perspective_project(vector3, d):

    x, y, z = vector3

    return (x * d/z, –y * d/z)

與上一個轉換函數不一樣的是,這個轉換函數還接受一個d參數(後面討論),而後全部的x、y座標都會接受這個d的洗禮,同時z也會插一腳,把本來的座標進行縮放。

d的意思是視距(viewing distance),也就是攝像頭到3D世界物體在屏幕上的像素體現之間的距離。好比說,一個在(10, 5, 100)的物體移動到了(11, 5, 100),視距是100的時候,它在屏幕上就恰好移動了1個像素,但若是它的z不是100,或者視距不是100,那麼可能移動距離就再也不是1個像素的距離。有些抽象,不過玩過3D遊戲的話(這裏指國外的3D大做),都有一種滾輪調節遠近的功能,這就是視距(固然調的時候視野也會變化,這個下面說)。

在咱們玩遊戲的時候,視距就爲咱們的眼睛到屏幕的直線距離(以像素爲單位)。

視野

那麼咱們怎麼選取一個好的d呢?咱們固然能夠不斷調整實驗來獲得一個,不過咱們還能夠經過視野(field of view)來計算一個出來。視野也就是在一個時刻能看到的角度。看一下左圖的視野和視距的關係,能夠看到二者是有制約關係,當視野角度(fov)增大的時候,d就會減少;而d增長的話,視野角度就會減少,能看到的東西也就變少了。

視野是決定在3D畫面上展示多少東西的絕好武器,而後咱們還須要一個d來決定透視深度,使用一點點三角只是,咱們就能夠從fov計算出d,寫一下下面的代碼學習學習:
Internet上,你老是能找到99%以上的須要的別人寫好的代碼。不過偶爾仍是要本身寫一下的,不用擔憂本身的數學是不及格的,這個很簡單~ 不少時候實際動手試一下,你就能明白更多。

Python

 

1

2

3

4

from math import tan

def calculate_viewing_distance(fov, screen_width):

    d = (screen_width/2.0) / tan(fov/2.0)

    return d

fov角度可能取45~60°比較合適,這樣看起來很天然。固然每一個人每一個遊戲都有特別的地方,好比FPS的話,fov可能比較大;而策略性遊戲的fov角度會比較小,這樣能夠看到更多的東西。不少時候還須要不停的變化fov,最明顯的CS中的狙擊槍(從沒玩過,不過聽過),開出來和關掉是徹底不一樣的效果,改的就是視野角度。

今天又是補充了一大堆知識,等不及了吧~咱們下一章就能看到一個用pygame畫就的3D世界了!

用Python和Pygame寫遊戲-從入門到精通(19)

 

3D世界

讓咱們如今開始寫一個3D的程序,鞏固一下這幾回學習的東西。由於咱們尚未好好深刻如何畫3D物體,暫時就先用最簡單的投影(上次討論過的第二種)方法來畫吧。這個程序畫一個空間裏的立方體,只不過各個部分並不會隨着距離而產生大小上的變化。

您能夠看到,不少的小球構成了立方體的各個邊,經過按住方向鍵,能夠水平或垂直方向的更改「攝像頭」的位置,Q和A鍵會把攝像頭拉近或拉遠,而W和S會改變視距,綠色的三角是視距和視角的示意圖。fov角大的話,立方體就顯得比較短,反之就顯得比較長。

 

代碼稍微有點長,下面有解釋,靜下心來慢慢閱讀。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

import pygame

from pygame.locals import *

from gameobjects.vector3 import Vector3

 

from math import *

from random import randint

 

SCREEN_SIZE =  (640, 480)

CUBE_SIZE = 300

 

def calculate_viewing_distance(fov, screen_width):

    d = (screen_width/2.0) / tan(fov/2.0)

    return d

 

def run():

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)

 

    default_font = pygame.font.get_default_font()

    font = pygame.font.SysFont(default_font, 24)

 

    ball = pygame.image.load("ball.png").convert_alpha()

 

    # 3D points

    points = []

 

    fov = 90. # Field of view

    viewing_distance = calculate_viewing_distance(radians(fov), SCREEN_SIZE[0])

 

    # 邊沿的一系列點

    for x in xrange(0, CUBE_SIZE+1, 20):

        edge_x = x == 0 or x == CUBE_SIZE

 

        for y in xrange(0, CUBE_SIZE+1, 20):

            edge_y = y == 0 or y == CUBE_SIZE

 

            for z in xrange(0, CUBE_SIZE+1, 20):

                edge_z = z == 0 or z == CUBE_SIZE

 

                if sum((edge_x, edge_y, edge_z)) >= 2:

 

                    point_x = float(x) - CUBE_SIZE/2

                    point_y = float(y) - CUBE_SIZE/2

                    point_z = float(z) - CUBE_SIZE/2

 

                    points.append(Vector3(point_x, point_y, point_z))

 

    # 以z序存儲,相似於css中的z-index

    def point_z(point):

        return point.z

    points.sort(key=point_z, reverse=True)

 

    center_x, center_y = SCREEN_SIZE

    center_x /= 2

    center_y /= 2

 

    ball_w, ball_h = ball.get_size()

    ball_center_x = ball_w / 2

    ball_center_y = ball_h / 2

 

    camera_position = Vector3(0.0, 0.0, -700.)

    camera_speed = Vector3(300.0, 300.0, 300.0)

 

    clock = pygame.time.Clock()

 

    while True:        

 

        for event in pygame.event.get():

            if event.type == QUIT:

                return            

 

        screen.fill((0, 0, 0))

 

        pressed_keys = pygame.key.get_pressed()

 

        time_passed = clock.tick()

        time_passed_seconds = time_passed / 1000.

 

        direction = Vector3()

        if pressed_keys[K_LEFT]:

            direction.x = -1.0

        elif pressed_keys[K_RIGHT]:

            direction.x = +1.0

 

        if pressed_keys[K_UP]:

            direction.y = +1.0

        elif pressed_keys[K_DOWN]:

            direction.y = -1.0

 

        if pressed_keys[K_q]:

            direction.z = +1.0

        elif pressed_keys[K_a]:

            direction.z = -1.0

 

        if pressed_keys[K_w]:

            fov = min(179., fov+1.)

            w = SCREEN_SIZE[0]

            viewing_distance = calculate_viewing_distance(radians(fov), w)

        elif pressed_keys[K_s]:

            fov = max(1., fov-1.)

            w = SCREEN_SIZE[0]

            viewing_distance = calculate_viewing_distance(radians(fov), w)

 

        camera_position += direction * camera_speed * time_passed_seconds      

 

        # 繪製點

        for point in points:

 

            x, y, z = point - camera_position

 

            if z > 0:

                x =  x * viewing_distance / z

                y = -y * viewing_distance / z

                x += center_x

                y += center_y

                screen.blit(ball, (x-ball_center_x, y-ball_center_y))

 

        # 繪製表

        diagram_width = SCREEN_SIZE[0] / 4

        col = (50, 255, 50)

        diagram_points = []

        diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )

        diagram_points.append( (0, 100) )

        diagram_points.append( (diagram_width, 100) )

        diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )

        diagram_points.append( (diagram_width/2, 100) )

        pygame.draw.lines(screen, col, False, diagram_points, 2)        

 

        # 繪製文字

        white = (255, 255, 255)

        cam_text = font.render("camera = "+str(camera_position), True, white)

        screen.blit(cam_text, (5, 5))

        fov_text = font.render("field of view = %i"%int(fov), True, white)

        screen.blit(fov_text, (5, 35))

        txt = "viewing distance = %.3f"%viewing_distance

        d_text = font.render(txt, True, white)

        screen.blit(d_text, (5, 65))

 

        pygame.display.update()

 

if __name__ == "__main__":

    run()

上面的例子使用Vector3來管理向量數據,點的存儲是按照z座標來的,這樣在blit的時候,離攝像機近的後畫,就能夠覆蓋遠的,不然看起來就太奇怪了……

在主循環的代碼中,會根據按鍵攝像頭會更改位置——固然這是用戶的感受,實際上代碼作的是更改了點的位置。而3D的移動和2D是很是像的,只不過多了一個z來判斷覆蓋遠近(也就是繪製順序),同樣的基於時間移動,同樣的向量運算,只是由Vector2變爲了Vector3。

而後代碼須要繪製所有的點。首先,點的位置須要根據camera_position變量校訂,若是結果的z比0還大,說明點在攝像頭以前,須要畫的,不然就是不須要畫。y須要校準一下方向,最後把x、y定位在中間(小球仍是有一點點尺寸的)。

 

 

3D第一部分總結

3D是迄今爲止遊戲發展中最大的里程碑(下一個會是什麼呢?虛擬體驗?),咱們這幾回學習的,是3D的基礎,你能夠看到,僅有2D繪圖經驗也能很方便的過渡過來。僅僅是Vector2→Vector3,擔任3D向量仍是有一些特有的操做的,須要慢慢學習,可是基本的思想不會變。

可是,請繼續思考~ 難道全部的3D遊戲就是一系列的3D座標再手動轉換爲2D畫上去就行了?很惋惜或者說很幸運不是的,咱們有3D引擎來作這些事情,對Pygame來講,原生的3D處理時不存在的,那麼如何真正繪製3D畫面?有一個很是普遍的選擇——OpenGL,不瞭解的請自行Wiki,不過OpenGL並非Pygame的一部分,並且3D實際作下去實在很繁雜,這個系列就暫時不深刻講解了。

儘管有3D引擎幫咱們作投影等事情,咱們這幾回學的東西絕對不是白費,對基礎的必定了解能夠更好的寫入3D遊戲,對畫面的掌握也會更好。若是有須要,這個系列的主線完成後,會根據你們的要求追加講解OpenGL的內容。

 

用Python和Pygame寫遊戲-從入門到精通(20)

 

什麼是聲音?

又要開始講原理了啊,作遊戲真是什麼都要懂,物理數學美術心理學和編程等等等等,你們都不容易呀~~

聲音的本質是振動,經過各類介質傳播到咱們的耳朵裏。基本任何物質均可以振動,好比說一旦咱們敲打桌子,桌子表面會快速振動,推進附近的空氣一塊兒振動,而這種振動會傳播(宛如水中扔一顆石子,水波會慢慢傳播同樣),這種振動最終進入咱們的耳道,使得鼓膜振動,引發咱們的聽覺。

振動的幅度(響度)越大,聽到的聲音也就越大,這個很好理解,咱們越用力拍桌子,聲音也就越大(同時手也越疼——)。同時,振動的快慢(音調)也會直接影響咱們對聲音高低的判斷,也就是平時說的高音和低音的差異,決定着個音調的要素每秒振動的次數,也就是頻率,單位是赫茲(Hz)。好比100Hz意味着這個振動在1秒內進行了100次。音色也是一個重要指標,敲打木頭和金屬聽到的聲音徹底不一樣,是音色的做用,這個的要素是有振動波形的形狀來決定。

現實中不少聲音都是許多不一樣的聲音組合而來的。同時聲音在傳播的時候也會發生變化,最直接的就是隨着距離增大,響度會減少;而在不一樣的環境中,由於反射和混合,聲音的效果也徹底不同。這些都要好好考慮,好比腳步聲,空曠的山谷中應該是「空谷足音」的效果,樓梯上則是比較短可是漸漸靠近的效果。甚至發聲物體的速度也會影響咱們聽到的聲音,謂之「多普勒效應」……好麻煩!不過最後遊戲裏可能不是那麼符合現實的,好比說太空中發射導彈什麼,按說是聽不到聲音的,由於沒有介質傳播,不過爲了效果好,咱也不在乎了……

聲音的存儲

聲音徹底是一種模擬的信號,而咱們的計算機只能存儲數字(二進制)信號,咋辦?數字化咯~

(一下說明摘錄修改自軒轅天數-絲竹的文章,表示感謝)

以最多見的WAV文件爲例,要把聲音記錄成WAV格式,電腦要先把聲音的波形「畫在一張座標紙上」。而後呢,電腦要看了「橫座標第一格處,波形圖的縱座標是多少啊?哦,差很少是500啊(僅僅是打比方,並且這個「差很少」很關鍵),那麼橫座標第二格呢?…」最後,電腦就得出來一大堆座標值。而後再通過一些其餘後續工做,電腦就把這些座標值保存下來了。

 

當要放音的時候,電腦要根據這些「座標值在座標紙上面畫點」,最後「用線把點連起來」,差很少就把原先的波形還原出來了。其實數字化錄音基本上就是這樣的原理。

電腦記錄波形時,用的座標紙格子越密,天然記錄下來的點就越多、越精確,未來還原出來的波形就越接近原始波形?上邊例子的橫座標軸格子密度就至關於採樣頻率(好比,44.1KHz),縱座標格子密度就至關於量化精度(好比,16BIT)。這就是「KHZ」、「BIT」的值越高,音樂的音質越好的緣由。

這個世界上天然不只僅有WAV這一種存儲聲音的文件格式,宛若圖像文件格式中的BMP同樣,WAV是一種無壓縮的格式,體積最大;而OGG則好像PNG,是無損的壓縮,能夠徹底保持圖像的本真,可是大小又比較小;經常使用的MP3,則是相似於JPG的有損壓縮格式。

聲音處理

想要得到聲音,最簡單的天然是錄製,不過有的時候比較困難,好比錄製心跳要很高昂的儀器,而錄製火山爆發的聲音實在過於……

這時候咱們能夠手動合成聲音,而錄製得到的聲音還須要通過處理,好比淨化等,有不少軟件能夠選擇,開源的Audacity就是一個很不錯的選擇。具體的這裏就不說了,一門大學問啊。

網上也有不少聲音素材可供下載,好的專業的素材都是賣錢的,哎這個世界什麼都是錢啊~~

Pygame中聲音的初始化

此次來不及舉一個實際例子放聲音了,先說一下初始化。

在pygame中,使用mixer模塊來播放聲音,不過在實際播放以前,咱們須要使用pygame.mixer.init函數來初始化一些參數,不過在有的平臺上,pygame.mixer.init會隨着pygame.init一塊兒被初始化,pygame乾脆提供了一個pygame.mixer.pre_init()來進行最早的初始化工做,參數說明以下:

  • frequency – 聲音文件的採樣率,儘管高採樣率可能會下降性能,可是再次的聲卡均可以輕鬆對應44.1KHz的聲音回放,因此就設這個值吧;
  • size – 量化精度
  • stereo – 立體聲效果,1:mono,2:stereo,具體請google,通常設2好了
  • buffer – 緩衝大小,2的倍數,設4096就差很少了吧

你能夠像這樣初始化聲音:

Python

 

1

2

pygame.mixer.pre_init(44100, 16, 2, 4096)

pygame.init()

這裏先用pre_init來設定了參數,而後在pygame.init中初始化全部的東西。

若是你須要從新設定聲音的參數,那麼你須要先執行pygame.mixer.quit而後再執行pygame.mixer.init,不過通常用不到吧……

用Python和Pygame寫遊戲-從入門到精通(21)

 

 

Sound對象

在初始化聲音系統以後,咱們就能夠讀取一個音樂文件到一個Sound對象中了。pygame.mixer.Sound()接受一個文件名,或者也能夠使一個文件對象,不過這個文件必須是WAV或者OGG,切記!

Python

 

1

hello_sound = Pygame.mixer.Sound("hello.ogg")

一旦這個Sound對象出來了,你能夠使用play方法來播放它。play(loop, maxtime)能夠接受兩個參數,loop天然就是重複的次數,-1意味着無限循環,1呢?是兩次,記住是重複的次數而不是播放的次數;maxtime是指多少毫秒後結束,這個很簡單。當你不使用任何參數調用的時候,意味着把這個聲音播放一次。一旦play方法調用成功,就會返回一個Channel對象,不然返回一個None。

Channel對象

Channel,也就是聲道,能夠被聲卡混合(共同)播放的數據流。遊戲中能夠同時播放的聲音應該是有限的,pygame中默認是8個,你能夠經過pygame.mixer.get_num_channels()來得知當前系統能夠同時播放的聲道數,而一旦超過,調用sound對象的play方法就會返回一個None,若是你肯定本身要同時播放不少聲音,能夠用set_num_channels()來設定一下,最好一開始就設,由於從新設定會中止當前播放的聲音。

那麼Channel對象有什麼用呢?若是你只是想簡單的播放一下聲音,那麼根本不用管這個東西,不過若是你想創造出一點有意境的聲音,好比說一輛火車從左到右呼嘯而過,那麼應該是一開始左聲道比較響,而後至關,最後右聲道比較響,直至慢慢消失。這就是Channel能作到的事情。Channel的set_volume(left, right)方法接受兩個參數,分別是表明左聲道和右聲道的音量的小數,從0.0~1.0。

豎起咱們的耳朵

OK,來個例子試試吧~

這個例子裏咱們經過釋放金屬小球並讓它們自由落體和彈跳,聽碰撞的聲音。這裏面不只僅有此次學習的聲音,還有幾個一塊兒沒有接觸到的技術,最重要的一個就是重力的模擬,咱們能夠設置一個重力因子來影響小球下落的速度,還有一個彈力系數,來決定每次彈跳損失的能量,雖然不難,可是遊戲中引入這個東西能讓咱們的遊戲仿真度大大提升。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

SCREEN_SIZE = (640, 480)

 

# 重力因子,其實是單位 像素/平方秒

GRAVITY = 250.0

# 彈力系數,不要超過1!

BOUNCINESS = 0.7

 

import pygame

from pygame.locals import *

from random import randint

from gameobjects.vector2 import Vector2

 

def stero_pan(x_coord, screen_width):

    """這個函數根據位置決定要播放聲音左右聲道的音量"""

    right_volume = float(x_coord) / screen_width

    left_volume = 1.0 - right_volume

    return (left_volume, right_volume)

 

class Ball():

    """小球類,實際上咱們能夠使用Sprite類來簡化"""

    def __init__(self, position, speed, image, bounce_sound):

        self.position = Vector2(position)

        self.speed = Vector2(speed)

        self.image = image

        self.bounce_sound = bounce_sound

        self.age = 0.0

 

    def update(self, time_passed):

        w, h = self.image.get_size()

        screen_width, screen_height = SCREEN_SIZE

 

        x, y = self.position

        x -= w/2

        y -= h/2

        # 是否是要反彈了

        bounce = False

 

        # 小球碰壁了麼?

        if y + h >= screen_height:

            self.speed.y = -self.speed.y * BOUNCINESS

            self.position.y = screen_height - h / 2.0 - 1.0

            bounce = True

        if x <= 0:

            self.speed.x = -self.speed.x * BOUNCINESS

            self.position.x = w / 2.0 + 1

            bounce = True

        elif x + w >= screen_width:

            self.speed.x = -self.speed.x * BOUNCINESS

            self.position.x = screen_width - w / 2.0 - 1

            bounce = True

 

        # 根據時間計算如今的位置,物理好的馬上發現這其實不標準,

        # 正規的應該是「s = 1/2*g*t*t」,不過這樣省事省時一點,咱只是模擬~

        self.position += self.speed * time_passed

        # 根據重力計算速度

        self.speed.y += time_passed * GRAVITY

 

        if bounce:

            self.play_bounce_sound()

 

        self.age += time_passed

 

    def play_bounce_sound(self):

        """這個就是播放聲音的函數"""

        channel = self.bounce_sound.play()

 

        if channel is not None:

            # 設置左右聲道的音量

            left, right = stero_pan(self.position.x, SCREEN_SIZE[0])

            channel.set_volume(left, right)

 

    def render(self, surface):

        # 真有點麻煩了,有愛的,本身用Sprite改寫下吧……

        w, h = self.image.get_size()

        x, y = self.position

        x -= w/2

        y -= h/2

        surface.blit(self.image, (x, y))

 

def run():

    # 上一次的內容

    pygame.mixer.pre_init(44100, 16, 2, 1024*4)

    pygame.init()

    pygame.mixer.set_num_channels(8)

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)

 

    pygame.mouse.set_visible(False)

    clock = pygame.time.Clock()

 

    ball_image = pygame.image.load("ball.png").convert_alpha()

    mouse_image = pygame.image.load("mousecursor.png").convert_alpha()

 

    # 加載聲音文件

    bounce_sound = pygame.mixer.Sound("bounce.ogg")

    balls = []

 

    while True:

        for event in pygame.event.get():

            if event.type == QUIT:

                return

            if event.type == MOUSEBUTTONDOWN:

                # 剛剛出來的小球,給一個隨機的速度

                random_speed = ( randint(-400, 400), randint(-300, 0) )

                new_ball = Ball( event.pos,

                                 random_speed,

                                 ball_image,

                                 bounce_sound )

                balls.append(new_ball)

 

        time_passed_seconds = clock.tick() / 1000.

        screen.fill((255, 255, 255))

        # 爲防止小球太多,把超過壽命的小球加入這個「死亡名單」

        dead_balls = []

        for ball in balls:

            ball.update(time_passed_seconds)

            ball.render(screen)

            # 每一個小球的的壽命是10秒

            if ball.age > 10.0:

                dead_balls.append(ball)

        for ball in dead_balls:

            balls.remove(ball)

 

        mouse_pos = pygame.mouse.get_pos()

        screen.blit(mouse_image, mouse_pos)

        pygame.display.update()

 

if __name__ == "__main__":

    run()

 

 

用Python和Pygame寫遊戲-從入門到精通(22)

 

Sound對象

方法名

做用

fadeout

淡出聲音,可接受一個數字(毫秒)做爲淡出時間

get_length

得到聲音文件長度,以秒計

get_num_channels

聲音要播放多少次

get_volume

獲取音量(0.0 ~ 1.0)

play

開始播放,返回一個Channel對象,失敗則返回None

set_volume

設置音量

stop

馬上中止播放

Channels對象

方法名

做用

fadeout

相似

get_busy

若是正在播放,返回true

get_endevent

獲取播放完畢時要作的event,沒有則爲None

get_queue

獲取隊列中的聲音,沒有則爲None

get_volume

相似

pause

暫停播放

play

相似

queue

將一個Sound對象加入隊列,在當前聲音播放完畢後播放

set_endevent

設置播放完畢時要作的event

set_volume

相似

stop

馬上中止播放

unpause

繼續播放

Music對象:

方法名

做用

fadeout

相似

get_endevent

相似

get_volume

相似

load

加載一個音樂文件

pause

相似

play

相似

rewind

從頭開始從新播放

set_endevent

相似

set_volume

相似

stop

馬上中止播放

unpause

繼續播放

get_pos

得到當前播放的位置,毫秒計

 

界面如上,運行的時候,腳本讀取./MUSIC下全部的OGG和MP3文件(若是你不是Windows,可能要去掉MP3的判斷),顯示的也很簡單,幾個控制按鈕,下面顯示當前歌名(顯示中文老是不那麼方便的,若是你運行失敗,請具體參考代碼內的註釋本身修改):

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

# -*- coding: utf-8 -*-

# 注意文件編碼也必須是utf-8

SCREEN_SIZE = (800, 600)

# 存放音樂文件的位置

MUSIC_PATH = "./MUSIC"

 

import pygame

from pygame.locals import *

from math import sqrt

import os

import os.path

 

def get_music(path):

 

    # 從文件夾來讀取全部的音樂文件

    raw_filenames = os.listdir(path)

 

    music_files = []

    for filename in raw_filenames:

        # 不是Windows的話,仍是去掉mp3吧

        if filename.lower().endswith('.ogg') or filename.lower().endswith('.mp3'):

            music_files.append(os.path.join(MUSIC_PATH, filename))

 

    return sorted(music_files)

 

class Button(object):

    """這個類是一個按鈕,具備自我渲染和判斷是否被按上的功能"""

    def __init__(self, image_filename, position):

 

        self.position = position

        self.image = pygame.image.load(image_filename)

 

    def render(self, surface):

        # 屢見不鮮的代碼了

        x, y = self.position

        w, h = self.image.get_size()

        x -= w / 2

        y -= h / 2

        surface.blit(self.image, (x, y))

 

    def is_over(self, point):

        # 若是point在自身範圍內,返回True

        point_x, point_y = point

        x, y = self.position

        w, h = self.image.get_size()

        x -= w /2

        y -= h / 2

 

        in_x = point_x >= x and point_x < x + w

        in_y = point_y >= y and point_y < y + h

        return in_x and in_y

 

def run():

 

    pygame.mixer.pre_init(44100, 16, 2, 1024*4)

    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0)    

 

    #font = pygame.font.SysFont("default_font", 50, False)

    # 爲了顯示中文,我這裏使用了這個字體,具體本身機器上的中文字體請本身查詢

    # 詳見本系列第四部分://eyehere.net/2011/python-pygame-novice-professional-4/

    font = pygame.font.SysFont("simsunnsimsun", 50, False)    

 

    x = 100

    y = 240

    button_width = 150

    buttons = {}

    buttons["prev"] = Button("prev.png", (x, y))

    buttons["pause"] = Button("pause.png", (x+button_width*1, y))

    buttons["stop"] = Button("stop.png", (x+button_width*2, y))

    buttons["play"] = Button("play.png", (x+button_width*3, y))

    buttons["next"] = Button("next.png", (x+button_width*4, y))

 

    music_filenames = get_music(MUSIC_PATH)

    if len(music_filenames) == 0:

        print "No music files found in ", MUSIC_PATH

        return

 

    white = (255, 255, 255)

    label_surfaces = []

    # 一系列的文件名render

    for filename in music_filenames:

        txt = os.path.split(filename)[-1]

        print "Track:", txt

        # 這是簡體中文Windows下的文件編碼,根據本身系統狀況請酌情更改

        txt = txt.split('.')[0].decode('gb2312')

        surface = font.render(txt, True, (100, 0, 100))

        label_surfaces.append(surface)

 

    current_track = 0

    max_tracks = len(music_filenames)

    pygame.mixer.music.load( music_filenames[current_track] )  

 

    clock = pygame.time.Clock()

    playing = False

    paused = False

 

    # USEREVENT是什麼?請參考本系列第二部分:

    # //eyehere.net/2011/python-pygame-novice-professional-2/

    TRACK_END = USEREVENT + 1

    pygame.mixer.music.set_endevent(TRACK_END)

 

    while True:

 

        button_pressed = None

 

        for event in pygame.event.get():

 

            if event.type == QUIT:

                return

 

            if event.type == MOUSEBUTTONDOWN:

 

                # 判斷哪一個按鈕被按下

                for button_name, button in buttons.iteritems():

                    if button.is_over(event.pos):

                        print button_name, "pressed"

                        button_pressed = button_name

                        break

 

            if event.type == TRACK_END:

                # 若是一曲播放結束,就「模擬」按下"next"

                button_pressed = "next"

 

        if button_pressed is not None:

 

            if button_pressed == "next":

                current_track = (current_track + 1) % max_tracks

                pygame.mixer.music.load( music_filenames[current_track] )

                if playing:

                    pygame.mixer.music.play()

 

            elif button_pressed == "prev":

 

                # prev的處理方法:

                # 已經播放超過3秒,從頭開始,不然就播放上一曲

                if pygame.mixer.music.get_pos() > 3000:

                    pygame.mixer.music.stop()

                    pygame.mixer.music.play()

                else:

                    current_track = (current_track - 1) % max_tracks

                    pygame.mixer.music.load( music_filenames[current_track] )

                    if playing:

                        pygame.mixer.music.play()

 

            elif button_pressed == "pause":

                if paused:

                    pygame.mixer.music.unpause()

                    paused = False

                else:

                    pygame.mixer.music.pause()

                    paused = True

 

            elif button_pressed == "stop":

                pygame.mixer.music.stop()

                playing = False

 

            elif button_pressed == "play":

                if paused:

                    pygame.mixer.music.unpause()

                    paused = False

                else:

                    if not playing:

                        pygame.mixer.music.play()

                        playing = True

 

        screen.fill(white)

 

        # 寫一下當前歌名

        label = label_surfaces[current_track]

        w, h = label.get_size()

        screen_w = SCREEN_SIZE[0]

        screen.blit(label, ((screen_w - w)/2, 450))

 

        # 畫全部按鈕

        for button in buttons.values():

            button.render(screen)

 

        # 由於基本是不動的,這裏幀率設的很低

        clock.tick(5)

        pygame.display.update()

 

if __name__ == "__main__":

 

    run()

這個程序雖然能夠運行,仍是很簡陋,有興趣的能夠改改,好比顯示播放時間/總長度,甚至更厲害一點,鼠標移動到按鈕上班,按鈕會產生一點變化等等,咱們如今已經什麼都學過了,惟一欠缺的就是實踐而已!

 

用Python和Pygame寫遊戲-從入門到精通(py2exe篇)

 

perl有perlcc(免費高效但配置極其複雜),perlapp(簡單效果也不錯可是收費)等工具;而對python來講,py2exe是不二之選,首先是免費的,並且壓出來的文件,雖然不能和編譯軟件相比,仍是不錯的了。

到py2exe的官方網站下載安裝包,注意要對應本身的python版本。

py2exe是須要寫一個腳本進行打包的操做,使用下面這個專爲pygame寫就的腳本(參考py2exe官方),能夠極大的方便打包操做,注意在使用前修改BuildExe裏的各個參數。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

#!python

# -*- coding: gb2312 -*-

 

# 這個腳本專爲pygame優化,使用py2exe打包代碼和資源至dist目錄

#

# 使用中如有問題,能夠留言至:

#  //eyehere.net/2011/python-pygame-novice-professional-py2exe/

#

# 安裝需求:

#         python, pygame, py2exe 都應該裝上

 

# 使用方法:

#         1: 修改此文件,指定須要打包的.py和對應數據

#         2: python pygame2exe.py

#         3: 在dist文件夾中,enjoy it~

 

try:

    from distutils.core import setup

    import py2exe, pygame

    from modulefinder import Module

    import glob, fnmatch

    import sys, os, shutil

except ImportError, message:

    raise SystemExit,  "Sorry, you must install py2exe, pygame. %s" % message

 

# 這個函數是用來判斷DLL是不是系統提供的(是的話就不用打包)

origIsSystemDLL = py2exe.build_exe.isSystemDLL

def isSystemDLL(pathname):

    # 須要hack一下,freetype和ogg的dll並非系統DLL

    if os.path.basename(pathname).lower() in ("libfreetype-6.dll", "libogg-0.dll", "sdl_ttf.dll"):

        return 0

    return origIsSystemDLL(pathname)

# 把Hack過的函數從新寫回去

py2exe.build_exe.isSystemDLL = isSystemDLL

 

# 這個新的類也是一個Hack,使得pygame的默認字體會被拷貝

class pygame2exe(py2exe.build_exe.py2exe):

    def copy_extensions(self, extensions):

        # 得到pygame默認字體

        pygamedir = os.path.split(pygame.base.__file__)[0]

        pygame_default_font = os.path.join(pygamedir, pygame.font.get_default_font())

        # 加入拷貝文件列表

        extensions.append(Module("pygame.font", pygame_default_font))

        py2exe.build_exe.py2exe.copy_extensions(self, extensions)

 

# 這個類是咱們真正作事情的部分

class BuildExe:

    def __init__(self):

        #------------------------------------------------------#

        ##### 對於一個新的遊戲程序,須要修改這裏的各個參數 #####

        #------------------------------------------------------#

 

        # 起始py文件

        self.script = "MyGames.py"

        # 遊戲名

        self.project_name = "MyGames"

        # 遊戲site

        self.project_url = "about:none"

        # 遊戲版本

        self.project_version = "0.0"

        # 遊戲許可

        self.license = "MyGames License"

        # 遊戲做者

        self.author_name = "xishui"

        # 聯繫電郵

        self.author_email = "blog@eyehere.net"

        # 遊戲版權

        self.copyright = "Copyright (c) 3000 xishui."

        # 遊戲描述

        self.project_description = "MyGames Description"

        # 遊戲圖標(None的話使用pygame的默認圖標)

        self.icon_file = None

        # 額外須要拷貝的文件、文件夾(圖片,音頻等)

        self.extra_datas = []

        # 額外須要的python庫名

        self.extra_modules = []

        # 須要排除的python庫

        self.exclude_modules = []

        # 額外須要排除的dll

        self.exclude_dll = ['']

        # 須要加入的py文件

        self.extra_scripts = []

        # 打包Zip文件名(None的話,打包到exe文件中)

        self.zipfile_name = None

        # 生成文件夾

        self.dist_dir ='dist'

 

    def opj(self, *args):

        path = os.path.join(*args)

        return os.path.normpath(path)

 

    def find_data_files(self, srcdir, *wildcards, **kw):

        # 從源文件夾內獲取文件

        def walk_helper(arg, dirname, files):

            # 固然你使用其餘的版本控制工具什麼的,也能夠加進來

            if '.svn' in dirname:

                return

            names = []

            lst, wildcards = arg

            for wc in wildcards:

                wc_name = self.opj(dirname, wc)

                for f in files:

                    filename = self.opj(dirname, f)

 

                    if fnmatch.fnmatch(filename, wc_name) and not os.path.isdir(filename):

                        names.append(filename)

            if names:

                lst.append( (dirname, names ) )

 

        file_list = []

        recursive = kw.get('recursive', True)

        if recursive:

            os.path.walk(srcdir, walk_helper, (file_list, wildcards))

        else:

            walk_helper((file_list, wildcards),

                        srcdir,

                        [os.path.basename(f) for f in glob.glob(self.opj(srcdir, '*'))])

        return file_list

 

    def run(self):

        if os.path.isdir(self.dist_dir): # 刪除上次的生成結果

            shutil.rmtree(self.dist_dir)

 

        # 得到默認圖標

        if self.icon_file == None:

            path = os.path.split(pygame.__file__)[0]

            self.icon_file = os.path.join(path, 'pygame.ico')

 

        # 得到須要打包的數據文件

        extra_datas = []

        for data in self.extra_datas:

            if os.path.isdir(data):

                extra_datas.extend(self.find_data_files(data, '*'))

            else:

                extra_datas.append(('.', [data]))

 

        # 開始打包exe

        setup(

            cmdclass = {'py2exe': pygame2exe},

            version = self.project_version,

            description = self.project_description,

            name = self.project_name,

            url = self.project_url,

            author = self.author_name,

            author_email = self.author_email,

            license = self.license,

 

            # 默認生成窗口程序,若是須要生成終端程序(debug階段),使用:

            # console = [{

            windows = [{

                'script': self.script,

                'icon_resources': [(0, self.icon_file)],

                'copyright': self.copyright

            }],

            options = {'py2exe': {'optimize': 2, 'bundle_files': 1,

                                  'compressed': True,

                                  'excludes': self.exclude_modules,

                                  'packages': self.extra_modules,

                                  'dist_dir': self.dist_dir,

                                  'dll_excludes': self.exclude_dll,

                                  'includes': self.extra_scripts} },

            zipfile = self.zipfile_name,

            data_files = extra_datas,

            )

 

        if os.path.isdir('build'): # 清除build文件夾

            shutil.rmtree('build')

 

if __name__ == '__main__':

    if len(sys.argv) < 2:

        sys.argv.append('py2exe')

    BuildExe().run()

    raw_input("Finished! Press any key to exit.")

能夠先從簡單的程序開始,有了一點經驗再嘗試打包複雜的遊戲。
一些Tips:

  • 若是執行出錯,會生成一個xxx.exe.log,參考這裏的log信息看是否是少打包了東西。
  • 一開始能夠使用console來打包,這樣能夠在命令行裏看到更多的信息。
  • 對於每個遊戲,基本都須要拷貝上面的原始代碼修改成獨一無二的打包執行文件。
  • 即便一個很小的py文件,最終生成的exe文件也很大(看安裝的庫而定,我這裏最小4.7M左右),事實上py2exe在打包的時候會把無數的不須要的庫都打進來致使最終文件臃腫,若是你安裝了很繁雜的庫(wxPython等)更是如此。使用zip打包之後查看裏面的庫文件,把不須要的逐一加入到self.exclude_modules中,最後能夠把文件尺寸控制在一個能夠接受的範圍內。

「dist_dir」應該是屬於py2exe的特有options而不是setup的。用Python和Pygame寫遊戲-從入門到精通(Sprite篇)

 

pygame.sprite.Sprite是pygame精靈的基類,通常來講,你老是須要寫一個本身的精靈類繼承一下它而後加入本身的代碼。舉個例子:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

import cStringIO, base64

import pygame

from pygame.locals import *

 

class Ball(pygame.sprite.Sprite):

    def __init__(self, color, initial_position):

        pygame.sprite.Sprite.__init__(self)

        ball_file = cStringIO.StringIO(base64.decodestring(

"""iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ

bWFnZVJlYWR5ccllPAAABBJJREFUeNqsVj2PG1UUvfPp8XictXfHa+9mlyJCNEQRWiToqACJAgGC

LqJNlQZR0IFEj8RPSJkGGooUpEWJkGhR0tAAElI2tsfjjxnPjIdz7oyDF2wSUK72yN43793z7rkf

Y8N2HFmbbVliGIYiyzIpy1Isy3oHeMswzLOyXJ2tVit9VhTFAxz5Cfge+A7IZIcZmySObQudwIE0

veanraB1w/O8l5x6D9eXy6UkSaJYLBa6BvsNuAV8uY3sCQlvX4LANM0Xw/Dgdhj2Xm02m+K6LqPR

PXmeS5qmMp/PZTabyXQ6lclkosS1/QJcB+5vkthrAkoAuc4uHx//0B8MvCAIxG/5jEg0kpIkmcwX

icTxBIhlHWEURXoedgW4B3wIfHuBJM9yMQ3j5PTk5N7g6MjtdrrS3e9Ku90GUUvc2hkdMYJx5Ivn

NRC19UReRlRLR/sGeB34UUkMJBcJlcHg6K4SdDvS7/el1+tJp7MnQdCWRqMhDGWZLmWCCFog9rBm

GBYc50rOKON4uqkSC+IQSC3moeX7N09PX/i4AwLkAoQDxeFhHziU8CCUzt6e+EFLc2QaJi4mFQHy

kQLZMpME+WJF1sabdYA7Nq4jQbv9OZPs+75cgkSMYH9/X6PhJ9dpTLjruFLkBRyjACBd1BoLzzY8

T3O0IRntJvCZDXsTTnq262CzrzmgRHu4+QEIQhAxNzRWU1mTxfjOwvBIAOlIYNnWtja5bqM33mN/

sBEdx9bNPOQ1PWlqZJdAFKoMrEI6R+9gj6t7cUl1zjKnjFvsfaybr1Uqlv94ypXSKCud+aefpezs

7O3LL9s4c5U65gCrhGDDpUkqyWIuU1STweNlJRe7nAlmA+ZaVbnmiD4KFNEWC+3VqjB5YImDdMA+

YKONx2OVgxefojRL8CzmCxkOhxLhWYy+mGIvz6RKmv096X91PErP4Byazapbs3vZB45bVQqTzBzQ

kjQBQSTnjx7JcDTCRSLkKNY9SbKACsttHKZdrIqHILnGCNhoDU0qG83U5mNUVTOKShRPYo3m8fAc

nT/S/3mWFy2KrXKNOFbuI+Rr1FvLsB731Ho2m2pU7I1Sx8pSHTLaESIZjob6nfso2w77mSR3IMsN

zh4mmLOIBAkO6fjAgESdV1MYiV4kiUZHRDjD3E0Qza580D+rjsUdAQEj4fRl8wUkqBttPeo5RlJI

uB71jIASc8D+i4W8IoX8CviC5cuI+JlgpLsgcF1ng6RQyaoX1oWX1i67DTxe9w+9/EHW9VOrngCW

ZfNFpmvVWOfUzZ/mfG0HwHBz4ZV1kz8nvLuL+YPnRPDJ00J8A/j9fzrnW+sjeUbjbP8amDyj86z+

tXL5PwzOC4njj4K3gavA8cazczYacLd+p/+6y8mfAgwAsRuLfp/zVLMAAAAASUVORK5CYII="""))

        self.image = pygame.image.load(ball_file, 'file').convert_alpha()

        self.rect = self.image.fill(color, None, BLEND_ADD)

        self.rect.topleft = initial_position

 

pygame.init()

screen = pygame.display.set_mode([350, 350])

 

ball = Ball((255, 0, 0), (100, 100))

screen.blit(ball.image, ball.rect)

pygame.display.update()

while pygame.event.poll().type != KEYDOWN:

    pygame.time.delay(10)

那一大堆的字符串,相信懂Python的人會明白的,不明白的請去查閱一下base64編碼和Python對應的StringIO、base64庫。我這裏使用這種方法而不是直接讀取文件,只是想告訴你們pygame.image.load方法不只僅能夠讀取文件,也能夠讀取文件對象。是否是感受一會兒思路開闊了?Python那麼多方便的文件對象,之後遊戲的資源文件就能夠不用一一獨立放出來了,使用zipfile,咱們很容易就能夠把資源文件打包起來,這樣看起來咱的遊戲可就專業多了~這是後話,之後有機會再講。

而本例沒有直接畫一個圓,而是使用用了顏色混合的方法,這樣能夠畫出有立體感的球體,效果如左圖。而上面一大堆的字符串,其實就是那個球體的圖像文件編碼之後的東西。這個和本教程沒啥大聯繫,請自行學習光與色的知識……

可是可是,看了上面的代碼你們必定會有意見了,這樣感受比直接用Surface寫的代碼還多啊!一點好處都沒有的樣子。確實會有這樣的錯覺,可是一個球看不出好處來,多個球呢?咱們就能夠這麼寫了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

balls = []

for color, location in [([255, 0, 0], [50, 50]),

                        ([0, 255, 0], [100, 100]),

                        ([0, 0, 255], [150, 150])]:

    boxes.append(Box(color, location))

 

...

for b in balls: screen.blit(b.image, b.rect)

pygame.display.update()

 

# 咱們還能用一種更牛的重繪方式

# rectlist = [screen.blit(b.image, b.rect) for b in balls]

# pygame.display.update(rectlist)

# 這樣的好處是,pygame只會重繪有更改的部分

我就不給出完整代碼和效果圖了,請你們本身試驗。

不過光這樣還不足以體現sprite的好處,sprite最大的優點在於動畫,這裏就須要用一下update方法,舉一個例子,把第一個程序,從33行開始換成下面的代碼:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

class MoveBall(Ball):

    def __init__(self, color, initial_position, speed, border):

        super(MoveBall, self).__init__(color, initial_position)

        self.speed = speed

        self.border = border

        self.update_time = 0

 

    def update(self, current_time):

        if self.update_time < current_time:

            if self.rect.left < 0 or self.rect.left > self.border[0] - self.rect.w:

                self.speed[0] *= -1

            if self.rect.top < 0 or self.rect.top > self.border[1] - self.rect.h:

                self.speed[1] *= -1

 

            self.rect.x, self.rect.y = self.rect.x + self.speed[0], self.rect.y + self.speed[1]

            self.update_time = current_time + 10

 

pygame.init()

screen = pygame.display.set_mode([350, 350])

 

balls = []

for color, location, speed in [([255, 0, 0], [50, 50], [2,3]),

                        ([0, 255, 0], [100, 100], [3,2]),

                        ([0, 0, 255], [150, 150], [4,3])]:

    balls.append(MoveBall(color, location, speed, (350, 350)))

 

while True:

    if pygame.event.poll().type == QUIT: break

 

    screen.fill((0,0,0,))

    current_time = pygame.time.get_ticks()

    for b in balls:

        b.update(current_time)

        screen.blit(b.image, b.rect)

    pygame.display.update()

咱們能夠看到小球歡快的運動起來,碰到邊界就會彈回來,這纔是sprite類的真正用處。每個Sprite類都會有各自的速度屬性,每次調用update都會各自更新本身的位置,主循環只須要update+blit就能夠了,至於各個小球到底在一個怎樣的狀態,徹底能夠不在乎。不過精靈的魅力仍是不只在此,上面的代碼中咱們把每一個精靈加入一個列表,而後分別調用每一個精靈的update方法,太麻煩了!使用pygame.sprite.Group類吧,創建它的一個實例balls,而後用add方法把精靈加入,而後只須要調用balls.update(args..)就能夠了,連循環的不用寫。一樣的使用balls.draw()方法,你可讓pygame只重繪有變化的部分。請嘗試使用(記住還有一個balls.clear()方法,實際寫一下就知道這個方法用來幹嗎了)。

儘管咱們已經說了不少,也着實領略到了精靈的好處,但故事尚未結束,pygame.sprite有着層與碰撞的概念。層的引入是由於Group默認是沒有次序的,因此哪一個精靈覆蓋哪一個精靈徹底就不知道了,解決方法嘛,使用多個Group、使用OrderedUpdates,或者使用LayeredUpdates,至於具體使用方法,相信若是您須要用到的時候,已經到至關的高度了,隨便看看文檔就明白了,我就很少說了;而碰撞,又是一個無底洞啊,下次有機會再講吧~

用Python和Pygame寫遊戲-從入門到精通(實戰一:塗鴉畫板1)

功能樣式

作以前總要有個數,咱們的程序作出來會是個什麼樣子。所謂從頂到底或者從底到頂啥的,咱就不研究了,這個小程序隨你怎麼弄了,並且咱們主要是來熟悉pygame,高級的軟件設計方法一律不談~

由於是抄襲畫圖板,也就是鼠標按住了能在上面塗塗畫畫就是了,選區、放大鏡、滴管功能啥的就通通不要了。畫筆的話,基本的鉛筆畫筆老是要的,也能夠考慮加一個刷子畫筆,這樣有一點變化;而後顏色應該是要的,不然太過單調了,不過調色板啥的就暫時免了,提供幾個候選色就行了;而後橡皮……橡皮不就是白色的畫筆麼?免了免了!還有啥?彷佛夠了。。。 OK,開始吧!

框架

pygame程序的框架都是差很少的,考慮到咱們這個程序的實際做用,大概創建這樣的一個代碼架子就能夠了。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import pygame

from pygame.locals import *

 

class Brush():

    def __init__(self):

        pass

 

class Painter():

    def __init__(self):

        self.screen = pygame.display.set_mode((800, 600))

        pygame.display.set_caption("Painter")

        self.clock = pygame.time.Clock()

 

    def run(self):

        self.screen.fill((255, 255, 255))

        while True:

            # max fps limit

            self.clock.tick(30)

            for event in pygame.event.get():

                if event.type == QUIT:

                    return

                elif event.type == KEYDOWN:

                    pass

                elif event.type == MOUSEBUTTONDOWN:

                    pass

                elif event.type == MOUSEMOTION:

                    pass

                elif event.type == MOUSEBUTTONUP:

                    pass

 

            pygame.display.update()

 

if __name__ == '__main__':

    app = Painter()

    app.run()

這個很是簡單,準備好畫板類,畫筆類,暫時還都是空的,其實也就是作了一些pygame的初始化工做。若是這樣還不能讀懂的話,您須要把前面22篇從頭再看看,有幾句話不懂就看幾遍:)

這裏只有一點要注意一下,咱們把幀率控制在了30,沒有人但願在畫畫的時候,CPU風扇狂轉的。並且只是畫板,沒有自動運動的物體,純粹的交互驅動,咱們也不須要很高的刷新率。

第一次的繪圖代碼

按住鼠標而後在上面移動就畫東西,咱們很容易能夠想到這個流程:

 

1

2

3

按下左鍵  →  繪製flag開

移動鼠標  →  flag開的時候,在移動座標上留下痕跡

放開左鍵  →  繪製flag關

馬上試一試吧:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

 

    def start_draw(self):

        self.drawing = True

    def end_draw(self):

        self.drawing = False

 

    def draw(self, pos):

        if self.drawing:

            pygame.draw.circle(self.screen, self.color, pos, self.size)

 

class Painter():

    def __init__(self):

        #*#*#*#*#

        self.brush = Brush(self.screen)

 

    def run(self):

         #*#*#*#*#

                elif event.type == KEYDOWN:

                    # press esc to clear screen

                    if event.key == K_ESCAPE:

                        self.screen.fill((255, 255, 255))

                elif event.type == MOUSEBUTTONDOWN:

                    self.brush.start_draw()

                elif event.type == MOUSEMOTION:

                    self.brush.draw(event.pos)

                elif event.type == MOUSEBUTTONUP:

                    self.brush.end_draw()

框架中有的代碼我就不貼了,用#*#*#*#*#代替,最後會給出完整代碼的。

這裏主要是給Brush類增長了一些功能,也就是上面咱們提到的流程想對應的功能。留下痕跡,咱們是使用了在座標上畫圓的方法,這也是最容易想到的方法。這樣的效果好很差呢?咱們試一試:

哦,太糟糕了,再劣質的鉛筆也不會留下這樣斷斷續續的筆跡。上面是當咱們鼠標移動的快一些的時候,點之間的間距很大;下面是移動慢一些的時候,勉勉強強顯得比較連續。從這裏咱們也能夠看到pygame事件響應的頻度(這個距離和上面設置的最大幀率有關)。

怎麼辦?要修改幀率讓pygame平滑的反應麼?不,那樣作得不償失,換一個角度思考,若是有間隙,咱們讓pygame把這個間隙鏈接起來很差麼?

第二次的繪圖代碼

思路仍是很簡單,當移動的時候,Brush在上一次和這一次的點之間連一條線就行了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None     # <--

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos    # <--

    def end_draw(self):

        self.drawing = False

 

    def draw(self, pos):

        if self.drawing:

            pygame.draw.line(self.screen, self.color,

                    self.last_pos, pos, self.size * 2)

            self.last_pos = pos

在__init__和start_draw中各加了一句,用來存儲上一個點的位置,而後draw也由剛剛的話圓變成畫線,效果如何?咱們來試試。嗯,好多了,若是你動做能溫柔一些的話,線條已經很圓潤了,至少沒有斷斷續續的存在了。

知足了麼?我但願你的回答是「NO」,爲何,若是你劃線很快的話,你就能明顯看出棱角來,就好像左圖上半部分,仍是能看出是由幾個線段組合的。只有永不知足,咱們才能不停進步。

不過對咱們這個例程而言,差很少了,通常人在真正畫東西的時候,也不會動那麼快的:)

那麼這個就是咱們最終的繪圖機制了麼?回頭看看咱們的樣式,好用還須要加一個筆刷……所謂筆刷,不只僅是很粗,並且是由不少細小的毛組成,畫出來的線是給人一種一縷一縷的感受,用這個方法能夠實現麼?好像很是很是的困難。。。孜孜不倦的咱們再次進入了沉思……

這個時候,若是沒有頭緒,就得借鑑一下前輩的經驗了。看看人家是如何實現的?

 

Photoshop的筆刷尺寸改大,你會發現它會畫成這樣

若是你的Photoshop不錯,應該知道它裏面複雜的筆刷設定,而Photoshop畫出來的筆畫,並非真正一直線的,而是由無數細小的點組成的,這些點之間的間距是如此的密,以致於咱們誤會它是一直線……因此說,咱們還得回到第一種方法上,把它發揚光大一下~ 這沒有什麼很差意思的,放棄第二種方法並不意味着咱們是多麼的愚蠢,而是說明咱們從本身身上又學到了不少!

(公元前1800年)醫生:來,試試吃點兒這種草根,感謝偉大的部落守護神賜與咱們神藥!
(公元900年)醫生:別再吃那種草根,簡直是野蠻不開化不尊重上帝,這是一篇祈禱詞,天天虔誠地向上帝祈禱一次,不久就會治癒你的疾病。
(公元1650年)醫生:祈禱?!封建迷信!!!來,只要喝下這種藥水,什麼病都能治好!
(公元1960年)醫生:什麼藥水?早就不用了!別喝那騙人的」萬靈藥」,仍是這種藥片的療效快!
(公元1995年)醫生:哪一個庸醫給你開的處方?那種藥片吃半瓶也抵不上這一粒,來來來,試試科技新成果—抗生素
(公元2003年)醫生:據最新科學研究,抗生素反作用太強,畢竟是人造的東西呀……來,試試吃點兒這種草根!早在公元前1800年,文獻就有記載了。

返璞歸真,大抵如此了。

第三次的繪圖代碼

此次咱們考慮的更多,但願在點與點之間充滿咱們的筆畫,很天然的咱們就須要一個循環來作這樣的事情。咱們的筆畫有兩種,普通的實心和刷子,實心的話,用circle來畫也不失爲一個好主意;刷子的話,咱們可能須要一個刷子的圖案來填充了。

下面是咱們新的Brush類:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None

        self.space = 1

        # if style is True, normal solid brush

        # if style is False, png brush

        self.style = False

        # load brush style png

        self.brush = pygame.image.load("brush.png").convert_alpha()

        # set the current brush depends on size

        self.brush_now = self.brush.subsurface((0,0), (1, 1))

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos

    def end_draw(self):

        self.drawing = False

 

    def set_brush_style(self, style):

        print "* set brush style to", style

        self.style = style

    def get_brush_style(self):

        return self.style

 

    def set_size(self, size):

        if size < 0.5: size = 0.5

        elif size > 50: size = 50

        print "* set brush size to", size

        self.size = size

        self.brush_now = self.brush.subsurface((0,0), (size*2, size*2))

    def get_size(self):

        return self.size

 

    def draw(self, pos):

        if self.drawing:

            for p in self._get_points(pos):

                # draw eveypoint between them

                if self.style == False:

                    pygame.draw.circle(self.screen,

                            self.color, p, self.size)

                else:

                    self.screen.blit(self.brush_now, p)

 

            self.last_pos = pos

 

    def _get_points(self, pos):

        """ Get all points between last_point ~ now_point. """

        points = [ (self.last_pos[0], self.last_pos[1]) ]

        len_x = pos[0] - self.last_pos[0]

        len_y = pos[1] - self.last_pos[1]

        length = math.sqrt(len_x ** 2 + len_y ** 2)

        step_x = len_x / length

        step_y = len_y / length

        for i in xrange(int(length)):

            points.append(

                    (points[-1][0] + step_x, points[-1][1] + step_y))

        points = map(lambda x:(int(0.5+x[0]), int(0.5+x[1])), points)

        # return light-weight, uniq list

        return list(set(points))

咱們增長了幾個方法,_get_points()返回上一個點到如今點之間全部的點(這話聽着真彆扭),draw根據這些點填充。
同時咱們把get_size()、set_size()也加上了,用來設定當前筆刷的大小。
而變化最大的,則是set_style()和get_style(),咱們如今載入一個PNG圖片做爲筆刷的樣式,當style==True的時候,draw再也不使用circle填充,而是使用這個PNG樣式,固然,這個樣式大小也是應該可調的,全部咱們在set_size()中,會根據size大小實時的調整PNG筆刷。

固然,咱們得在主循環中調用set方法,才能讓這些東西工做起來~ 過一下子再講。再回顧下咱們的樣式,還有什麼?顏色……咱們立刻把顏色設置代碼也加進去吧,太簡單了!我這裏就先偷偷懶了~

控制代碼

到如今,咱們已經完成了繪圖部分的全部功能了。如今已經能夠在屏幕上自由發揮了,可是筆刷的顏色和大小好像不能改啊……咱們有這樣的接口你卻不調用,浪費了。

…… 真抱歉,我原想一次就把塗鴉畫板講完了,沒想到筆刷就講了這麼多,再把GUI說一下就文章就太長了,很差消化。因此仍是分紅兩部分吧,下一次咱們把GUI部分加上,就能完成對筆刷的控制了,咱們的第一個實戰也就宣告成功了!

用Python和Pygame寫遊戲-從入門到精通(實戰一:塗鴉畫板2)

By xishui | 2011/08/27

趁熱打鐵趕快把咱們這個畫板完成吧~

 

……鼠繪無能,不許笑!全部評論中噗嗤畫的好搓啊畫的好棒啊等,都會被我無情撲殺掉!可是能告訴我怎樣畫能夠更漂亮的話,絕對歡迎。

上次講Brush的時候,由於以爲太簡單把color設置跳過了,如今實際寫的時候才發現,由於咱們設置了顏色須要對刷子也有效,因此實際上set_color方法還有一點點收尾工做須要作:

Python

 

1

2

3

4

5

6

    def set_color(self, color):

        self.color = color

        for i in xrange(self.brush.get_width()):

            for j in xrange(self.brush.get_height()):

                self.brush.set_at((i, j),

                        color + (self.brush.get_at((i, j)).a,))

也就是在設定color的時候,順便把筆刷的顏色也改了,可是要保留原來的alpha值,其實也很簡單就是了……

按鈕菜單部分

上圖能夠看到,按鈕部分分別爲鉛筆、毛筆、尺寸大小、(當前樣式)、顏色選擇者幾個組成。咱們只以筆刷選擇爲例講解一下,其餘的都是相似的。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# 初始化部分

        self.sizes = [

                pygame.image.load("big.png").convert_alpha(),

                pygame.image.load("small.png").convert_alpha()

            ]

        self.sizes_rect = []

        for (i, img) in enumerate(self.sizes):

            rect = pygame.Rect(10 + i * 32, 138, 32, 32)

            self.sizes_rect.append(rect)

 

# 繪製部分

        for (i, img) in enumerate(self.pens):

            self.screen.blit(img, self.pens_rect[i].topleft)

 

# 點擊判斷部分

        for (i, rect) in enumerate(self.pens_rect):

            if rect.collidepoint(pos):

                self.brush.set_brush_style(bool(i))

                return True

這些代碼其實是我這個例子最想給你們說明的地方,按鈕式咱們從未接觸過的東西,然而遊戲中按鈕的應用我都沒必要說。

不過這代碼也都不困難,基本都是咱們學過的東西,只不過變換了一下組合而已,我稍微說明一下:

初始化部分:讀入圖標,並給每一個圖標一個Rect
繪製部分: 根據圖表的Rect繪製圖表
點擊判斷部分:根據點擊的位置,依靠「碰撞」來判斷這個按鈕是否被點擊,若點擊了,則作相應的操做(這裏是設置樣式)後返回True。這裏的collidepoint()是新內容,也就是Rect的「碰撞」函數,它接收一個座標,若是在Rect內部,就返回True,不然False。

好像也就如此,有了必定的知識積累後,新東西的學習也變得易如反掌了。

在這個代碼中,爲了明晰,我把各個按鈕按照功能都分紅了好幾組,在實際應用中按鈕數量不少的時候可能並不合適,請本身斟酌。

完整代碼

OK,這就結束了~ 下面把整個代碼貼出來。不過,我是一邊寫代碼一遍寫文章,思路不是很連貫,並且python也很久不用了……若是有哪裏寫的有問題(沒有就怪了),還請不吝指出!

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

import pygame

from pygame.locals import *

import math

 

# 2011/08/27 Version 1, first imported

 

class Brush():

    def __init__(self, screen):

        self.screen = screen

        self.color = (0, 0, 0)

        self.size  = 1

        self.drawing = False

        self.last_pos = None

        self.space = 1

        # if style is True, normal solid brush

        # if style is False, png brush

        self.style = False

        # load brush style png

        self.brush = pygame.image.load("brush.png").convert_alpha()

        # set the current brush depends on size

        self.brush_now = self.brush.subsurface((0,0), (1, 1))

 

    def start_draw(self, pos):

        self.drawing = True

        self.last_pos = pos

    def end_draw(self):

        self.drawing = False

 

    def set_brush_style(self, style):

        print "* set brush style to", style

        self.style = style

    def get_brush_style(self):

        return self.style

 

    def get_current_brush(self):

        return self.brush_now

 

    def set_size(self, size):

        if size < 0.5: size = 0.5

        elif size > 32: size = 32

        print "* set brush size to", size

        self.size = size

        self.brush_now = self.brush.subsurface((0,0), (size*2, size*2))

    def get_size(self):

        return self.size

 

    def set_color(self, color):

        self.color = color

        for i in xrange(self.brush.get_width()):

            for j in xrange(self.brush.get_height()):

                self.brush.set_at((i, j),

                        color + (self.brush.get_at((i, j)).a,))

    def get_color(self):

        return self.color

 

    def draw(self, pos):

        if self.drawing:

            for p in self._get_points(pos):

                # draw eveypoint between them

                if self.style == False:

                    pygame.draw.circle(self.screen, self.color, p, self.size)

                else:

                    self.screen.blit(self.brush_now, p)

 

            self.last_pos = pos

 

    def _get_points(self, pos):

        """ Get all points between last_point ~ now_point. """

        points = [ (self.last_pos[0], self.last_pos[1]) ]

        len_x = pos[0] - self.last_pos[0]

        len_y = pos[1] - self.last_pos[1]

        length = math.sqrt(len_x ** 2 + len_y ** 2)

        step_x = len_x / length

        step_y = len_y / length

        for i in xrange(int(length)):

            points.append(

                    (points[-1][0] + step_x, points[-1][1] + step_y))

        points = map(lambda x:(int(0.5+x[0]), int(0.5+x[1])), points)

        # return light-weight, uniq integer point list

        return list(set(points))

 

class Menu():

    def __init__(self, screen):

        self.screen = screen

        self.brush  = None

        self.colors = [

                (0xff, 0x00, 0xff), (0x80, 0x00, 0x80),

                (0x00, 0x00, 0xff), (0x00, 0x00, 0x80),

                (0x00, 0xff, 0xff), (0x00, 0x80, 0x80),

                (0x00, 0xff, 0x00), (0x00, 0x80, 0x00),

                (0xff, 0xff, 0x00), (0x80, 0x80, 0x00),

                (0xff, 0x00, 0x00), (0x80, 0x00, 0x00),

                (0xc0, 0xc0, 0xc0), (0xff, 0xff, 0xff),

                (0x00, 0x00, 0x00), (0x80, 0x80, 0x80),

            ]

        self.colors_rect = []

        for (i, rgb) in enumerate(self.colors):

            rect = pygame.Rect(10 + i % 2 * 32, 254 + i / 2 * 32, 32, 32)

            self.colors_rect.append(rect)

 

        self.pens = [

                pygame.image.load("pen1.png").convert_alpha(),

                pygame.image.load("pen2.png").convert_alpha()

            ]

        self.pens_rect = []

        for (i, img) in enumerate(self.pens):

            rect = pygame.Rect(10, 10 + i * 64, 64, 64)

            self.pens_rect.append(rect)

 

        self.sizes = [

                pygame.image.load("big.png").convert_alpha(),

                pygame.image.load("small.png").convert_alpha()

            ]

        self.sizes_rect = []

        for (i, img) in enumerate(self.sizes):

            rect = pygame.Rect(10 + i * 32, 138, 32, 32)

            self.sizes_rect.append(rect)

 

    def set_brush(self, brush):

        self.brush = brush

 

    def draw(self):

        # draw pen style button

        for (i, img) in enumerate(self.pens):

            self.screen.blit(img, self.pens_rect[i].topleft)

        # draw < > buttons

        for (i, img) in enumerate(self.sizes):

            self.screen.blit(img, self.sizes_rect[i].topleft)

        # draw current pen / color

        self.screen.fill((255, 255, 255), (10, 180, 64, 64))

        pygame.draw.rect(self.screen, (0, 0, 0), (10, 180, 64, 64), 1)

        size = self.brush.get_size()

        x = 10 + 32

        y = 180 + 32

        if self.brush.get_brush_style():

            x = x - size

            y = y - size

            self.screen.blit(self.brush.get_current_brush(), (x, y))

        else:

            pygame.draw.circle(self.screen,

                    self.brush.get_color(), (x, y), size)

        # draw colors panel

        for (i, rgb) in enumerate(self.colors):

            pygame.draw.rect(self.screen, rgb, self.colors_rect[i])

 

    def click_button(self, pos):

        # pen buttons

        for (i, rect) in enumerate(self.pens_rect):

            if rect.collidepoint(pos):

                self.brush.set_brush_style(bool(i))

                return True

        # size buttons

        for (i, rect) in enumerate(self.sizes_rect):

            if rect.collidepoint(pos):

                if i:   # i == 1, size down

                    self.brush.set_size(self.brush.get_size() - 0.5)

                else:

                    self.brush.set_size(self.brush.get_size() + 0.5)

                return True

        # color buttons

        for (i, rect) in enumerate(self.colors_rect):

            if rect.collidepoint(pos):

                self.brush.set_color(self.colors[i])

                return True

        return False

 

class Painter():

    def __init__(self):

        self.screen = pygame.display.set_mode((800, 600))

        pygame.display.set_caption("Painter")

        self.clock = pygame.time.Clock()

        self.brush = Brush(self.screen)

        self.menu  = Menu(self.screen)

        self.menu.set_brush(self.brush)

 

    def run(self):

        self.screen.fill((255, 255, 255))

        while True:

            # max fps limit

            self.clock.tick(30)

            for event in pygame.event.get():

                if event.type == QUIT:

                    return

                elif event.type == KEYDOWN:

                    # press esc to clear screen

                    if event.key == K_ESCAPE:

                        self.screen.fill((255, 255, 255))

                elif event.type == MOUSEBUTTONDOWN:

                    # <= 74, coarse judge here can save much time

                    if ((event.pos)[0] <= 74 and

                            self.menu.click_button(event.pos)):

                        # if not click on a functional button, do drawing

                        pass

                    else:

                        self.brush.start_draw(event.pos)

                elif event.type == MOUSEMOTION:

                    self.brush.draw(event.pos)

                elif event.type == MOUSEBUTTONUP:

                    self.brush.end_draw()

 

            self.menu.draw()

            pygame.display.update()

 

if __name__ == '__main__':

    app = Painter()

    app.run()

200行左右,註釋也不是不少,由於在這兩篇文章裏都講了,有哪裏不明白的請留言,我會根據實際狀況再改改。

用Python和Pygame寫遊戲-從入門到精通(實戰二:惡搞俄羅斯方塊1)

By xishui | 2011/09/02

遊戲是爲了什麼而存在的?Bingo,是爲了娛樂~ 在這個最高主題以前,技術啥的什麼都無所謂!

前一段時間,有位姓劉的網友用Pygame寫了個俄羅斯方塊,在用py2exe打包的時候遇到一些問題,和我交流了一下。有興趣的能夠在這裏下載,除了代碼,打包後的exe文件也一併提供了。

受他啓發,此次咱們就以俄羅斯方塊爲主題作一個遊戲吧,可是,咱不能走尋常路啊,得把它整的很是有趣才行。記得曾經在網上看到一個搞笑俄羅斯方塊,當時看了笑到肚子疼啊,時隔好久如今翻出來,同樣笑到脫力:

咱們就來作一個這樣的俄羅斯方塊吧:)作好了之後,給朋友玩玩,好好看看他(她,它?)的囧表情!

構架原理

構架這個詞太大了,其實就是草稿了~ 看過這個視頻,咱們能夠看到這個蛋疼的遊戲有幾種模式,把幾個可能用到咱們遊戲中的模式分別整理一下:

  1. 落下馬上消失
  2. 一屏幕長的長條
  3. 掉各類房間的方塊,掛了之後算房錢
  4. 長條的寬度稍稍寬於通常尺寸
  5. 落下奇怪的東西(豆莢,氣泡等)
  6. 長條會從底部消失
  7. 方塊很是很是小
  8. 同時快速落下三個方塊
  9. 落下超級瑪麗,碰到蘑菇長大掛掉
  10. 固然咱們至少得有一個正常的模式

很是的多,不過這個界面仍是都同樣的,不一樣的是咱們掉下的東西和消除判斷。咱們先把UI考慮一下,就用普通的俄羅斯方塊的界面就能夠了,像這樣:

 

雖然說至關不酷,不過各個部分一目瞭然,應該也能夠了。分別是遊戲顯示區,右邊則是下一個方塊的顯示,得分顯示區域,和一些功能按鈕。

接下來咱們考慮這個程序的運行機理,咱們先從最基本的狀況開始。在經典俄羅斯方塊運行的時候,不停的有隨機的方塊落下,用戶控制它們的轉向和位置落下,落下之後,若是穩固的方塊堆有哪一行是徹底填充的,就消除得分。

俄羅斯方塊的思想其實很是的簡單,人們熱衷於它,不得不說簡單又有有足夠變化的規則是主因,還有就是用戶受衆很大的關係……

右半部分的都很簡單,分別是下一個方塊,分數和一些功能按鈕,左半部分是和諧,這裏得不停的刷新,由於方塊無論有沒有操做都會緩慢落下直至徹底落地。而一旦落地,就須要看是否消除並刷新分數,同時落下接着的方塊,並顯示下一個方塊。

原理的進一步思考

俄羅斯方塊誕生於1985年,那時候尚未什麼成熟的「面向對象」的編程方法,因此俄羅斯方塊從一開始,界面就是以古樸的數組的方式運行的。

若是你有用其餘語言編寫俄羅斯方塊的經驗的話,就會知道大多數的實現方法都是維護一個二維數組(長度對應區域中的格子數,好比20×10。固然也能夠是一維的,由於知道寬度,因此轉換很容易),當數組某一位是1的時候,說明對應的位置有方塊,這個數組不停的更新,程序就把這個數組實時的畫到屏幕上,如此而已。

咱們再考慮的仔細一點,由於是使用pygame編寫,有沒有什麼更好的方法呢?若是咱們把每個方塊(這裏指四個小方塊組成的總體)當作一個Sprite,那麼就能夠很方便的繪製,可是Sprite老是方形的,作碰撞判斷就無效了,因此這樣不行。那若是把每個小方塊做爲一個Sprie,再把四個小方塊組成的放開作一個Group呢?聽起來不錯,可是再想一想也很是麻煩,咱們就得判斷碰撞的方向,要多作不少事情……考慮良久,感受仍是使用數組最靠譜啊,真惋惜!因此咱們也仍是用數組來作這事情吧。

實現初期的一些優化思考

儘管咱們仍然使用數組這種「古老」的方式,咱們仍是應該要利用一下pygame的繪圖優點,不然就太失敗了。舉個例子,通常的俄羅斯方塊,在重繪的時候把數組從頭至尾掃描一遍,碰到一個1就畫一個方塊,俄羅斯方塊的刷新率就算很低,一秒鐘也要畫了好屢次吧(不然後期速度快的時候畫面就「頓」了)。算它是15次,這樣一來,每秒鐘要掃描15次,須要畫幾百甚至上千次方塊,調用這麼屢次繪圖函數,仍是至關浪費資源的。

咱們能夠這麼考慮,除了數組以外,咱們還維護一個已經落下的方塊的圖形的Surface,這樣每次只須要把這個Surface貼到屏幕上就能夠了,很是的輕量級。而這個Surface的更新也只須要在新的方塊着地之後進行,徹底能夠在判斷消除的代碼裏一塊兒作,平均幾秒鐘纔會執行一次,大大減小了計算機的工做了。固然,這個簡單的程序裏,咱們這麼作也許並不能看到性能的提高,不過一直有這麼的思想,當咱們把工程越作越大的時候,和別人的差距也許就會體現出來了。

同時,出於人性化的思考,咱們是否是能夠提供用戶點擊其餘窗口的時候就把遊戲暫停了?實現起來並不困難,可是好感度的提高但是至關的大。

實現中的一些細節

按左向左,按右向右,這一點事毫無疑問的,不過當咱們按着向左向右不放的時候,就會持續移動,這一點也是要注意的,上面那位朋友實現的俄羅斯方塊就沒有考慮這一點,固然可能仍是中途的版本的關係,咱們這裏要考慮到。

由於咱們要實現幾種不一樣模式的俄羅斯方塊,那麼比較通常的考慮方法就是先實現一個通用的、標準的而定製能力又很強的遊戲類。當咱們之後去作其餘模式的時候,能夠很方便的從這個類擴展出來,因此一開始設計的時候,就要儘量多的考慮各類變種。

另外,考慮此次就徹底使用鍵盤來控制吧,鼠標就不要了,上面的概念圖,旁邊幾個按鈕請無視……

下一次開始,咱們就從基本的框架開始,慢慢地搭一個不一樣尋常的俄羅斯方塊出

用Python和Pygame寫遊戲-從入門到精通(實戰二:惡搞俄羅斯方塊2)

By xishui | 2011/09/10

咱們接着來作這個整死人不償命的俄羅斯方塊。

代碼組織和名詞約定

上一次咱們稍微整理了一下游戲運行的框架,這裏須要整理一下python代碼的框架,一個典型的pygame腳本結構以下:

其中,lib爲pygame的腳本,遊戲中聲音、圖像、控制模塊等都放在這裏;而data就是遊戲的資源文件,圖像、聲音等文件放在這裏。固然這東西並非硬性規定的,你能夠用你本身喜歡的結構來組織本身的pygame遊戲,事實上,除了付你工錢的那傢伙之外,沒有人能夠強迫你這樣作或那樣作~ 此次我仍是用這種典型的方法來存放各個文件,便於你們理解。

由於我是抽空在Linux和Windows上交叉編寫的,代碼中沒有中文註釋,遊戲裏也沒有中文的輸出,因此但願看到清楚解釋的話,仍是應該好好的看這幾篇文章。固然最後我會放出全部的代碼,也會用py2exe編譯一份exe出來,方便傳給不會用python的人娛樂。

由於主要是講解pygame,不少python相關的知識點就一代而過了,只稍微解釋一下可能有些疑問的,若是有看不懂的,請留言,我會酌情追加到文章中。

咱們約定,那個掉落方塊的區域叫board,落下的方塊叫shape,組成方塊的最小單位叫tile

咱們的lib中可能會有這幾個文件(未必是所有):

 

1

2

3

4

5

6

game.py    ---- 主循環,該文件會調用main,menu來展現不一樣界面

main.py    ---- 遊戲界面,但爲了實現不一樣模式,這裏需再次調用tetris

menu.py    ---- 菜單界面,開始的選擇等

shape.py   ---- 方塊的類

tetris.py  ---- 遊戲核心代碼,各類不一樣種類的俄羅斯方塊實現

util.py    ---- 各類工具函數,如讀取圖片等

引導文件

run_game.py很簡單,並且基本全部的pygame工程裏都長得同樣:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

#!/usr/bin/env python

 

import sys, os

 

try:

    libdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')

    sys.path.insert(0, libdir)

except:

    # in py2exe, __file__ is gone...

    pass

 

import game

game.run()

將lib目錄加入搜索路徑就完事了,沒有什麼值得特別說明的。

主循環文件

到如今爲止,咱們全部的pygame都只有一個界面,打開是什麼,到關閉也就那個樣子。但實際上的遊戲,通常進去就會有一個開始界面,那裏咱們能夠選「開始」,「繼續」,「選項」……等等內容。pygame中如何實現這個呢?

由於pygame一開始運行,就是一根筋的等事件並響應,因此咱們就須要在事件的循環中加入界面的判斷,而後針對的顯示界面。例如:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

    def loop(self):

        clock = pygame.time.Clock()

        while self.stat != 'quit':

            elapse = clock.tick(25)

            if self.stat == 'menu':

                self.stat = self.menu.run(elapse)

            elif self.stat == 'game':

                self.stat = self.main.run(elapse)

 

            if self.stat.startswith('level'):

                level = int(self.stat.split()[1])

                print "Start game at level", level

                self.main.start(level)

                self.stat = "game"

 

            pygame.display.update()

        pygame.quit()

由於有不少朋友說以前使用的while True的方法不能正常退出,這裏我就聽取你們的意見乾脆把退出也做爲一種狀態,兼容性可能會好一些。不過在個人機器上(Windows 7 64bit + Python 2.6 32bit + Pygame 1.9.1 32bit)是正常的,懷疑是否是沒法正常退出的朋友使用了64位的Python和Pygame(儘管64位Pygame也有,但並非官方推出的,不保證效果)。

這裏定義了幾個遊戲狀態,最主要的就是’menu‘和’game‘,意義也是一目瞭然的。咱們有遊戲對象和菜單對象,當遊戲處於某種狀態的時候,就調用對應對象的run方法,這個run接受一個時間參數,具體意義相信你們也明白了,基於時間的控制。

同時,咱們還有一個’level X‘的狀態,這個主要是控制菜單到遊戲之間的轉換,不過雖然寫的level,實際的意義是模式,由於咱們但願有幾種不一樣的遊戲模式,因此在從菜單到遊戲過渡的時候,須要這個信息。

這個程序中,全部的狀態都是經過字符串來實現的,說實話未必很好。雖然容易理解可是效率等可能不高,也許使用標誌變量會更好一些。不過既然是例子,首先天然是但願你們可以看的容易一些。因此最終仍是決定使用這個方法。

Menu類

菜單顯示了一些選項,而且在用戶調節的時候能夠顯示當前的選項(通常來講就是高亮出來),最後肯定時,改變狀態。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Menu:

    OPTS = ['LEVEL 1', 'LEVEL 2', 'LEVEL 3', 'QUIT']

    def __init__(self, screen):

        self.screen = screen

        self.current = 0

 

    def run(self, elapse):

        self.draw()

        for e in pygame.event.get():

            if e.type == QUIT:

                return 'quit'

            elif e.type == KEYDOWN:

                if e.key == K_UP:

                    self.current = (self.current - 1) % len(self.OPTS)

                elif e.key == K_DOWN:

                    self.current = (self.current + 1) % len(self.OPTS)

                elif e.key == K_RETURN:

                    return self.OPTS[self.current].lower()

        return 'menu'

菜單的話,大概就是長這個樣子,都是咱們已經熟練掌握的東西,按上下鍵的時候會修改當前的選項,而後draw的時候也就判斷一下顏色有些不一樣的標識一下就OK了。這裏的draw就是把幾個項目寫出來的函數。繪圖部分和控制部分儘可能分開,比較清晰,也容易修改。

這裏的run其實並無用到elapse參數,不過咱們仍是把它準備好了,首先能夠與main一致,其次若是咱們想在開始菜單里加一些小動畫什麼的,也比較便於擴展。

工具函數

工具庫util.py裏其實沒有什麼特別的,都是一些便於使用的小東西,好比說在加載資源文件是,咱們但願只給出一個文件名就能正確加載,那就須要一個返回路徑的函數,就像這樣:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

_ME_PATH = os.path.abspath(os.path.dirname(__file__))

DATA_PATH = os.path.normpath(os.path.join(_ME_PATH, '..', 'data'))

 

def file_path(filename=None):

    """ give a file(img, sound, font...) name, return full path name. """

    if filename is None:

        raise ValueError, 'must supply a filename'

 

    fileext = os.path.splitext(filename)[1]

    if fileext in ('.png', '.bmp', '.tga', '.jpg'):

        sub = 'image'

    elif fileext in ('.ogg', '.mp3', '.wav'):

        sub = 'sound'

    elif fileext in ('.ttf',):

        sub = 'font'

 

    file_path = os.path.join(DATA_PATH, sub, filename)

    print 'Will read', file_path

 

    if os.path.abspath(file_path):

        return file_path

    else:

        raise ValueError, "Cant open file `%s'." % file_path

這個函數能夠根據給定的文件名,本身搜索相應的路徑,最後返回全路徑以供加載。

此次把一些周邊的代碼說明了一下,固然僅有這些沒法構成一個能夠用的俄羅斯方塊,下一次咱們就要開始搭建俄羅斯方塊的遊戲代碼了

用Python和Pygame寫遊戲-從入門到精通(實戰二:惡搞俄羅斯方塊3)

By xishui | 2011/09/18

咱們講解了俄羅斯方塊的各個宏觀的部分,此次就是更細緻的編程了,不過代碼量實在不小,若是徹底貼出來估計會嚇退不少人,因此我打算這裏只貼出數據和方法名,至於方法裏的代碼就省略了,一切有興趣的朋友,請參考最後放出來的源文件。

這個是main調用的Tetris類,這個類實現了咱們所看到的遊戲畫面,是整個俄羅斯方塊遊戲的核心代碼。爲了明晰,它還會調用shape類來實現當前的shape,下面會講:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

class Tetris(object):

    W = 12          # board區域橫向多少個格子

    H = 20          # 縱向多少個格子

    TILEW = 20      # 每一個格子的高/寬的像素數

    START = (100, 20) # board在屏幕上的位置

    SPACE = 1000    # 方塊在多少毫秒內會落下(如今是level 1)

 

    def __init__(self, screen):

        pass

 

    def update(self, elapse):

        # 在遊戲階段,每次都會調用這個,用來接受輸入,更新畫面

        pass

 

    def move(self, u, d, l, r):

        # 控制當前方塊的狀態

        pass

 

    def check_line(self):

        # 判斷已經落下方塊的狀態,而後調用kill_line

        pass

 

    def kill_line(self, filled=[]):

        # 刪除填滿的行,須要播放個消除動畫

        pass

 

    def get_score(self, num):

        # 計算得分

       pass

 

    def add_to_board(self):

        # 將觸底的方塊加入到board數組中

       pass

 

    def create_board_image(self):

        # 創造出一個穩定方塊的圖像

        pass

 

    def next(self):

        # 產生下一個方塊

        pass

 

    def draw(self):

        # 把當前狀態畫出來

        pass

 

    def display_info(self):

        # 顯示各類信息(分數,等級等),調用下面的_display***

        pass

 

    def _display_score(self):

        pass

 

    def _display_next(self):

        pass

 

    def game_over(self):

        # 遊戲結束

        pass

這裏的東西基本都是和python語言自己相關的,pygame的內容並很少,因此就很少講了。看一下__init__的內容,瞭解告終構和數據,整個運做也就能明白了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

    def __init__(self, screen)

        self.stat = "game"

        self.WIDTH = self.TILEW * self.W

        self.HEIGHT = self.TILEW * self.H

        self.screen = screen

        # board數組,空則爲None

        self.board = []

        for i in xrange(self.H):

            line = [ None ] * self.W

            self.board.append(line)

        # 一些須要顯示的信息

        self.level = 1

        self.killed = 0

        self.score = 0

        # 多少毫秒後會落下,固然在init裏確定是不變的(level老是一)

        self.time = self.SPACE * 0.8 ** (self.level - 1)

        # 這個保存自從上一次落下後經歷的時間

        self.elapsed = 0

        # used for judge pressed firstly or for a  long time

        self.pressing = 0

        # 當前的shape

        self.shape = Shape(self.START,

                (self.WIDTH, self.HEIGHT), (self.W, self.H))

        # shape須要知道周圍世界的事情

        self.shape.set_board(self.board)

        # 這個是「世界」的「快照」

        self.board_image = pygame.Surface((self.WIDTH, self.HEIGHT))

        # 作一些初始化的繪製

        self.screen.blit(pygame.image.load(

            util.file_path("background.jpg")).convert(), (0, 0))

        self.display_info()

注意咱們這裏update方法的實現有些不一樣,並非等待一個事件就馬上相應。記得一開是說的左右移動的對應麼?按下去天然馬上移動,但若是按下了沒有釋放,那麼方塊就會持續移動,爲了實現這一點,咱們須要把event.get和get_pressed混合使用,代碼以下:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

    def update(self, elapse):

        for e in pygame.event.get():    # 這裏是普通的

            if e.type == KEYDOWN:

                self.pressing = 1           # 一按下,記錄「我按下了」,而後就移動

                self.move(e.key == K_UP, e.key == K_DOWN,

                        e.key == K_LEFT, e.key == K_RIGHT)

                if e.key == K_ESCAPE:

                    self.stat = 'menu'

            elif e.type == KEYUP and self.pressing:

                self.pressing = 0        # 若是釋放,就撤銷「我按下了」的狀態

            elif e.type == QUIT:

                self.stat = 'quit'

        if self.pressing:         # 即便沒有得到新的事件,也要根據「我是否按下」來查看

            pressed = pygame.key.get_pressed()    # 把按鍵狀態交給move

            self.move(pressed[K_UP], pressed[K_DOWN],

                    pressed[K_LEFT], pressed[K_RIGHT])

        self.elapsed += elapse    # 這裏是在指定時間後讓方塊自動落下

        if self.elapsed >= self.time:

            self.next()

            self.elapsed = self.elapsed - self.time

            self.draw()

        return self.stat

稍微看一下消除動畫的實現,效果就是若是哪一行填滿了,就在把那行刪除前閃兩下:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

    def kill_line(self, filled=[]):

        if len(filled) == 0:

            return

        # 動畫的遮罩

        mask = pygame.Surface((self.WIDTH, self.TILEW), SRCALPHA, 32)

        for i in xrange(5):

            if i % 2 == 0:

                # 比較透明

                mask.fill((255, 255, 255, 100))

            else:

                # 比較不透明

                mask.fill((255, 255, 255, 200))

            self.screen.blit(self.board_image, self.START)

            # 覆蓋在滿的行上面

            for line in filled:

                self.screen.blit(mask, (

                        self.START[0],

                        self.START[1] + line * self.TILEW))

                pygame.display.update()

            pygame.time.wait(80)

        # 這裏是使用刪除填滿的行再在頂部填空行的方式,比較簡單

        # 若是清空再讓方塊下落填充,就有些麻煩了

        [self.board.pop(l) for l in sorted(filled, reverse=True)]

        [self.board.insert(0, [None] * self.W) for l in filled]

        self.get_score(len(filled))

這個類自己沒有操縱shape的能力,第一塊代碼中move的部分,實際上是簡單的調用了self.shape的方法。而shape則響應當前的按鍵,作各類動做。同時,shape還有繪製自身和下一個圖像的能力。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

class Shape(object):

    # shape是畫在一個矩陣上面的

    # 由於咱們有不一樣的模式,因此矩陣的信息也要詳細給出

    SHAPEW = 4    # 這個是矩陣的寬度

    SHAPEH = 4    # 這個是高度

    SHAPES = (

        (   ((0,0,0,0),     #

             (0,1,1,0),     #   [][]

             (0,1,1,0),     #   [][]

             (0,0,0,0),),   #

        ),

        # 還有不少圖形,省略,具體請查看代碼

        ),

    )

    COLORS = ((0xcc, 0x66, 0x66), # 各個shape的顏色

        )

 

    def __init__(self, board_start, (board_width, board_height), (w, h)):

        self.start = board_start

        self.W, self.H = w, h           # board的橫、縱的tile數

        self.length = board_width / w   # 一個tille的長寬(正方形)

        self.x, self.y = 0, 0     # shape的起始位置

        self.index = 0          # 當前shape在SHAPES內的索引

        self.indexN = 0         # 下一個shape在SHAPES內的索引

        self.subindex = 0       # shape是在怎樣的一個朝向

        self.shapes = []        # 記錄當前shape可能的朝向

        self.color = ()

        self.shape = None

        # 這兩個Surface用來存放當前、下一個shape的圖像

        self.image = pygame.Surface(

                (self.length * self.SHAPEW, self.length * self.SHAPEH),

                SRCALPHA, 32)

        self.image_next = pygame.Surface(

                (self.length * self.SHAPEW, self.length * self.SHAPEH),

                SRCALPHA, 32)

        self.board = []         # 外界信息

        self.new()            # let's dance!

 

    def set_board(self, board):

        # 接受外界情況的數組

        pass

 

    def new(self):

        # 新產生一個方塊

        # 注意這裏實際上是新產生「下一個」方塊,而立刻要落下的方塊則

        # 從上一個「下一個」方塊那裏得到

        pass

 

    def rotate(self):

        # 翻轉

        pass

 

    def move(self, r, c):

        # 左右下方向的移動

 

    def check_legal(self, r=0, c=0):

        # 用在上面的move判斷中,「這樣的移動」是否合法(如是否越界)

        # 合法纔會實際的動做

        pass

 

    def at_bottom(self):

        # 是否已經不能再降低了

        pass

 

    def draw_current_shape(self):

        # 繪製當前shhape的圖像

        pass

    def draw_next_shape(self):

        # 繪製下一個shape的圖像

        pass

    def _draw_shape(self, surface, shape, color):

        # 上兩個方法的支援方法

        # 注意這裏的繪製是繪製到一個surface中方便下面的draw方法blit

        # 並非畫到屏幕上

        pass

 

    def draw(self, screen):

        # 更新shape到屏幕上

        pass

框架如上所示,一個Shape類主要是有移動旋轉和標識本身的能力,當用戶按下按鍵時,Tetris會把這些按鍵信息傳遞給Shape,而後它相應以後在返回到屏幕之上。

這樣的Tetris和Shape看起來有些複雜,不過想清楚了仍是能夠接受的,主要是由於咱們得提供多種模式,因此分割的細一些容易繼承和發展。好比說,咱們實現一種方塊一落下就消失的模式,之須要這樣作就能夠了:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class Tetris1(Tetris):

    """ 任何方塊一落下即消失的模式,只須要覆蓋check_line方法,

    不是返回一個填滿的行,而是返回全部有東西的行 """

    def check_line(self):

        self.add_to_board()

        filled = []

        for i in xrange(self.H-1, -1, -1):

            line = self.board[i]

            sum = 0

            for t in line:

                sum += 1 if t else 0

            if sum != 0:    # 這裏與通常的不一樣

                filled.append(i)

            else:

                break

        if i == 0 and sum !=0:

            self.game_over()

        self.create_board_image() # used for killing animation

        self.kill_line(filled)

        self.create_board_image() # used for update

此次全是代碼,不由讓人感到索然。

遊戲玩起來很開心,開發嘛,說白了就是艱難而持久的戰鬥(我的開發還要加上「孤獨」這個因素),代碼是絕對不可能缺乏的。因此你們也就寬容一點,網上找個遊戲玩了幾下感受不行就不要罵街了,多多鼓勵:)誰來作都不容易啊!

咱們此次基本就把代碼都實現了,下一次就有個完整的能夠動做的東西了

用Python和Pygame寫遊戲-從入門到精通(實戰二:惡搞俄羅斯方塊4)

By xishui | 2011/09/27

惡搞俄羅斯方塊的製造之旅也能夠結束了,經過上三次的說明,基本就整遍了整個代碼,雖然說都說了一些類名和方法名而沒有涉及到具體的實現,不過實現就是排列幾句代碼,你們必定沒問題吧:)

揉成一團

老是能夠把這幾回說的東西放在一塊兒運行了,界面的美化啥的我徹底沒有作,因此很難看,我們主要學習的東西是pygame,就不在這上面多花功夫了。

運行界面:

 

這個是第4種模式的截圖,會落下莫名其妙的東西的版本…… 落下個貓先生記念「夏目友人賬3」的完結。。。

各個模式說明:

  • 1: 落下的直接消失
  • 2: 落下長條
  • 3: 很是小的方塊
  • 4: 上圖所示,落下亂糟糟的東西(固然能夠隨便改)
  • 5: 暫時和6同樣,發揮交給大家了:)
  • 6: 正常模式

完成度說明:

直接進去菜單是沒有背景的,你很容易本身加一個……遊戲過程當中空格暫停,Esc返回菜單,返回菜單時直接覆寫在當前的遊戲畫面上。暫時懶得弄了,你們先湊合湊合。

資源說明:

圖片和音效是網上隨便找的,許可什麼的,我什麼不知道……
背景音樂史俄羅斯方塊之經典「永恆俄羅斯」的音樂中的Hawker’s song,理論上應該是有版權的,不過都已經20多年了,並且我們是學習,學習~ 不要太在乎了(笑)

 

總結

若是您作過遊戲,稍微看看這裏面的代碼必定會嗤之以鼻,看似有條不紊實際上可實在有些亂,各類界面的跳轉也是很讓人崩潰。無論圖像仍是運動,都是用最原始的東西組織起來的,維護起來簡直要命啊。

咱們須要「遊戲引擎」來讓咱們的遊戲書寫更加的漂亮。

爲何一開始不提供一個引擎?若是一開始就引入引擎的概念,會讓咱們對引擎的認識不深入。只有真正的用原始的代碼寫過一個遊戲後,咱們才能意識到「引擎」的做用和必要性。對咱們面對的東西有足夠深入的認識,才能讓咱們更卓越!

當時光看這幾篇文章的內容,是不夠理解的,只要把代碼通讀,而後完成一個新的模式(就是那個空着的模式5),纔能有足夠的認識(這個代碼寫的不夠漂亮,着重理解的是pygame,而不是整個代碼設計,我可不能誤人子弟啊)。

下一個遊戲會更加的精彩……

相關文章
相關標籤/搜索