天天看點

02.07 使用建造者模式(Builder Pattern)重新建構遊戲頁面

這是《小遊戲從0到1設計模式重構》系列内容第9篇,所有源碼及資料在“程式員LIYI”公衆号回複“小遊戲從0到1”擷取。

建造者模式是将一類複雜産品的建造過程,以一定的順序分解成很多子步驟。每個具體産品的建造,會遵循同樣的流程,但因為每一個步驟的具體實作不盡相同,是以建構出的産品呈現出不同的表象和行為。

在我們目前的小遊戲項目中,最有可能應用建造者模式的是頁面對象。兩個頁面GameOverPage和IndexPage它們擁有的頁面元素不同,正好适用建造者模式統一流程下的不同子步驟進行建構。

建造者模式主要有四個部分:

  • product産品類
  • Builder建造者類
  • Director指揮者類
  • 消費代碼。

在這一小節中,産品類是頁面對象,已經有了,不需要再建立;需要建立的是Builder建造者類和Director建造指揮者類。

我們将遊戲頁面的建立,分為前景、中景和背景三個部分,這是我們建立頁面的子步驟。首先我們建立一個建造者基類PageBuilder,這個基類是作為接下來兩個具體的子類的父類而存在的,在這個基類裡面,我們要定義建立頁面對象的基本步驟。我們看一下它的代碼:

// page/page_builder.js
class PageBuilder{
  page 
  constructor(page){
    this.page = page 
  }
  // 建立背景
  buildBackground(){}
  // 建立中景
  buildGameElements(){}
  // 建構前景
  buildForeground(){}
  
  // 建立背景對象
  buildBgObject(){
    this.page.bg = require("./background.js")
    this.page.addElement(this.page.bg)
  }
  // 建立音頻管理者對象
  buildAudioManager(){
    // 音頻管理者
    this.page.audioManager = require("../manager/audio_manager.js")
    // 初始化音效管理者
    this.page.audioManager.init({
      musicButtonOnImageUrl: "http://xiuxing-1252822131.cossh.myqcloud.com/book/sound-on.jpg",
      musicButtonOffImageUrl: "http://xiuxing-1252822131.cossh.myqcloud.com/book/sound-off.jpg",
      bgAudioUrl: "https://opengameart.org/sites/default/files/audio_preview/01%20track%201.ogg.mp3"
    })
    this.page.addElement(this.page.audioManager)
  }
  // 傳回産品對象
  getPage(){
    return this.page
  }
}
export default PageBuilder           

複制

在這個建造者類中,我們有三個基本的建構方法:buildBackground負責建構背景、buildGameElements負責建構中景遊戲元素、buildForeground負責建構前景。但這三個方法隻是“虛”方法,具體的實作要在子類中完成。

為了讓子類友善複用代碼,我們可以将有兩個子頁面都調用的代碼,抽離為特殊的對象建構方法,放在父類中。例如,buildAudioManager負責建構音頻管理者對象,buildBgObject這個方法負責建構背景對象。這些方法的具體建立代碼,以前已經寫過了,我們隻是把它們從一個地方拷貝到另一個地方。

接下來我們建立一個具體的建造者類IndexPageBuilder,負責建構遊戲首頁:

// page/index_page_builder.js
import PageBuilder from './page_builder'

class IndexPageBuilder extends PageBuilder{
  // 建立背景
  buildBackground(){
    this.buildBgObject()
  }
  // 建立中景
  buildGameElements(){
    // 球
    this.page.ball = require("./ball")
    // 球的初始化
    this.page.ball.init({})
    this.page.addElement(this.page.ball)

    // 擋闆對象
    this.page.leftPanel = require("./left_panel.js")
    this.page.rightPanel = require("./right_panel.js")
    // 擋闆初始化
    this.page.leftPanel.init({})
    this.page.rightPanel.init({})
    this.page.addElement(this.page.leftPanel)
    this.page.addElement(this.page.rightPanel)

    // 記分闆對象
    this.page.userBoard = require("./user_board.js")
    this.page.systemBoard = require("./system_board.js")
    // 檢查使用者授權情況,拉取使用者頭像并準備繪制
    this.page.userBoard.init({})
    this.page.addElement(this.page.userBoard)
    this.page.addElement(this.page.systemBoard)
  }
  // 建構前景
  buildForeground(){
    this.buildAudioManager()
  }
}
export default IndexPageBuilder           

複制

在這個具體的子類中,重寫了父類中三個基本的“虛”方法。

接下來再建立一個遊戲結束頁面建造者類GameOverPageBuilder:

// page/game_over_page_builder.js
import PageBuilder from './page_builder'

class GameOverPageBuilder extends PageBuilder{
  // 建構前景
  buildForeground(){
    this.buildAudioManager()
  }
}
export default GameOverPageBuilder           

複制

遊戲結束頁面不像遊戲首頁那樣複雜,隻重寫一個建構前景的“虛”方法就可以了。

最後出場的是建造指揮者類PageBuildDirector:

// page/page_build_director.js
import IndexPage from './index_page'
import GameOverPage from './game_over_page'
import IndexPageBuilder from './index_page_builder'
import GameOverPageBuilder from './game_over_page_builder'

class PageBuildDirector{
  // 建構頁面
  static buildPage(pageName){
    let page,builder
    switch (pageName) {
      case 'index':
        page = new IndexPage()
        builder = new IndexPageBuilder(page)
        break;
      case 'gameOver':
      default:
        page = new GameOverPage()
        builder = new GameOverPageBuilder(page)
        break;
    }
    builder.buildBackground()
    builder.buildGameElements()
    builder.buildForeground()

    return builder.getPage()
  }
}
export default PageBuildDirector           

複制

在面向對象程式設計中,對象不一定要被執行個體化,有時候使用靜态方法就可以達到目的。在這個指揮者類中,隻有一個靜态方法,在這個靜态方法中有兩個case,一個負責建造首頁,一個負責建造遊戲結束頁。無論是建構哪個頁面,它們的建造順序和建造方法是一緻的。

接下來就是修改game.js代碼,開始使用已經完成的建造者模式:

// game.js
...
import PageBuildDirector from './page/page_build_director'
...
class Game extends Event {
  ...
  constructor() { 
    ...
    this.gameOverPage = PageBuildDirector.buildPage("gameOver") // 遊戲結束頁面
    this.indexPage = PageBuildDirector.buildPage("index") // 首頁
  }
  ...
}           

複制

遊戲的運作效果與之前一緻:

02.07 使用建造者模式(Builder Pattern)重建立構遊戲頁面

最後總結一下,本小節應用了建造者模式,我們使用了兩個頁面建構類IndexPageBuilder和GameOverPageBuilder,分别完成遊戲首頁和遊戲結束頁面的建構。每個頁面含有的頁面元素不同,具體的建構過程也不盡相同,但擁有相同的”先背景、後中景、再前景“這樣一個建立順序,我們将這個建立順序通過建造者模式固定下來。

因為我們的遊戲很簡單,頁面也很簡單,難以彰顯建造者模式的強大;在一個擁有很有複雜對象的軟體系統中,建造者模式可以讓對象的建立變得簡單清晰的作用才會完全顯露出來。

階段源碼

本小節階段源碼見:https://github.com/rixingyike/wegame01/tree/5.2.1

我講明白沒有,歡迎留言讨論。

2021年02月6日