13

拼图游戏

教程面向已经能简单使用pixi.js的开发者,通过创建一个拼图游戏,来演示怎么完整的开发一款pixi游戏并最终发布。
此教程也被pixi.js官方收录为教程 How to make jigsaw game

此项目中你可以学会怎么使用ES6+开发,怎么划分模块,怎么提前加载资源,怎么进行屏幕自适应,怎么播放音频视频,怎么分层,怎么通过继承pixi类来扩展功能,怎么实现多国语言,怎么用webpack进行开发期的调试以及最终怎么构建发布游戏(webpack详细教程可参考之前的文章《使用webpack搭建pixi.js开发环境》)。

欢迎关注专栏 pixijs游戏开发

前言

在线体验

demo.png

  • 下面将完整讲解所有流程,详细讲解每一个类,请结合源代码一起开始吧。

我们开始吧

  • 配置环境

    • 安装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发布配置脚本。
  • package.json文件: node项目的配置文件。

源码说明

  • 游戏主页 src/index.html

    <!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;
        }
        .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" class="autofit"></canvas>
      <!--这里不需要引入js,webpack会自动帮我们引入-->
    </body>
    </html>
  • 配置文件 src/js/config.js,用于配置游戏资源和常量。

    //游戏基本信息,游戏名字,版本,宽,高等。
    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,能让程序根据浏览器语言自动调整程序界面语言。

    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库实现功能。

    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模块功能。

    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
        //让canvas显示在中心,高铺满的话,两边留黑边,宽铺满的话,上下留黑边
        let x = viewRect.x + (viewRect.width - width) / 2
        let y = viewRect.y + (viewRect.height - height) / 2
    
        //自适应
        let autofitItems = document.querySelectorAll('.autofit')
        autofitItems.forEach(item => {
          //设置canvas(autofit)的宽高,注意这里千万不要直接设置canvas.width和height。
          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.js

    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.js

    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文件并混淆和去除无用引用,优化图片资源。

于小懒
191 声望381 粉丝

长的是深夜,短的是人生。在你成长的这些年里,真正放不下的只有筷子。