Arkanout

Description

Cette fois-ci je me suis lancé sur un clone d'Arkanoïd / Breakout. J'ai pu continuer à me familiariser avec la bibliothèque Phaser, notamment :

  • L'utilisation de staticGroup() qui permet de regrouper des objets comme des sprites et leurs appliquer les mêmes contraintes (utile ici pour les briques, on peut définir un collider qui observe le groupe entier plutôt que d'en appliquer un par brique, dans une boucle).
  • L'utilisation de setTexture() et setBodySize() pour la transformation du pad ( quand on ramasse un bonus ). Cela évite de détruire le sprite et devoir en créer un nouveau ( et réappliquer les différents colliders ... )

En une après midi, j'ai créé ce petit clone qui permet de jouer un niveau complet. Le point qui me satisfait le moins concerne le rebond de la balle sur le pad. En effet, mes cours de maths sont loin, la motivation et le temps venaient à manquer. J'ai donc recherché sur le net une formule qui pouvait remplir ce rôle. J'ai trouvé quelques pistes mais hélas rien de très concluant, ou bien trop compliqué pour un mini-projet sur un temps (et ma patience) limité.

J'ai donc adapté un bout de formule glânée sur le net, et bidouillé tout ça...

Quelques pistes d'amélioration :

  • Une gestion des bonus différentes, ici j'utilise une image pour le pad, mais on pourrait dessiner "à la main" le pad et augmenter sa taille programmatiquement.
  • Avoir plusieurs vies avant de voir le Game Over.
  • Une interface utilisateur.
  • La formule du rebond de la balle sur le pad !

Chargement du jeu en cours ...

Code source

Arkanout/ArkanoutGame.ts

// games/Arkanout/ArkanoutGame.ts

import * as Phaser from "phaser";

import ArkanoutPlayScene from "./scenes/ArkanoutPlayScene";

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: { x: 0, y: 0 },
    },
  },
  scene: [ArkanoutPlayScene],
};

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

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

  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;

Arkanout/scenes/ArkanoutPlayScene.ts

// games/Arkanout/scenes/ArkanoutPlayScene.ts

import * as Phaser from "phaser";
import { random } from "lodash";

const MAX_ROW = 10;
const MAX_COLUMN = 5;
const OFFSET_X = 80;
const SPACING_X = 40;
const SPACING_Y = 100;
const BRICK_HEIGHT = 32;
const BRICK_WIDTH = 64;

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

const PADDLE_SIZES: Size[] = [
  { width: 128, height: 24 },
  { width: 159, height: 24 },
];

export default class ArkanoutPlayScene extends Phaser.Scene {
  canvas?: HTMLCanvasElement;
  canvasWidth?: number;
  canvasHeight?: number;
  paddle?: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
  paddleGrowth?: number;
  ball?: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
  ballSpeed?: number;
  bricks?: Phaser.Physics.Arcade.StaticGroup;
  left?: Phaser.Input.Keyboard.Key;
  right?: Phaser.Input.Keyboard.Key;
  score?: number;
  scoreText?: Phaser.GameObjects.Text;

  constructor() {
    super("arkanout");
  }

  preload(): void {
    this.canvas = this.sys.game.canvas;
    this.canvasHeight = this.canvas!.height;
    this.canvasWidth = this.canvas!.width;

    this.loadAssets();
  }

  loadAssets(): void {
    this.load.setBaseURL("/images/games/arkanout/");

    this.load.image("paddle-0", "paddle-little.png");
    this.load.image("paddle-1", "paddle-medium.png");
    this.load.image("paddle-2", "paddle-big.png");
    this.load.image("ball", "ball-red.png");
    this.load.image("bonus", "ball-green.png");
    this.load.image("brick-0", "brick-blue.png");
    this.load.image("brick-1", "brick-orange.png");
    this.load.image("brick-2", "brick-red.png");
    this.load.image("brick-3", "brick-green.png");
    this.load.image("brick-4", "brick-yellow.png");
  }

  create(): void {
    this.initializeInputs();
    this.initializeObjects();
    this.initializeScore();
    this.initializeColliders();
  }

  initializeInputs(): void {
    this.left = this.input?.keyboard?.addKey(
      Phaser.Input.Keyboard.KeyCodes.LEFT
    );
    this.right = this.input?.keyboard?.addKey(
      Phaser.Input.Keyboard.KeyCodes.RIGHT
    );
  }

  initializeObjects(): void {
    this.paddle = this.physics.add
      .sprite(400, 550, "paddle-0")
      .setImmovable(true);
    this.paddleGrowth = 0;

    this.ball = this.physics.add
      .sprite(400, 500, "ball")
      .setBounce(1)
      .setCollideWorldBounds(true)
      .setVelocity(Phaser.Math.Between(-200, 200), -300)
      .setMaxVelocity(500, 500);

    this.ballSpeed = 1.0;

    this.bricks = this.physics.add.staticGroup();

    for (let row = 0; row < MAX_ROW; row++) {
      for (let column = 0; column < MAX_COLUMN; column++) {
        this.bricks.create(
          OFFSET_X + SPACING_X + row * BRICK_WIDTH,
          SPACING_Y + column * BRICK_HEIGHT,
          `brick-${column}`
        );
      }
    }
  }

  initializeColliders(): void {
    this.physics.add.collider(
      this.ball!,
      this.paddle!,
      this.ballBounce,
      undefined,
      this
    );

    this.physics.add.collider(
      this.ball!,
      this.bricks!,
      this.hitBrick,
      undefined,
      this
    );
  }

  initializeScore(): void {
    this.score = 0;
    this.scoreText = this.add.text(
      this.canvasWidth! / 2,
      20,
      this.score.toString(),
      {
        fontSize: "32px",
        fontFamily: "PressStart2P",
      }
    );
    this.scoreText.setX(this.canvasWidth! / 2 - this.scoreText.width / 2);
    this.scoreText.setDepth(999_999);
  }

  update(): void {
    this.handlePaddleMovement();

    if (this.isBallFellOnTheGround() || this.areBricksBroke()) {
      this.gameOver();
    }
  }

  handlePaddleMovement(): void {
    if (this.input?.keyboard?.checkDown(this.left!, 20)) {
      if (this.paddle!.getBounds().left > 0) {
        this.paddle!.x -= 10;
      }
    } else if (this.input?.keyboard?.checkDown(this.right!, 20)) {
      if (this.paddle!.getBounds().right < this.canvasWidth!) {
        this.paddle!.x += 10;
      }
    }
  }

  isBallFellOnTheGround(): boolean {
    return this.ball!.getBounds().bottom >= this.canvasHeight!;
  }

  areBricksBroke(): boolean {
    return this.bricks?.getLength() === 0;
  }

  hitBrick(
    ball:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile,
    brick:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile
  ): void {
    const brickSprite =
      brick as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    const position: Phaser.Geom.Rectangle = brickSprite.getBounds();

    brickSprite.destroy();
    this.ballSpeed! += 0.001;

    const ballSprite =
      ball as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    ballSprite.setVelocityY(ballSprite.body.velocity.y * this.ballSpeed!);

    this.increaseScore();
    if (random(1, 10) % 3 === 0) {
      const bonus: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody =
        this.physics.add
          .sprite(position.centerX, position.centerY, "bonus")
          .setVelocityY(200);
      this.physics.add.collider(
        this.paddle!,
        bonus,
        this.handleBonusCatched,
        undefined,
        this
      );
    }
  }

  ballBounce(
    ball:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile,
    paddle:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile
  ) {
    const ballSprite =
      ball as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
    const paddleSprite =
      paddle as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;

    const impactPoint = ballSprite.x - paddleSprite.x;
    const impactAngle = impactPoint * Phaser.Math.DEG_TO_RAD;

    let velocityY = -300 * Math.sin(impactAngle);
    if (velocityY > -10.0) {
      velocityY = random(-50.0, -100.0);
    }
    let velocityX = -300 * Math.cos(impactAngle);
    this.ballSpeed! += 0.001;
    ballSprite.setVelocityY(velocityY * this.ballSpeed!);
    ballSprite.setVelocityX(velocityX);
  }

  handleBonusCatched(
    paddle:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile,
    bonus:
      | Phaser.Types.Physics.Arcade.GameObjectWithBody
      | Phaser.Physics.Arcade.Body
      | Phaser.Physics.Arcade.StaticBody
      | Phaser.Tilemaps.Tile
  ) {
    bonus.destroy();
    if (this.paddleGrowth! < 2) this.paddleGrowth!++;
    (paddle as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody).setTexture(
      `paddle-${this.paddleGrowth!}`
    );
    const newSize: Size = PADDLE_SIZES[this.paddleGrowth! - 1];
    (paddle as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody).setBodySize(
      newSize.width,
      newSize.height
    );
  }

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

  gameOver(): void {
    this.physics.pause();

    const text = this.add.text(
      this.canvasWidth! / 2,
      this.canvasHeight! / 2,
      "GAME OVER",
      {
        fontSize: "52px",
        fontFamily: "PressStart2P",
      }
    );
    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