Flappy

Description

Premier petit jeu que je réalise avec la bibliothèque PhaserJS, un clone de flappy bird.

J'ai pu ici me familiariser avec certains concepts clés de PhaserJS :

  • Les fonctions principales du cycles de vie
  • La création de GameObject et leurs fonctions utilitaires
  • Utilisation d'un spritesheet
  • Manipulation du texte

Ca m'a pris quelques heures, le code source et le jeu être améliorer sur pas mal d'aspect :

  • Le scoring: on pourrait mettre un collider invisible qui augmenterai let score au moment où l'oiseau passe les tuyaux
  • Le recyclage des tuyaux : on pourrait faire un roulement entre les objets qui dépassent l'écran et ceux qui apparaissent pour économiser de la mémoire, car pour le moment on se contente de détruire puis de créer de nouveaux objets.
  • Une interface / ui : Présentation des touches, pouvoir mettre en pause, un bouton recommencer directement dans l'interface du jeu.
  • Une difficulté variable / croissante : On pourrait imaginer introduire de l'aléatoire pondéré pour par exemple augmenter la fréquence d'apparition des tuyaux, l'espacement entre le tuyau du haut et du bas...

L'idée avec ces petits projets n'est pas forcement de faire des jeux exempts défauts mais plutôt une suite de mini-jeu qui peuvent se développer rapidement.

Chargement du jeu en cours ...

Code source

Flappy/FlappyGame.ts

// games/Flappy/FlappyGame.ts

import * as Phaser from "phaser";

import FlappyPlayScene from "./scenes/FlappyPlayScene";

const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 600;

let config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,
  parent: "phaser-container",
  backgroundColor: "#282c34",
  scale: {
    mode: Phaser.Scale.ScaleModes.NONE,
    width: DEFAULT_WIDTH,
    height: DEFAULT_HEIGHT,
  },
  physics: {
    default: "arcade",
    arcade: {
      gravity: { y: 400, x: 0 },
    },
  },
  scene: [FlappyPlayScene],
};

interface FlappyGameConfig {
  width: number;
  height: number;
}

function createGame(props: any): Phaser.Game {
  const { width, height } = props as FlappyGameConfig;

  if (config.scale) {
    config.scale.width = width || DEFAULT_WIDTH;
    config.scale.height = height || DEFAULT_HEIGHT;
  }

  const game: Phaser.Game = new Phaser.Game(config);
  return game;
}

export default createGame;

Flappy/scenes/FlappyPlayScene.ts

// games/Flappy/scenes/FlappyPlayScene.ts

import * as Phaser from "phaser";
import { remove, random, last } from "lodash";
import { Press_Start_2P } from "next/font/google";

const MAX_PIPE_ON_SCREEN: number = 2;
const DEFAULT_SPACE_BEETWEEN_PIPES: number = 175;
const MINIMUM_PIPE_HEIGHT_TOP = 100;
const MAXIMUM_PIPE_HEIGHT_TOP = 400;
const FLAP_VELOCITY = -220;

export default class FlappyPlayScene extends Phaser.Scene {
  canvas?: HTMLCanvasElement;
  canvasHeight?: number;
  canvasWidth?: number;
  spaceBar?: Phaser.Input.Keyboard.Key;
  bird?: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;

  pipes?: Phaser.Types.Physics.Arcade.ImageWithDynamicBody[];
  pipesOb?: Phaser.Physics.Arcade.Group;

  score?: number;
  scoreText?: Phaser.GameObjects.Text;

  constructor() {
    super("flappy");
    this.pipes = [];
  }

  preload(): void {
    this.canvas = this.sys.game.canvas;
    this.canvasHeight = this.canvas!.height;
    this.canvasWidth = this.canvas!.width;
    this.load.setBaseURL("/images/games/flappy/");

    this.load.image("sky", "sky.png");
    this.load.image("pipe", "pipe.png");
    this.load.spritesheet("bird", "birdSprite.png", {
      frameWidth: 16,
      frameHeight: 16,
    });

    this.spaceBar = this.input?.keyboard?.addKey(
      Phaser.Input.Keyboard.KeyCodes.SPACE
    );
  }

  create(): void {
    this.createBackground();
    this.createBird();
    this.initializePipes();
    this.createColliders();
    this.initializeScore();

    this.anims.create({
      key: "fly",
      frames: this.anims.generateFrameNumbers("bird", { start: 9, end: 15 }),
      frameRate: 8,
      repeat: -1,
    });

    this.bird!.play("fly");
    this.input.on("pointerdown", () => this.bird?.setVelocityY(FLAP_VELOCITY));
  }

  createColliders(): void {
    this.physics.add.collider(
      this.bird!,
      this.pipes!,
      this.gameOver,
      undefined,
      this
    );
  }

  updatePipes(): void {
    this.pipes?.forEach((pipe) => {
      if (pipe.getTopRight().x <= 0) {
        pipe.destroy();
        remove(this.pipes!, pipe);
      }
    });
  }

  initializeScore(): void {
    this.score = 0;
    this.scoreText = this.add.text(
      this.canvasWidth! / 2,
      this.canvasHeight! - 100,
      this.score.toString(),
      {
        fontSize: 54,
        color: "#ffffff",
        stroke: "#000000",
        strokeThickness: 3,
        resolution: 4,
      }
    );
    this.scoreText.setX(this.canvasWidth! / 2 - this.scoreText.width / 2);
    this.scoreText.setDepth(999_999);
  }

  createBackground(): void {
    this.add.image(0, 0, "sky").setOrigin(0);
  }

  createBird(): void {
    this.bird = this.physics.add
      .sprite(this.canvasWidth! / 3, this.canvasHeight! / 2, "bird")
      .setFlipX(true)
      .setScale(3);

    this.bird.setBodySize(this.bird.width, this.bird.height - 8);
  }

  initializePipes() {
    this.pipes = [
      ...this.pipes!,
      ...this.createPipes(
        (this.canvasWidth! / 5) * 3,
        random(MINIMUM_PIPE_HEIGHT_TOP, MAXIMUM_PIPE_HEIGHT_TOP)
      ),
      ...this.createPipes(
        (this.canvasWidth! / 5) * 4.75,
        random(MINIMUM_PIPE_HEIGHT_TOP, MAXIMUM_PIPE_HEIGHT_TOP)
      ),
    ];
  }

  createPipes(
    x: number,
    y: number,
    spacing: number = DEFAULT_SPACE_BEETWEEN_PIPES
  ): [
    Phaser.Types.Physics.Arcade.ImageWithDynamicBody,
    Phaser.Types.Physics.Arcade.ImageWithDynamicBody
  ] {
    const upperPipe: Phaser.Types.Physics.Arcade.ImageWithDynamicBody =
      this.physics.add
        .image(x, y, "pipe")
        .setOrigin(0, 1)
        .setImmovable(true)
        .setVelocityX(-200)
        .setY(y);

    upperPipe.body.allowGravity = false;

    const lowerPipe: Phaser.Types.Physics.Arcade.ImageWithDynamicBody =
      this.physics.add
        .image(x, y, "pipe")
        .setOrigin(0, 0)
        .setImmovable(true)
        .setVelocityX(-200)
        .setY(y + spacing);
    lowerPipe.body.allowGravity = false;

    return [upperPipe, lowerPipe];
  }

  checkGameStatus() {
    if (
      this.bird!.getBounds().bottom >= this.canvasHeight! ||
      this.bird!.y <= 0
    ) {
      this.gameOver();
    }
  }

  update(): void {
    if (Phaser.Input.Keyboard.JustDown(this.spaceBar!)) {
      this.bird?.setVelocityY(FLAP_VELOCITY);
    }

    this.updatePipes();

    if (this.shouldGeneratePipes()) {
      const newPipes: [
        Phaser.Types.Physics.Arcade.ImageWithDynamicBody,
        Phaser.Types.Physics.Arcade.ImageWithDynamicBody
      ] = this.createPipes(
        this.canvasWidth!,
        random(MINIMUM_PIPE_HEIGHT_TOP, MAXIMUM_PIPE_HEIGHT_TOP)
      );
      this.pipes = [...this.pipes!, ...newPipes];
      this.createColliders();
      this.increaseScore();
    }

    this.checkGameStatus();
  }

  increaseScore() {
    this.score! += 1;
    this.scoreText!.text = this.score!.toString();
    this.scoreText!.setX(this.canvasWidth! / 2 - this.scoreText!.width / 2);
  }

  shouldGeneratePipes(): boolean {
    let generatePipes = false;

    const hasMaxPipes = this.pipes!.length >= MAX_PIPE_ON_SCREEN * 2;

    const lastPipe = last(this.pipes!);
    const xPos = lastPipe!.getTopLeft().x;
    const lastPipesFarEnough = xPos <= this.canvasWidth! / 2;

    generatePipes = hasMaxPipes && lastPipesFarEnough;

    return generatePipes;
  }

  gameOver(): void {
    this.physics.pause();
    this.bird?.anims.stop();

    const text = this.add.text(
      this.canvasWidth! / 2,
      this.canvasHeight! / 2,
      "GAME OVER",
      {
        fontSize: 54,
        color: "#ffffff",
        stroke: "#000000",
        strokeThickness: 4,
        resolution: 4,
      }
    );
    text.setX(this.canvasWidth! / 2 - text.width / 2);
    text.setY(this.canvasHeight! / 2 - text.height / 2);
  }

  restart(): void {
    this.scene.restart();
  }
}

Tags

  • phaser
  • javascript
  • typescript
  • arcade
  • clone
  • jeux-video

Cet article à été posté le