Table of Contents
首先需要说明的是,《snowmen-attack》是 Phaser 案例自带的小游戏,我只是改成了 webpack 开发方式,添加了缩放和移动端触摸支持。
原版《snowman-attack》地址:https://github.com/photonstorm/phaser3-examples/tree/master/public/src/games/snowmen%20attack
Phaser 版本:3.50.1
改版《snowmen-attack》地址:https://github.com/tonyzhou1890/snowmen-attack
webpack 模板
我用的开发模板是 phaser3-project-template。如果是简单的小游戏,这个模板够用了,但如果规模大点就不建议了,因为这个模板真的很简陋。
为了支持将 public 目录(自建的资源目录)的资源拷贝到 dist,需要安装 copy-webpack-plugin,我安装的 6.2.1 版本,因为新版会报错。为了支持 css,安装了 css-loader 和 style-loader。配置如下:
// webpack/base.js const webpack = require("webpack"); const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { mode: "development", devtool: "eval-source-map", module: { rules: [ // …… { test: /\.css$/, exclude: /node_modules/, use: [ { loader: 'style-loader' }, { loader: "css-loader" } ] }, ] }, plugins: [ // …… new CopyWebpackPlugin({ patterns: [{ from: path.resolve(__dirname, "../src/public"), to: './public' }] }) ], };
分辨率适配
Phaser 的案例分辨率基本都是写死的,比如这个小游戏,写死的 1024 x 768。这在实际情况下是不可行的。游戏分辨率的适配和平时图片、看板的适配是差不多的,无非等比缩放、裁切、拉伸等。然而这些都不是我需要的,我需要的是按照宽/高缩放,另一边自适应。有点词不达意,还是画图吧。
如图。白色边框为手机尺寸(目标分辨率),虚线为保持宽高比缩放的结果,红色为预设分辨率。我的期望目标是适配白色。所以我的做法是,不保持画布宽高比进行缩放。先按照手机的宽高比调整画布的宽/高,然后计算缩放比例。游戏里的相关数值根据调整后的宽/高计算。不考虑缩放比例,整体的缩放交给游戏引擎。
// utils.js /** * 获取系统信息,并计算目标宽高和缩放系数 * @param {number|undefined} initWidth * @param {number|undefined} initHeight */ export function systemInfo(initWidth, initHeight) { const DPR = window.devicePixelRatio const pixelWidth = window.innerWidth * DPR const pixelHeight = window.innerHeight * DPR let targetWidth = initWidth ?? pixelWidth let targetHeight = initHeight ?? pixelHeight let ratio = targetWidth / targetHeight let scale = pixelWidth / pixelHeight > ratio ? pixelHeight / targetHeight : pixelWidth / targetWidth let offsetX = (pixelWidth - scale * targetWidth) / 2 let offsetY = (pixelHeight - scale * targetHeight) / 2 let fitWidth = pixelWidth / scale let fitHeight = pixelHeight / scale return { innerWIdth: window.innerWidth, innerHeight: window.innerHeight, DPR, pixelWidth, pixelHeight, initWidth, initHeight, targetWidth, targetHeight, ratio, scale, offsetX, offsetY, fitWidth, fitHeight } }
// index.js const { fitWidth, fitHeight } = systemInfo(1024, 768) const config = { type: Phaser.AUTO, parent: 'root', width: fitWidth, height: fitHeight, backgroundColor: '#3366b2', scale: { mode: Phaser.Scale.ScaleModes.FIT }, };
这个游戏里我只用到了 fitWidth 和 fitHeight,其余的忽略。
另外,还需要考虑横屏和全屏。因为这是一个横屏游戏,所以在只有横屏的情况下,才能开始游戏。
// utils.js /** * 横屏检测 */ export function isLandscape() { return window.innerWidth > window.innerHeight || (window.orientation === 90 || window.orientation === -90) }
因为 pc 没有 orientation 属性,所以只需要宽度大于高度就可以。
至于全屏,这个只能在非全屏的情况下给用户一个按钮,用户点击按钮后调用浏览器全屏 api,然后开始游戏。
// utils.js /** * 全屏检测 */ export function isFullscreen() { return document.fullscreenElement === document.querySelector('#root') }
// index.js /** * 点击按钮的回调 * 设备检查,其中宽度大于高度是必须的 */ function checkDevice() { if (!isLandscape()) return if (!isFullscreen()) { document.body.requestFullscreen() .then(res => { setTimeout(() => { startGame() }, 16); }) } else { startGame() } }
这里不管调用全屏是否成功都开始游戏。因为并不是不能玩,只是体验不佳而已。添加的延迟是为了确保全屏已经完成。
触摸屏适配
游戏里玩家雪人的移动是通过上下方向键完成的,发射雪球则是空格键。在手机端,当然是希望点击完成。所以,我在每个轨道末端添加了一个点击区。第一次点击是移动,第二次点击是发射雪球。
import Phaser from 'phaser' export default class TouchRect extends Phaser.GameObjects.Graphics { constructor(scene, x, y, width, height, trackId) { super(scene, x, y, width, height, trackId) this.trackId = trackId scene.add.existing(this) this.rect = new Phaser.Geom.Rectangle(x, y, width, height) this.text = new Phaser.GameObjects.Text(scene, x + width / 2, y + height / 2, '此处可点击').setOrigin(0.5, 0.5) scene.add.existing(this.text) scene.input.on('pointerdown', this.touch, this) } draw() { this.fillStyle('#333333', 0.3) this.fillRectShape(this.rect) } clearRect() { this.setVisible(false) this.text.setVisible(false) } touch(p) { if (this.visible === false && this.rect.contains(p.x, p.y)) { this.scene.player.handleTouch(this.trackId) } } }
// Player.js handleTouch(i) { if (this.currentTrack.id !== i) { this.moveToTrack(i) } else if (!this.isThrowing) { this.throw() } } moveToTrack(i) { this.currentTrack = this.scene.tracks[i] this.y = this.currentTrack.y this.sound.play('move') }
// Track.js import Phaser from 'phaser' import Snowman from './Snowman' import PlayerSnowball from './PlayerSnowball' import EnemySnowball from './EnemySnowball' import TouchRect from './TouchRect' export default class Track { constructor(scene, id, trackY) { this.scene = scene this.id = id this.y = trackY this.gameWidth = scene.game.config.width this.gameHeight = scene.game.config.height // …… this.touchRect = new TouchRect(scene, this.gameWidth - 200, this.y - this.gameHeight * 0.15, 200, this.gameHeight * 0.15, id) this.touchRect.draw() } start(minDelay, maxDelay) { // …… this.touchRect.clearRect() } }
至此,游戏适配部分基本完成(其余细节忽略)。
部署
webpack externals
因为我的 ecs 只有 1M 带宽,为了加快加载,只能通过 cdn 加载 Phaser。而这就需要用到 webpack 的 externals。externals 的介绍和各种用法这里不表,只写我用到的。
为了在开发时使用本地包,打包线上使用 cdn,需要安装 cross-env 进行环境区分。
// package.json "scripts": { "build": "cross-env ENV=prod webpack --config webpack/prod.js ", "start": "cross-env ENV=dev webpack-dev-server --config webpack/base.js --open" },
// webpack/base.js new HtmlWebpackPlugin({ template: "./index.html", externals: process.env.ENV === 'prod' ? [ { script: '<script crossorigin="anonymous" integrity="sha512-2kb3Q9IR7K9be52kC2yJGEflRxcLWqIzKlwki1I6Y9TMP2sqneTYdbYe1b/+7EJyr2c8mBH6/QrlC+eqbvJdSg==" src="https://lib.baomitu.com/phaser/3.50.1/phaser.min.js"></script>' } ] : [] }),
// index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snowmen-attack</title> <% htmlWebpackPlugin.options.externals.map(function(item){ %> <%= item.script ? item.script : '' %> <% }) %> </head> <body> <div id="root">加载中……</div> </body> </html>
oss
图片、音频等静态资源我是通过 oss 加速的。因为是第一次使用 oss,所以还是记录一下比较好。
首先,oss 里新建 bucket。
填一下名称,读写权限选择公共读(因为没有用 cdn)。创建好后,进入 bucket 进行文件管理,操作和网盘差不多,不需要后端支持的话直接创建文件夹/上传文件就行。
现在可以用了吗?no,还需要设置权限,否则跨域无法访问。
如上图菜单里有个权限管理,进入,跨域设置,创建/编辑规则。如下图:
有很多文章在来源那一块填的是通配符,这在我看来很不安全(一般情况通配符确实没问题,因为访问量太低),所以指定了域名。
这样配置之后就没有 oss 跨域问题了。因为指定了域名,也一定程度上防止了盗用导致高额流量费。
最后,别忘了代码里根据环境切换静态资源地址:
// config.js export const baseUrl = (() => { switch (process.env.ENV) { case 'prod': return 'https://xxx.oss-cn-shanghai.aliyuncs.com/snowman-attack/1.0.0/' default: return './public/' } })()