关于phaser:Phaser-小游戏-不靠谱的忍者
需要1. 按住屏幕,棍子伸长,放开手指,棍子放下。2. 具备计时性能。3. 计时完结或者棍子没有放到平安区域,则游戏完结。独自一个 Scene 作为背景,避免显示 Scene 重叠时有遮挡// background.tsexport default class extends Phaser.Scene { constructor() { super({ key: 'BackgroundScene', active: true }); } create(): void { const graphics = this.add.graphics(); graphics.fillGradientStyle(0x241a47, 0x241a47, 0x274aa0, 0x274aa0); graphics.fillRect(0, 0, window.game.width, window.game.height); this.scene.launch('FootScene'); this.scene.launch('StartScene'); }}底部组件底部的云层成果作为公共组件应用// foot.tsimport pngCloud from '@images/cloud.png';const config: FootConfig = { circleNum: 7, blueCircleFrame: 0, whileCircleFrame: 1, blueCloudY: 160, whileCloudY: 190, height: 90};export default class extends Phaser.Scene { constructor() { super({ key: 'FootScene' }); } preload(): void { this.load.spritesheet('ssCloud', pngCloud, { frameWidth: 256, frameHeight: 256}); } create(): void { const blueGroup = this.add.group([], { key: 'ssCloud', frame: [config.blueCircleFrame], frameQuantity: config.circleNum, setXY: { y: config.blueCloudY, stepX: 120, stepY: 0 } }); const whileGroup = this.add.group([], { key: 'ssCloud', frame: [config.whileCircleFrame], frameQuantity: config.circleNum, setXY: { y: config.whileCloudY, stepX: 120, stepY: 0 } }); this.resize(); window.addEventListener('resize', () => { this.resize(); }); } resize(): void { const viewHeight = document.documentElement.clientHeight / window.rem; const camerasY = (window.game.height - viewHeight) / 2 + viewHeight - config.height; this.cameras.main.setPosition(0, camerasY); }}增加动画// foot.tsexport default class extends Phaser.Scene { create(): void { this.add.tween({ targets: blueGroup.getChildren(), props: { y: (target) => { return target.y + Phaser.Math.Between(-5, 0); }, scale: (target) => { return target.scale + Phaser.Math.FloatBetween(-0.05, 0.05); } }, duration: 3000, yoyo: true, repeat: -1, ease: Phaser.Math.Easing.Sine.InOut }); this.add.tween({ targets: whileGroup.getChildren(), props: { y: (target) => { return target.y + Phaser.Math.Between(-5, 0); }, scale: (target) => { return target.scale + Phaser.Math.FloatBetween(0, 0.1); } }, duration: 3000, yoyo: true, repeat: -1, ease: Phaser.Math.Easing.Sine.InOut }); }}适配,始终显示在窗口底部,css 的 fixed 成果// foot.tsexport default class extends Phaser.Scene { create(): void { this.resize(); window.addEventListener('resize', () => { this.resize(); }); } resize(): void { const viewHeight = document.documentElement.clientHeight / window.rem; const camerasY = (window.game.height - viewHeight) / 2 + viewHeight - config.height; this.cameras.main.setPosition(0, camerasY); }}开始页面// start.tsimport pngBtnStart from '@images/btn_start.png';import pngTitle from '@images/title.png';export default class extends Phaser.Scene { constructor() { super({ key: 'StartScene' }); } preload(): void { this.load.image('imgBtnStart', pngBtnStart); this.load.image('imgTitle', pngTitle); } create(): void { this.add.rectangle(window.game.width / 2, window.game.height / 2, window.game.width, window.game.height, 0x000000, 0.5); this.add.image(window.game.width / 2, 240, 'imgTitle').setOrigin(0.5, 0); const btnStart = this.add.sprite(window.game.width / 2, window.game.height / 2, 'imgBtnStart').setInteractive(); btnStart.on('pointerdown', () => { this.scene.start('MainScene'); }); this.add.tween({ targets: btnStart, props: { y: (target) => { return target.y + 20; } }, yoyo: true, loop: -1, duration: 2000, ease: Phaser.Math.Easing.Sine.InOut }); }}主场景显示动态信息// main.tsimport pngTips from '@images/tips.png';import pngProcess from '@images/process_border.png';import pngSprites from '@images/sprites.png';import pngNinja from '@images/ninja.png';let txtDistance: Phaser.GameObjects.Text; // 文本,显示间隔let rect: Phaser.GameObjects.Rectangle; // 进度条let stick: Phaser.GameObjects.Rectangle; // 棍子let processTimerEvent: Phaser.Time.TimerEvent; // 计时事件let overContainer: Phaser.GameObjects.Container; // 游戏完结内容容器let gameContainer: Phaser.GameObjects.Container; // 游戏内容容器let prePlatformDistance: number; // 到上一个站台的间隔let nextPlatformDistance: number; // 到下一个站台的间隔let curPlatformWidth: number; // 以后站台宽度let prePlatformWidth: number; // 上一个站台宽度let nextPlatformWidth: number; // 下一个站台宽度let curPlatformX: number; // 以后站台地位let nextPlatformX: number; // 下一个站台地位let ninja: Phaser.GameObjects.Sprite; // 不靠谱的忍者本者let distance = 0; // 间隔,站台数let isPlaying = false; // 是否处于解决流程中,区间在从按下到开释后的动画播放完结let isStart = false; // 是否开始游戏const config: GameConfig = { processLen: 500, // 进度条长度 processHeight: 29, // 进度条高度 platformHeight: 600, // 站台高度 stickWidth: 10, // 棍子宽度 stickHeight: -10 // 棍子长度,坐标系起因,取负值};// 变动的值独自拧进去let stickHeight = config.stickHeight;let processLen = config.processLen;export default class extends Phaser.Scene { constructor() { super({ key: 'MainScene' }); } preload(): void { this.load.image('imgTips', pngTips); this.load.image('imgProcess', pngProcess); this.load.spritesheet('ssSprites', pngSprites, { frameWidth: 150, frameHeight: 150}); this.load.spritesheet('ssNinja', pngNinja, { frameWidth: 462 / 6, frameHeight: 388 / 4}); } create(): void { // 提示信息 const tips = this.add.image(window.game.width / 2, 400, 'imgTips'); // 进度条 const process = this.add.image(0, 0, 'imgProcess'); txtDistance = this.add.text(260, -24, 'DISTANCE: 0', { fontFamily: 'Arial', fontSize: 40, color: '#ffffff' }).setOrigin(1); rect = this.add.rectangle(-250, 0, config.processLen, config.processHeight, 0xffffff).setOrigin(0, 0.5); // 进度条区域内容容器 const timerContainer = this.add.container(window.game.width / 2, 400, [process, txtDistance, rect]); timerContainer.setAlpha(0); // 游戏完结内容 const btnPlay = this.add.sprite(-110, 0, 'ssSprites', 0).setInteractive(); const btnHome = this.add.sprite(110, 0, 'ssSprites', 1).setInteractive(); overContainer = this.add.container(window.game.width / 2, 1800, [btnPlay, btnHome]); overContainer.setAlpha(0.5); // 初始化忍者 ninja = this.add.sprite(90, -600, 'ssNinja', 0).setOrigin(1); this.anims.create({ key: 'stand', frames: this.anims.generateFrameNumbers('ssNinja', { start: 0, end: 11 }), frameRate: 12, repeat: -1, yoyo: true }); this.anims.create({ key: 'walk', frames: this.anims.generateFrameNumbers('ssNinja', { start: 12, end: 19 }), frameRate: 12, repeat: -1 }); ninja.play('stand'); // 初始化棍子 stick = this.add.rectangle(90, -config.platformHeight, config.stickWidth, config.stickHeight, 0x000000).setOrigin(1, 0); gameContainer = this.add.container(window.game.width / 2, window.game.height, [ninja, stick]); gameContainer.setSize(window.game.width, window.game.height); gameContainer.setPosition(0, window.game.height); // 初始化站台 const firstPlatform = this.add.rectangle(0, 0, 100, config.platformHeight, 0x000000).setOrigin(0, 1); gameContainer.add(firstPlatform); gameContainer.bringToTop(ninja); curPlatformX = 0; curPlatformWidth = 100; prePlatformDistance = 0; }}生成站台// main.tsexport default class extends Phaser.Scene { create(): void { this.createPlatform(false); } createPlatform(playAnim: boolean): void { nextPlatformDistance = Phaser.Math.Between(150, 300); // 随机下个站台的间隔 nextPlatformWidth = Phaser.Math.Between(80, 150); // 随机下个站台的宽度 nextPlatformX = curPlatformX + nextPlatformDistance + curPlatformWidth; // 计算下个站台的地位 const rect = this.add.rectangle(nextPlatformX + 750, 0, nextPlatformWidth, config.platformHeight, 0x000000).setOrigin(0, 1); // 增加站台 gameContainer.add(rect); gameContainer.bringToTop(ninja); // 忍者提到最上层,掉下去的时候不会被站台遮挡 if (playAnim) { // 是否播放站台呈现时的动画 this.add.tween({ // 游戏容器挪动 targets: gameContainer, props: { x: (target) => { return target.x - (prePlatformDistance + prePlatformWidth); } }, duration: 300, ease: Phaser.Math.Easing.Sine.In }); this.add.tween({ // 新增站台挪动 targets: rect, props: { x: nextPlatformX }, duration: 500, ease: Phaser.Math.Easing.Sine.In }); this.add.tween({ // 忍者回到指定地位 targets: ninja, props: { y: (target) => { return target.y + 10; }, x: curPlatformX + (curPlatformWidth - 20) }, duration: 300 }); this.add.tween({ // 棍子回到指定地位 targets: stick, props: { alpha: 0 }, duration: 300, onComplete: () => { stick.setSize(config.stickWidth, config.stickHeight); stick.setAngle(0); stick.setX(curPlatformX + (curPlatformWidth - 10)); stick.setAlpha(1); isPlaying = false; } }); } else { rect.setPosition(nextPlatformX, 0); } }}生成动画// main.tscreateWalkTimeline(): void { const walkTimeline = this.tweens.createTimeline(); // 棍子动画 walkTimeline.add({ targets: stick, props: { angle: 90 }, duration: 500, ease: Phaser.Math.Easing.Bounce.Out, onComplete() { ninja.play('walk'); } }); // 走路动画 walkTimeline.add({ targets: ninja, props: { x: (target) => { return target.x + Math.abs(stickHeight) + ninja.getBounds().width / 2; }, y: (target) => { return target.y - 10; } }, duration: 500, onComplete: () => { ninja.play('stand'); this.calcDistance(); walkTimeline.destroy(); } }); walkTimeline.play();}计算一次游戏后果// main.tscalcDistance(): void { const near = Phaser.Math.Distance.Between(stick.x, 0, nextPlatformX, 0); // 下个站台的近端 const far = Phaser.Math.Distance.Between(stick.x, 0, nextPlatformX + nextPlatformWidth, 0);// 下个站台的远端 // 以后值变动 curPlatformX = nextPlatformX; prePlatformWidth = curPlatformWidth; curPlatformWidth = nextPlatformWidth; prePlatformDistance = nextPlatformDistance; if (-stickHeight > near && -stickHeight < far) { // 平安区域 this.createPlatform(true); txtDistance.setText(`DISTANCE: ${++distance}`); // 间隔 +1 } else { this.gameover(); // 游戏完结 if (-stickHeight < near) { this.add.tween({ targets: stick, props: { angle: 180 }, duration: 800, ease: Phaser.Math.Easing.Bounce.Out }); } } // 重置棍子长度 stickHeight = config.stickHeight;}监听事件// main.tsexport default class extends Phaser.Scene { create(): void { let stickTimerEvent: Phaser.Time.TimerEvent; this.input.on('pointerdown', () => { // 点击屏幕 if (!isPlaying) { tips.setAlpha(0); timerContainer.setAlpha(1); // 开始计时 if (!isStart) { isStart = true; processTimerEvent = this.time.addEvent({ callback: this.processTimer.bind(this), loop: true, delay: 1000 }); } // 棍子伸长事件 stickTimerEvent = this.time.addEvent({ callback: this.stickTimer, loop: true, delay: 10 }); } }); this.input.on('pointerup', () => { // 开释 if (stickTimerEvent && !isPlaying) { isPlaying = true; stickTimerEvent.destroy(); this.createWalkTimeline(); } }); // 从新开始 btnPlay.on('pointerdown', (pointer, localX, localY, event) => { event.stopPropagation(); // 阻止冒泡 this.reset(); this.scene.restart(); }); // 回到首页 btnHome.on('pointerdown', (pointer, localX, localY, event) => { event.stopPropagation(); this.reset(); this.scene.start('StartScene'); }); } stickTimer(): void { stickHeight -= 25; stick.setSize(config.stickWidth, stickHeight); } processTimer(): void { processLen -= 5; rect.setSize(processLen, config.processHeight); if (processLen === 0) { processTimerEvent.destroy(); this.gameover(); } } // 数据复位 reset(): void { processLen = config.processLen; isPlaying = false; isStart = false; distance = 0; }}游戏完结// main.tsgameover(): void { processTimerEvent.destroy(); // shake const vec2 = new Phaser.Math.Vector2(0.005, 0.01); this.add.tween({ targets: ninja, ease: Phaser.Math.Easing.Linear, duration: 600, props: { angle: 45, x: (target) => { return target.x + 50; }, y: 50 }, onComplete: () => { // 晃动成果 this.cameras.main.shake(200, vec2); this.scene.get('FootScene').cameras.main.shake(200, vec2); this.scene.get('BackgroundScene').cameras.main.shake(200, vec2); } }); this.add.tween({ targets: overContainer, props: { y: window.game.height / 2, alpha: 1 }, delay: 1200, duration: 800, ease: Phaser.Math.Easing.Back.InOut }); this.add.tween({ targets: gameContainer, props: { alpha: 0 }, delay: 1000, duration: 800, ease: Phaser.Math.Easing.Back.InOut });}写在前面phaser 自身的适配仿佛没有绝对窗口定位的性能(相似 css 的 fixed),如果游戏中有这样的需要的话,就得本人手动再做多一步适配。phaser 和 webpack 联合导入资源时,感觉有些麻烦,须要先 import 之后再应用 phaser 的 loader,不能一步到位。以上若有好的解决办法,还请各位不吝赐教。 ...