Phaser 小遊戲 —— 不靠譜的忍者

需求
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,不能一步到位。

以上如有好的處理方法,還請各位不吝賜教。css

預覽:https://hewq.github.io/apps/a...

代碼:https://github.com/hewq/Phase...html

參考:https://triqui.itch.io/irresp...webpack

做者:https://hewq.github.io/apps/r...git

相關文章
相關標籤/搜索