关于phaser:Phaser-小游戏-不靠谱的忍者

10次阅读

共计 10630 个字符,预计需要花费 27 分钟才能阅读完成。

需要
1. 按住屏幕,棍子伸长,放开手指,棍子放下。
2. 具备计时性能。
3. 计时完结或者棍子没有放到平安区域,则游戏完结。

独自一个 Scene 作为背景,避免显示 Scene 重叠时有遮挡

// background.ts
export 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.ts
import 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.ts
export 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.ts
export 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.ts
import 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.ts
import 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.ts
export 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.ts
createWalkTimeline(): 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.ts
calcDistance(): 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.ts
export 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.ts

gameover(): 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…

正文完
 0