用pixi.js製做拼圖遊戲

拼圖遊戲

這個教程面向已經能簡單使用pixi.js的開發者,經過建立一個拼圖遊戲,來演示怎麼完整的開發一款 pixi遊戲並最終發佈。

在此項目中你能夠學會怎麼使用ES6+開發,怎麼劃分模塊,怎麼提早加載資源,怎麼進行屏幕自適應,怎麼播放音頻視頻,怎麼分層,怎麼經過繼承pixi類來擴展功能,怎麼實現多國語言,怎麼用webpack進行開發期的調試以及最終怎麼構建發佈遊戲(webpack詳細教程可參考以前的文章《使用webpack搭建pixi.js開發環境》)。javascript

前言

在線體驗 html

demo.png

  • 下面將完整講解全部流程,詳細講解每個類,請結合源代碼一塊兒開始吧。

咱們開始吧

  • 配置環境java

    • 安裝nodejs
    • 選一個編輯器,推薦vscode
    • chrome瀏覽器。
    • 圖集製做工具texturepacker,免費版本便可。
  • 把項目 pixi-jigsaw clone下來。
  • 運行npm install安裝依賴庫。
  • 運行npm start啓動項目,會自動打開chrome瀏覽器並啓動遊戲,玩一把而後跟着下面的講解開始學習吧。

目錄及文件說明

  • res: 存放不須要放到遊戲裏面的源工程文件,例如texturepacker圖集項目,字體等等。
  • src: 全部的遊戲代碼和資源。
  • dist: 此目錄爲構建腳本動態生成,存放構建完成的項目文件,每次構建都會從新生成這個目錄。
  • webpack.common.js文件: webpack公共腳本。
  • webpack.dev.js文件: webpack開發配置腳本。
  • webpack.prod.js文件: webpack發佈配置腳本。
  • gulpfile.js文件:gulp構建腳本,用於發佈項目時候時候構建和優化整個項目。
  • package.json文件: node項目的配置文件。

源碼說明

  • 遊戲主頁 src/index.htmlnode

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>jigsaw puzzle</title>
      <style>
        html,
        body {
          width: 100%;
          height: 100%;
          padding: 0;
          margin: 0;
          overflow: hidden;
          background: transparent;
        }
        canvas {
          margin: 0;
          padding: 0;
          overflow: hidden;
          position: absolute;
        }
        .autofit {
          margin: 0;
          padding: 0;
          overflow: hidden;
          position: absolute;
          object-fit: cover;
        }
        .fullscreen {
          display: block;
          padding: 0;
          margin: auto;
          width: 100%;
          height: 100%;
          object-fit: cover;
          left: 0;
          top: 0;
          right: 0;
          bottom: 0;
          position: absolute;
        }
      </style>
    </head>
    <body>
      <canvas id="scene"></canvas>
      <script type="text/javascript" src="game.min.js" charset="utf-8"></script>
    </body>
    </html>
  • 配置文件 src/js/config.js,用於配置遊戲資源和常量。webpack

    //遊戲基本信息,遊戲名字,版本,寬,高等。
    export const meta = {
      name: 'jigsaw puzzle',
      version: '1.0.0',
      width: 796,
      height: 1280
    }
    
    //多國語言,根據瀏覽器語言自動加載相應的語言包資源。
    export const i18n = {
      'en': 'assets/i18n/en.json',
      'zh-cn': 'assets/i18n/zh-cn.json'
    }
    
    //遊戲視口顯示區域,不寫的話全屏顯示。
    export const viewRect = null
    
    //資源列表
    export const resources = [
      {
        name: 'main',
        url: 'assets/image/main.json'
      }, {
        name: 'sound_bg',
        url: 'assets/audio/bg.mp3'
      }, {
        name: 'sound_win',
        url: 'assets/audio/win.mp3'
      }, {
        name: 'sound_fail',
        url: 'assets/audio/fail.mp3'
      },      
      //若是圖片或者音頻視頻涉及多國語言,在這裏配置多國語言資源,程序會按需加載特定語言相關資源。
      {
        name: 'bg',
        i18n: {
          'en': 'assets/image/bg_en.png',
          'zh-cn': 'assets/image/bg_zh-cn.png',
        }
      }]
  • 多國語言模塊src/js/i18n.js,能讓程序根據瀏覽器語言自動調整程序界面語言。git

    import { parseQueryString } from './util'
    import mustache from 'mustache'
    
    export default class I18N {
      //i18n_config就是config.js裏面的i18n配置節
      constructor(i18n_config) {
        this.config = i18n_config
        this.words = {}
      }
      
      //維護一個鍵值對列表,i18n判斷完語言後,經過key查詢value。
      add(words) {
        Object.assign(this.words, words)
      }
    
      //判斷用戶語言若是querystring加 ?lang=zh-cn之類的,則按照這個現實,不然判斷瀏覽器語言。
      get language() {
        let lang = parseQueryString().lang
        let languages = Object.keys(this.config)
        if (lang && languages.indexOf(lang) !== -1) {
          return lang
        }
        lang = window.navigator.userLanguage || window.navigator.language
        if (lang && languages.indexOf(lang) !== -1) {
          return lang
        }
        return 'en'
      }
    
      //獲取當前語言的配置文件路徑,參考config.js i18n配置節
      get file() {
        let uri = this.config[this.language]
        return uri
      }
    
      //根據key獲取value
      //注意這裏用到了mustache模板
      //例如 get('hello {{ user }}', {user:'jack'}),返回'hello jack'。
      get(key, options) {
        let text = this.words[key]
        if (text) {
          if (options) {
            return mustache.render(text, options)
          }
          return text
        } else {
          console.warn('can not find key:' + key)
          return ''
        }
      }
    }
  • 音頻模塊src/js/sound.js,音頻模塊須要依賴pixi-sound庫實現功能。github

    import sound from 'pixi-sound'
    
    export default class Sound {
    
      //設置音量大小 volumn  0≤volume≤1
      setVolume(volume) {
        sound.volumeAll = Math.max(0,
          Math.min(1, parseFloat(volume))
        )
      }
    
      //播放音樂,name是音樂名字,config.js文件resources裏面音頻的name,loop是否循環播放。
      play(name, loop) {
        if (typeof loop !== 'boolean') {
          loop = false
        }
        let sound = app.res[name].sound
        sound.loop = loop
        return sound.play()
      }
    
      //中止播放name
      stop(name) {
        app.res[name].sound.stop()
      }
    
      //開啓關閉靜音
      toggleMuteAll() {
        sound.toggleMuteAll()
      }
    }
  • Application模塊src/js/app.js,此類繼承PIXI.Application,擴展了本身須要的功能,實現了自適應,資源加載,集成i18nsound模塊功能。web

    import * as PIXI from 'pixi.js'
    import Sound from './sound'
    import I18N from './i18n'
    import * as config from './config'
    import {
      throttle
    } from 'throttle-debounce'
    
    export default class Application extends PIXI.Application {
    
       // @param {jsonobject} options 和 PIXI.Application 構造函數須要的參數是同樣的
      constructor(options) {
    
        //禁用 PIXI ResizePlugin功能,防止pixi自動自適應.
        //pixi的自適應會修改canvas.width和canvas.height致使顯示錯誤,無法鋪滿寬或者高。
        options.resizeTo = undefined
    
        super(options)
        PIXI.utils.EventEmitter.call(this)
    
        //canvas顯示區域,若是設置了viewRect就顯示在viewRect矩形內,沒設置的話全屏顯示。
        this.viewRect = config.viewRect
    
        //防止調用過快發生抖動,throttle一下
        window.addEventListener('resize', throttle(300, () => {
          this.autoResize(this.viewRect)
        }))
        window.addEventListener('orientationchange', throttle(300, () => {
          this.autoResize(this.viewRect)
        }))
    
        //自適應
        this.autoResize(this.viewRect)
        
        //掛載模塊
        this.sound = new Sound()
        this.i18n = new I18N(config.i18n)
      }
    
      //自適應cavas大小和位置,按比例鋪滿寬或者高。
      autoResize() {
    
        let viewRect = Object.assign({
          x: 0,
          y: 0,
          width: window.innerWidth,
          height: window.innerHeight
        }, this.viewRect)
    
        //遊戲寬高比
        const defaultRatio = this.view.width / this.view.height
        
        //視口寬高比
        const windowRatio = viewRect.width / viewRect.height
    
        let width
        let height
    
        //這裏判斷根據寬適配仍是高適配
        if (windowRatio < defaultRatio) {
          width = viewRect.width
          height = viewRect.width / defaultRatio
        } else {
          height = viewRect.height
          width = viewRect.height * defaultRatio
        }
    
        //居中顯示canvas
        let x = viewRect.x + (viewRect.width - width) / 2
        let y = viewRect.y + (viewRect.height - height) / 2
    
        //讓canvas顯示在中心,高鋪滿的話,兩邊留黑邊,寬鋪滿的話,上下留黑邊
        this.view.style.left = `${x}px`
        this.view.style.top = `${y}px`
    
        //設置canvas的寬高,注意這裏千萬不要直接設置canvas.width和height。
        this.view.style.width = `${width}px`
        this.view.style.height = `${height}px`
    
        //若是有其餘須要自適應的dom一塊兒根據canvas自適應
        let autofitItems = document.querySelectorAll('.autofit')
        autofitItems.forEach(item => {
          item.style.left = `${x}px`
          item.style.top = `${y}px`
          item.style.width = `${width}px`
          item.style.height = `${height}px`
        })
      }
    
      //加載全部的資源
      load(baseUrl) {
    
        let loader = new PIXI.Loader(baseUrl)
        
        //爲了解決cdn緩存不更新問題,這裏獲取資源時候加個版本bust
        loader.defaultQueryString = `v=${config.meta.version}`
        
        //加載當前語言的配置文件
        loader.add(this.i18n.file)
    
        //加載全部遊戲資源
        config.resources.forEach(res => {
          if (res.i18n) {
            loader.add({
              name: res.name,
              url: res.i18n[this.i18n.language]
            })
          } else {
            loader.add(res)
          }
        })
    
        loader
          .on('start', () => {
            console.log('loader:start')
            this.emit('loader:start')
          })
          .on('progress', (loader, res) => {
            this.emit('loader:progress', parseInt(loader.progress))
          })
          .on('load', (loader, res) => {
            console.log(`loader:load ${res.url}`)
            // this.emit('load:res', res.url)
          })
          .on('error', (err, loader, res) => {
            console.warn(err)
            this.emit('loader:error', res.url)
          })
          .load((loader, res) => {
            console.log('loader:completed')
            app.res = res
            this.i18n.add(res[this.i18n.file].data)
            delete res[this.i18n.file]
            this.emit('loader:complete', res)
          })
    
        return loader
      }
    }
     //mixinEventEmitter
    Object.assign(Application.prototype,PIXI.utils.EventEmitter.prototype)
  • loading頁面src/js/loading.jschrome

    loading.png

    import {
      TextStyle,
      Container,
      Text,
      Graphics
    } from 'pixi.js'
    import {
      meta
    } from './config'
    
    //這是加載等待界面,菊花轉。可用於加載,網絡延遲時候顯示加載中。
    export default class Loading extends Container {
    
       //@param {object} options   
       //@param {boolean} options.progress 是否顯示加載進度文本
      constructor(options) {
        super()
    
        this.options = Object.assign({
          progress: true
        }, options)
    
        //一段弧的弧度
        let arcAngle = Math.PI * 0.2
    
        //弧之間的間距弧度
        let gapAngle = Math.PI * 0.05
    
        //pixi.js 裏面 graphics 從3點鐘方向開始爲0°,這裏爲了好看往回移動半個弧的距離。
        let offsetAngle = -arcAngle * 0.5
    
        //菊花的半徑
        let radius = 80
    
        //背景遮罩,一層灰色的遮罩,阻擋底層ui和操做
        let bg = new Graphics()
        bg.moveTo(0, 0)
        bg.beginFill(0x000000, 0.8)
        bg.drawRect(-meta.width / 2, -meta.height / 2, meta.width, meta.height)
        bg.interactive = true
        this.addChild(bg)
    
        //建立8個弧
        for (let i = 0; i < 8; i++) {
          let arc = new Graphics()
          arc.lineStyle(16, 0xffffff, 1, 0.5)
          let startAngle = offsetAngle + gapAngle * i + arcAngle * i
          let endAngle = startAngle + arcAngle
          arc.arc(0, 0, radius, startAngle, endAngle)
          this.addChild(arc)
        }
    
        //建立旋轉的弧,加載時候,有個弧會一直轉圈,順序的蓋在八個弧之上。
        let mask = new Graphics()
        this.addChild(mask)
        if (this.options.progress) {
          this.indicatorText = new Text('0%', new TextStyle({
            fontFamily: 'Arial',
            fontSize: 20,
            fill: '#ffffff',
          }))
          this.indicatorText.anchor.set(0.5)
          this.addChild(this.indicatorText)
        }
    
        //旋轉的弧當前轉到哪一個位置了,一共八個位置。
        let maskIndex = 0
    
        //啓動timer讓loading轉起來
        this.timer = setInterval(() => {
          mask.clear()
          mask.lineStyle(16, 0x000000, 0.5, 0.5)
          let startAngle = offsetAngle + gapAngle * maskIndex + arcAngle * maskIndex
          let endAngle = startAngle + arcAngle
          mask.arc(0, 0, radius, startAngle, endAngle)
          maskIndex = (maskIndex + 1) % 8
        }, 100)
      }
    
      //設置進度
      set progress(newValue) {
        if (this.options.progress) {
          this.indicatorText.text = `${newValue}%`
        }
      }
    
      destroy() {
        clearInterval(this.timer)
        super.destroy(true)
      }
    }
  • piece模塊,一張大拼圖中得一塊圖,src/js/piece.jsnpm

    piece.png

    import {
      Sprite,
      utils
    } from 'pixi.js'
    
    //一張大拼圖中得一塊圖,可拖拽。
    export default class Piece extends Sprite {
    
       // @param {texture} 塊顯示的圖片
       // @param {currentIndex} 塊當前的索引
       // @param {targetIndex} 塊的正確位置
       // 當塊的 targetIndex == currentIndex 說明塊在正確的位置了
       // piece 的索引(以3*3爲例)
       // 0  1  2
       // 3  4  5
       // 6  7  8
      constructor(texture, currentIndex, targetIndex) {
        super(texture)
        
        //mixin EventEmitter
        utils.EventEmitter.call(this)
    
        this.currentIndex = currentIndex
        this.targetIndex = targetIndex
    
        //讓塊相應觸摸事件
        this.interactive = true
    
        //監聽拖拽事件
        this
          .on('pointerdown', this._onDragStart)
          .on('pointermove', this._onDragMove)
          .on('pointerup', this._onDragEnd)
          .on('pointerupoutside', this._onDragEnd)
      }
    
      //開始拖拽
      _onDragStart(event) {
        this.dragging = true
        this.data = event.data
        
        //拖拽中得快設置成半透明
        this.alpha = 0.5
    
        //當前鼠標位置(相對於父節點的位置)
        let pointer_pos = this.data.getLocalPosition(this.parent)
    
        //鼠標點擊位置和piece位置的偏移量,用於移動計算,防止鼠標點擊後塊中心點瞬間偏移到鼠標位置。
        this.offset_x = pointer_pos.x - this.x
        this.offset_y = pointer_pos.y - this.y
    
        //塊原來的位置,用於交換兩個塊時候位置設置
        this.origin_x = this.x
        this.origin_y = this.y
         
        //發射拖拽開始事件
        this.emit('dragstart', this)
      }
    
      //拖拽移動中
      _onDragMove() {
        if (this.dragging) {
          const pos = this.data.getLocalPosition(this.parent)
          
          //根據鼠標位置,計算塊當前位置。
          this.x = pos.x - this.offset_x
          this.y = pos.y - this.offset_y
          this.emit('dragmove', this)
        }
      }
    
      //拖拽完成,鬆開鼠標或擡起手指
      _onDragEnd() {
        if (this.dragging) {
          this.dragging = false
          
          //恢復透明度
          this.alpha = 1
          this.data = null
          this.emit('dragend', this)
        }
      }
    
      //塊的中心點
      get center() {
        return {
          x: this.x + this.width / 2,
          y: this.y + this.height / 2
        }
      }
    }
    
    //mixin EventEmitter
    Object.assign(Piece.prototype, utils.EventEmitter.prototype)
  • 拼圖類src/js/jigsaw.js,控制拼圖邏輯。

    import {
      Texture,
      Container,
      Rectangle
    } from 'pixi.js'
    import Piece from './piece'
    
    //piece之間的空隙
    const GAP_SIZE = 2
    
    //拼圖類,控制拼圖邏輯,計算塊位置,檢查遊戲是否結束。
    export default class Jigsaw extends Container {
    
      //level難度,好比level=3,則拼圖切分紅3*3=9塊,可嘗試換成更大的值調高難度。
      //texture 拼圖用的大圖
      constructor(level, texture) {
        super()
        this.level = level
        this.texture = texture
    
        //移動步數
        this.moveCount = 0
    
        //全部塊所在的container(層級)
        this.$pieces = new Container()
        this.$pieces.y = 208
        this.$pieces.x = -4
        this.addChild(this.$pieces)
    
        //前景層,將拖拽中得塊置於此層,顯示在最前面
        this.$select = new Container()
        this.$select.y = 208
        this.$select.x = -4
        this.addChild(this.$select)
    
        this._createPieces()
      }
    
      //洗牌生成一個長度爲level*level的數組,裏面的數字是[0,level*leve)隨機值
      //例如level=3,返回[0,3,2,5,4,1,8,7,6]
      _shuffle() {
        let index = -1
        let length = this.level * this.level
        const lastIndex = length - 1
        const result = Array.from({
          length
        }, (v, i) => i)
        while (++index < length) {
          const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
          const value = result[rand]
          result[rand] = result[index]
          result[index] = value
        }
        return result
      }
    
      // 建立拼圖用的全部的塊(piece)
      _createPieces() {
      
        //每一個piece的寬和高
        this.piece_width = this.texture.orig.width / this.level
        this.piece_height = this.texture.orig.height / this.level
        
        //塊位置的偏移量,由於是以屏幕中心點計算的,全部全部塊向左偏移半張大圖的位置。
        let offset_x = this.texture.orig.width / 2
        let offset_y = this.texture.orig.height / 2
    
        let shuffled_index = this._shuffle()
    
        for (let ii = 0; ii < shuffled_index.length; ii++) {
    
          //從大圖中選一張小圖生成塊(piece),以level=3爲例,將大圖切成3*3=9塊圖
          // 0  1  2
          // 3  4  5
          // 6  7  8
          //而後根據shuffled_index從大圖上的位置取一個圖
          let row = parseInt(shuffled_index[ii] / this.level)
          let col = shuffled_index[ii] % this.level
          
          let frame = new Rectangle(col * this.piece_width, row * this.piece_height, this.piece_width, this.piece_height)
          
          //注意,這裏currentIndex=ii,targetIndex=shuffled_index[ii]
          let piece = new Piece(new Texture(this.texture, frame), ii, shuffled_index[ii])
          
          //將塊放在currentIndex所指示的位置位置
          let current_row = parseInt(ii / this.level)
          let current_col = ii % this.level
          piece.x = current_col * this.piece_width - offset_x + GAP_SIZE * current_col
          piece.y = current_row * this.piece_height - offset_y + GAP_SIZE * current_row
    
          piece
            .on('dragstart', (picked) => {
              //當前拖拽的塊顯示在最前
              this.$pieces.removeChild(picked)
              this.$select.addChild(picked)
            })
            .on('dragmove', (picked) => {
              //檢查當前拖拽的塊是否位於其餘塊之上
              this._checkHover(picked)
            })
            .on('dragend', (picked) => {
    
              //拖拽完畢時候恢復塊層級
              this.$select.removeChild(picked)
              this.$pieces.addChild(picked)
    
              //檢查是否有能夠交換的塊
              let target = this._checkHover(picked)
              if (target) {
                //有的話增長步數,交換兩個塊
                this.moveCount++
                this._swap(picked, target)
                target.tint = 0xFFFFFF
              } else {
                //沒有的話,迴歸原位
                picked.x = picked.origin_x
                picked.y = picked.origin_y
              }
            })
          this.$pieces.addChild(piece)
        }
      }
    
      // 交換兩個塊的位置
      // @param {*} 當前拖拽的塊
      // @param {*} 要交換的塊
      _swap(picked, target) {
      
        //互換指示當前位置的currentIndex和位置
        let pickedIndex = picked.currentIndex
        picked.x = target.x
        picked.y = target.y
        picked.currentIndex = target.currentIndex
    
        target.x = picked.origin_x
        target.y = picked.origin_y
        target.currentIndex = pickedIndex
      }
    
      //遊戲是否成功
      get success() {
    
        //全部的piece都在正確的位置
        let success = this.$pieces.children.every(piece => piece.currentIndex == piece.targetIndex)
    
        if (success) {
          console.log('success', this.moveCount)
        }
    
        return success
      }
    
      //當前的拖拽的塊是否懸浮在其餘塊之上
      _checkHover(picked) {
    
        let overlap = this.$pieces.children.find(piece => {
          //拖拽的塊中心點是否在其它塊矩形邊界內部
          let rect = new Rectangle(piece.x, piece.y, piece.width, piece.height)
          return rect.contains(picked.center.x, picked.center.y)
        })
    
        this.$pieces.children.forEach(piece => piece.tint = 0xFFFFFF)
    
        //改變底下塊的顏色,顯示塊可被交換
        if (overlap) {
          overlap.tint = 0x00ffff
        }
    
        return overlap
      }
    }
  • 結果頁src/js/result.js,這個頁面平淡無奇,惟一值得注意的是裏面用到了i18n用於根據當前語言調整ui顯示的語言,具體查看代碼app.i18n.get處。

    import {
      TextStyle,
      Container,
      Sprite,
      Text,
      Graphics
    } from 'pixi.js'
    import {
      meta
    } from './config'
    
    export default class Result extends Container {
      constructor() {
        super()
        this.visible = false
    
        let bg = new Graphics()
        bg.moveTo(0, 0)
        bg.beginFill(0x000000, 0.8)
        bg.drawRect(-meta.width / 2, -meta.height / 2, meta.width, meta.height)
        bg.interactive = true
        this.addChild(bg)
    
        //成功時候顯示
        this.$win = new Container()
        let win_icon = new Sprite(app.res.main.textures.win)
        win_icon.anchor.set(0.5)
        win_icon.y = -160
    
        this.$win.addChild(win_icon)
    
        let win_text = new Text(app.i18n.get('result.win', { prize: app.i18n.get('prize.win') }), new TextStyle({
          fontFamily: 'Arial',
          fontSize: 40,
          fontWeight: 'bold',
          fill: '#ffffff',
        }))
        win_text.anchor.set(0.5)
        this.$win.addChild(win_text)
    
        let win_button = new Sprite(app.res.main.textures.button_get)
        win_button.anchor.set(0.5)
        win_button.y = 80
        win_button.interactive = true
        win_button.buttonMode = true
        win_button.on('pointertap', () => {
          console.log('win')
          location.href = location.href.replace(/mobile(\d)/, 'mobile0')
        })
        this.$win.addChild(win_button)
    
        this.$fail = new Container()
    
        let fail_icon = new Sprite(app.res.main.textures.fail)
        fail_icon.y = -200
        fail_icon.anchor.set(0.5)
        fail_icon.interactive = true
        fail_icon.buttonMode = true
        fail_icon.on('pointertap', () => {
          console.log('fail')
          location.href = location.href.replace(/mobile(\d)/, 'mobile0')
        })
        this.$fail.addChild(fail_icon)
    
        //失敗時候顯示
        let fail_text = new Text(app.i18n.get('result.fail', { prize: app.i18n.get('prize.fail') }), new TextStyle({
          fontFamily: 'Arial',
          fontSize: 40,
          fontWeight: 'bold',
          fill: '#ffffff',
        }))
        fail_text.anchor.set(0.5)
        this.$fail.addChild(fail_text)
    
        this.addChild(this.$fail)
        this.addChild(this.$win)
      }
    
      //顯示成功
      win() {
        this.visible = true
        this.$win.visible = true
        this.$fail.visible = false
      }
    
      //顯示失敗
      fail() {
        this.visible = true
        this.$win.visible = false
        this.$fail.visible = true
      }
    }
  • 遊戲場景src/js/scene.js,這個類負責整個遊戲世界的顯示,控制遊戲的開始和結束。

    import {TextStyle,Container,Sprite,Text} from 'pixi.js'
    import Jigsaw from './jigsaw'
    import Result from './result'
    
    const STYLE_WHITE = new TextStyle({
      fontFamily: 'Arial',
      fontSize: 46,
      fontWeight: 'bold',
      fill: '#ffffff',
    })
    
    //遊戲時間顯示,30秒內沒完成,則遊戲失敗
    const TOTAL_TIME = 30 //second
    
    //倒計時
    let _countdown = TOTAL_TIME
    
    export default class Scene extends Container {
    
      constructor() {
        super()
    
        let bg = new Sprite(app.res.bg.texture)
        bg.anchor.set(0.5)
        this.addChild(bg)
    
        //提示圖
        let idol = new Sprite(app.res.main.textures.puzzle)
        idol.y = -198
        idol.x = -165
        idol.anchor.set(0.5)
        idol.scale.set(0.37)
        this.addChild(idol)
    
        //倒計時顯示
        this.$time = new Text(_countdown + '″', STYLE_WHITE)
        this.$time.anchor.set(0.5)
        this.$time.x = 170
        this.$time.y = -156
        this.addChild(this.$time)
    
        //拼圖模塊
        this.$jigsaw = new Jigsaw(3, app.res.main.textures.puzzle)
        this.addChild(this.$jigsaw)
      }
    
      //開始遊戲
      start() {
        
        //建立結果頁面
        let result = new Result()
        this.addChild(result)
    
        //播放背景音樂
        app.sound.play('sound_bg', true)
        
        //啓動倒計時timer,判斷遊戲成功仍是失敗。
        let timer = setInterval(() => {
          if (this.$jigsaw.success) {
            //成功後中止timer,中止背景音樂,播放勝利音樂,顯示勝利頁面。
            clearInterval(timer)
            app.sound.stop('sound_bg')
            app.sound.play('sound_win')
            result.win()
          } else {
            _countdown--
            this.$time.text = _countdown + '″'
            if (_countdown == 0) {
             //失敗後中止timer,中止背景音樂,播放失敗音樂,顯示失敗頁面。
              clearInterval(timer)
              app.sound.stop('sound_bg')
              app.sound.play('sound_fail')
              result.fail()
            }
          }
        }, 1000)
      }
    }
  • 遊戲入口類src/js/main.js

    import {
      Container
    } from 'pixi.js'
    import * as config from './config'
    import Application from './app'
    import Loading from './loading'
    import VideoAd from './ad'
    import Scene from './scene'
    import swal from 'sweetalert'
    
    //遊戲分層
    const layers = {
      back: new Container(),
      scene: new Container(),
      ui: new Container()
    }
    
    //啓動項目
    async function boot() {
    
      document.title = config.meta.name
    
      window.app = new Application({
        width: config.meta.width,
        height: config.meta.height,
        view: document.querySelector('#scene'),
        transparent: true
      })
    
      //把層加入場景內,並將層位置設置爲屏幕中心點.
      for (const key in layers) {
        let layer = layers[key]
        app.stage.addChild(layer)
        layer.x = config.meta.width / 2
        layer.y = config.meta.height / 2
      }
    }
    
    //預加載遊戲資源
    function loadRes() {
    
      let promise = new Promise((resolve, reject) => {
    
        //顯示loading進度頁面
        let loading = new Loading()
        layers.ui.addChild(loading)
        
        //根據application事件更新狀態
        app.on('loader:progress', progress => loading.progress = progress)
        app.on('loader:error', error => reject(error))
        app.on('loader:complete', () => {
          resolve()
          loading.destroy()
        })
        app.load()
      })
      return promise
    }
    
    //建立遊戲場景
    function setup() {
    
      let scene = new Scene()
      layers.scene.addChild(scene)
    
      //這裏註釋掉了播放視頻模塊,你能夠打開這部分,遊戲開始前將播放一個視頻,視頻播放完畢後纔會顯示遊戲。
      // let ad = new VideoAd()
      // layers.ui.addChild(ad)
      // ad.on('over', () => {
      scene.start()
      // })
    }
    
    window.onload = async () => {
      //啓動application
      boot()
      
      //加載資源,出錯的話就顯示錯誤提示
      try {
        await loadRes()
      } catch (error) {
        let reload = await swal({
          title: 'load resource failed',
          text: error,
          icon: 'error',
          button: 'reload'
        })
        if (reload) {
          location.reload(true)
        }
        return
      }
      
      //加載成功後顯示遊戲界面
      setup()
    }
  • 恭喜,至此遊戲代碼部分已經徹底講解完畢,給堅持下來的本身比個心,ღ( ´・ᴗ・` )。

項目構建

  • 運行npm run build可發佈項目,最終全部文件會拷貝到dist目錄下,會合並全部的js文件並混淆和去除無用引用,優化圖片資源。
相關文章
相關標籤/搜索