消除麻將
根據遊戲規則,兩張相同圖案的麻將,如果互相之間沒有其他麻將牌被直線阻隔(中間的距離可以無限),可以通過先後點擊選擇這兩張麻將,消除這兩張牌。
要實現以上功能,需要分步完成以下幾個能力:
- 要能實現“先後選中”的能力,因此要對鼠標點擊的操作做出響應。
- 需要能控制顯示、消失圖像,用以表現“選中”麻將,以及顯示“消除”的效果。
- 通過“桌子”的內置數據結構,對麻將牌的位置是否成直線、兩個選中的麻將判斷是否有阻隔。
選中麻將
對於麻將類 Mahjong 的 update() 方法,增加對於用戶輸入事件的檢測和處理,就能完成“選中麻將”的功能:
第一篇介紹的 Director 類,會在每一幀,都通過 pygame 把所有的用戶輸入事件,存放到 Director.events 屬性中,所以每個 Sprite 的子類對象,都可以在 update() 函數中去檢測判斷:用戶有什麼輸入。
由於 mahjong.MainScenario 類,在 start() 方法中,構建 Table 這個 Group 的子類對象時,傳入了 director 參數
table = Table(self.director)
因此每個 Mahjong 對象,都可以通過其 table 屬性獲得 director 對象,從而獲得每幀更新的用戶事件(們):self.table.director.events
桌上所有的 Mahjong 對象,由於存放在 Table 這個 Group 裡面,所以每幀其 update() 都會被調用。也就是說,每幀、每個麻將對象,都可以在 update() 裡檢測一遍:“我”有沒有被鼠標點中。用戶在此刻的所有操作,會被 pygame 放入 events 列表,需要我們通過循環迭代語句,獲取其中的每個事件。
通過 event.type 屬性,判斷 pygame.MOUSEBUTTONDOWN 就可以知道是否有鼠標按鈕按下的事件;隨後可以通過 pygame.mouse.get_pos() 可以獲得鼠標當前的位置;最後通過 Sprite.rect.collidepoint(pos) 可以判斷當前 Sprite 對象是否有“碰撞”到某個 pos 點位置。當前的 Sprite 就是麻將對象,所以我們就判斷鼠標是否“點擊”到了當前的麻將。
顯示選中特效
對於選中的麻將,我們希望是:
- 如果第一次選中麻將,在被選中的麻將上顯示一個“框框”
- 被選中的麻將,需要以某個方式記錄其座標
- 如果已經有一個麻將被選中,選中第二個麻將後,“框框”消失
所有需要控制顯示的對象,都繼承 Sprite 實現一個類,通過構造器來實現加載某個圖像數據。此對象的 image/rect 屬性通過加載一個圖片作為框框顯示,這個圖片需要是中間透明的,所以使用的 png 格式。
我們可以建立一個類 Edge,用來顯示“選中框”。此類有的 pos 屬性是一個數組,記錄選中的麻將牌的桌上座標。
Table 類添加一個屬性 edge,持有此 Edge 對象;另外一個屬性 is_show_edge 記錄框框是否已經顯示。
在每幀都調用的 Table.show() 方法裡面,根據 is_show_edge 屬性,來決定是否 add(self.edge),就可以實現根據 is_show_edge 來顯示/消失這個框框。
由於每次顯示 Edge 對象的位置不一樣,所以在 Table 上增加了一個 show_edge() 方法,用來修改 Table.edge 的位置:
其中 loc 參數表示被選中麻將的座標,會記錄到 Edge.pos 上,同時根據此座標計算並修改 edge.rect 的位置,並且對 is_show_edge 賦值為 True;當點擊事件觸發“點擊第二張牌”的時候,此屬性會被置為 False。
選中第二個牌的處理
點擊第二張牌後,需要判斷是否可以消除,代碼在 Mahjong.update():
由於 Table.show_edge() 會在 table.edge.pos 記錄被選中的第一張麻將的座標,所以第二張麻將被選中的時候,可以通過這個座標(i,j)從 table.heap 這個二維數組獲得被選中的麻將。
下面就是幾個情況,判斷是否可以消除,具體判斷:
- 兩個牌直接是否有阻隔
- 被選中的牌不能是空
- 兩張牌的圖案是一樣的
- 不能選中兩次是同一張牌
如果可以消除,通過對 heap[x][y] 的值賦值 None 就表示了消除。在 Table.show() 裡面,會跳過為 None 的 heap 成員,因此就可以作為消除牌的功能實現。
如果不能消除,這裡調用了一個 Table.show_text() 方法,用於顯示提示文字,後續會介紹如何顯示。
顯示爆炸效果
在上述邏輯中,通過了以下代碼實現“顯示”爆炸效果:
self.bomb.show(self.rect.left,self.rect.top)selected.bomb.show(selected.rect.left,selected.rect.top)
由於在 MainSenario 的 start() 方法中,為每個麻將對象,都添加了一個爆炸對象屬性 Mahjong.bomb,所以被選中的兩個麻將對象,都可以調用 self.bomb.show() 這個方法,傳入了需要顯示的座標。一旦調用這個方法,Bomb 類就會自己通過 Bomb.update() 方法,顯示一段時間“爆炸”的圖片。如果想內存佔用的小一點,也可以在 MainSenario.start() 方法中只構造兩個 Bomb 對象,然後在需要爆炸的時候,再顯示到對應的位置。
具體的方法是:
- 修改自己的顯示位置,把自己 add 到“特效層”的 effect 組裡
- 設置一個倒計時屬性 counter,需要顯示多少幀時間,就設置為多少,這裡是 30,也就是一秒,因為 director.fps 設置了 30
- 通過 update() 方法,每幀對 counter 減一,如果為 0,則從 effect 組裡去掉(通過 Group.remove(Sprite) 方法),從而消失。由於 effect 組並不會每幀都清空所有成員,和 table 組不一樣,所以不需要每次 update() 都去 add() 一次自己
顯示文字提示
文字提示,實際上也是一種 Sprite 對象,也需要對 image/rect 進行賦值,和上面的圖像不同的是,文字的 image 需要通過選擇字體和文字內容進行繪製。如果要顯示一段文字在遊戲畫面上,只需要:
從上面的代碼可以看出,我們可以選擇文字的字體、顏色,還可以選擇和其他內容共同“畫”在一個圖形上。
由於本遊戲只需要在一個地方顯示文字,而且字體只需要一種,所以在 Table 對象的屬性中構造好字體對象 font、顯示文字對象這兩個對象 text_sprite。另外,這個提示文字需要自動消失,所以還需要兩個屬性來記錄文字顯示了幾秒 show_text_time,以及何時開始 start_ticks。這個自動消失的功能和上面的爆炸特效功能類似,但是這裡使用了不同方法,純粹為了學習。
然後寫一個 show_text() 的方法,用來在桌上顯示文字:
這裡需要注意的是 self.show_text_time = time 這句,是記錄了當前文字要顯示多少秒,這個值會在 update() 中逐漸減少,用以讓文字自動消失。下面是 Table.show() 的代碼段:
這裡可以看到,每次 update() 調用 show(),然後都會判斷一下 show_text_time,用以決定是否要顯示文字提示。由於 self.start_ticks 記錄了啟動顯示的時間,所以根據 pygame.time.get_ticks() 返回的當前時間(毫秒數),就能知道已經顯示了多久。顯示和消失也是用 add() 和 remove() 控制。由於 Table.show() 的第一行是 self.empty(),會清空所有在 table 這個 Group 裡的 Sprite,所以下面要顯示的內容,都必須要調用 self.add()。
從上面的代碼可以看到,遊戲程序的所有“動態能力”,基本實現思想都是:
- 每個遊戲對象在構造器或者初始化函數中,構建好所需的各種對象
- 通過每幀調用 update() 函數進行“驅動”
- 在每幀的時刻,進行用戶操作檢測
- 在每幀的時刻,計算出當前幀遊戲的內部邏輯的狀態
- 根據當前幀的狀態,控制在屏幕上合適的位置,實現顯示、消失
因此,遊戲系統的動畫,也大多數是如此實現,是通過一幀幀的邏輯,來決定如何顯示下一個畫面,從而形成一個動畫。由於 udpate() 函數每幀都要調用,所以儘量減少在這個函數中構建新的對象,或者進行特別慢的操作如等待加載磁盤文件、等待網絡響應等。因為如果 update() 特別慢,整個遊戲的運行就會感覺特別卡。
下一篇介紹如何實現麻將的移動動畫,以及複雜的遊戲邏輯判斷。