需要
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    });}

写在前面

  1. phaser 自身的适配仿佛没有绝对窗口定位的性能(相似 css 的 fixed),如果游戏中有这样的需要的话,就得本人手动再做多一步适配。
  2. phaser 和 webpack 联合导入资源时,感觉有些麻烦,须要先 import 之后再应用 phaser 的 loader,不能一步到位。

以上若有好的解决办法,还请各位不吝赐教。

预览:https://hewq.github.io/apps/a...

代码:https://github.com/hewq/Phase...

参考:https://triqui.itch.io/irresp...

作者:https://hewq.github.io/apps/r...