Phaser. Unit 8. Dino game clone (part II). Improvements in scenery

Introduction

In the previous unit, we developed a clone of Google Chrome’s “secret” Dino game using simple web technologies such as HTML, CSS, JavaScript, and Phaser 3. This game can be run on any device using any browser (not only Chrome). The result is a game that closely resembles the original with less than 10% of the code lines.

Now we will add some improvements to the original game:

  • Adding colors:
    • Blue sky: The game will feature a blue sky background to create a more visually appealing and natural environment.
    • Brown ground: The ground will be colored brown to simulate a desert-like terrain, enhancing the thematic consistency of the game.
  • Using the whole screen:
    • Sky at the top: The sky background will extend from the top of the screen, providing a continuous and immersive visual experience.
    • Ground at the bottom: The ground will stretch to the bottom of the screen, ensuring that the game environment fully occupies the available screen space.
  • Adding more items:
    • Additional clouds: More clouds will be introduced to the game, floating at different heights to add depth and variety to the sky.
    • Mountains: Mountains will be added in the background, enhancing the scenery and creating a more dynamic landscape.
    • Nests with eggs: Nests containing eggs will be placed throughout the game, serving as potential collectibles or decorative elements to enrich gameplay.
  • Varying speeds based on distance:
    • Parallax effect: Items such as clouds, mountains, obstacles, and eggs will move at different speeds relative to their distance from the player. This creates a parallax effect, making closer items appear to move faster than those further away, adding depth and a sense of realism to the game.
Gameplay

CSS code (styles.css)

No changes are needed for the CSS code, so it will remain as before:

html, body {
  margin: 0px;
  padding: 0px;
  overflow: hidden;
}

HTML code (index.html)

No changes are needed for the HTML code, so it will also remain as before:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="description" content="Dino game clone using Phaser">
  <meta name="keywords" content="HTML, CSS, JavaScript, Phaser">
  <meta name="author" content="Fernando Ruiz Rico">
  
  <title>Dino Phaser</title>

  <link href="styles.css" rel="stylesheet">
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  <script src="preload.js"></script>
  <script src="create.js"></script>
  <script src="update.js"></script>
  <script src="config.js"></script>
</body>

</html>

Improving text styling (config.js)

To improve the visual quality of the text in the Dino game clone, we will reduce pixelation by decreasing the text resolution, resulting in smoother and clearer text edges. Additionally, we will add a white shadow effect with slight offsets and blur, enhancing the contrast and readability. This combination ensures that the text stands out against varying backgrounds, making it easier for players to read scores and instructions quickly and accurately, thus providing a more polished and professional look to the game.

Changes

We will update the TEXT_STYLE configuration to enhance readability and visual appeal by using a bold font, reducing pixelation, and adding a white shadow to improve contrast and visibility:

const TEXT_STYLE = { 
  fill: "#535353", font: '900 35px Courier', resolution: 2,
  shadow: { offsetX: 2, offsetY: 2, blur: 2, color: '#FFFFFF', fill: true }
};
  • Resolution Adjustment: The text resolution is decreased from 10 to 2. This will make the text appear with a lower level of pixelation.
  • Text Shadow Addition:
    • White Color: The shadow is white (#FFFFFF), which helps it stand out against various backgrounds.
    • Offset: The shadow is slightly offset by 2 pixels to the right (offsetX: 2) and 2 pixels down (offsetY: 2), creating a subtle but effective drop shadow.
    • Blur Radius: A small blur radius of 2 pixels (blur: 2) is applied to soften the shadow edges, enhancing the text’s readability.
    • Fully Filled: The shadow effect is fully filled (fill: true), ensuring that the shadow covers the entire text uniformly, thereby improving the overall visual quality and making the text easier to read.

File contents

// Fill the whole width and height of the screen
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

// Time in milliseconds for each obsticle (lower = faster)
const OBSTICLE_TIME = 750;

// Text style to be used when printing texts (scores, etc.)
const TEXT_STYLE = { 
  fill: "#535353", font: '900 35px Courier', resolution: 2,
  shadow: { offsetX: 2, offsetY: 2, blur: 2, color: '#FFFFFF', fill: true }
};

// Variables to be initialized each time the game is restarted
function initVariables() {
  this.isGameRunning = false;
  this.gameSpeed = 10;
  this.respawnTime = 0;
  this.score = 0;
}

// Phaser initialization
let game = new Phaser.Game({
  type: Phaser.AUTO,
  width: WIDTH,
  height: HEIGHT,
  pixelArt: true,
  transparent: true,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: { preload: preload, create: create, update: update }
});

// Reload the game when the device orientation changes or the user resizes the window
let resizing = false;
window.addEventListener('resize', () => {
  if (resizing) clearTimeout(resizing);
  resizing = setTimeout(() => window.location.reload(), 500);
});

The new assets

We will add mountains in the background and some eggs in the foreground. You can find both images below, and you can also download a zip file containing all the assets here.

Mountain
Eggs

Loading the new assets (preload.js)

By adding mountains and eggs, the game gains additional scenery elements, making the game environment more detailed and engaging. To achieve this, only a couple of changes are required inside the “preload.js” file since we are adding only two new images (mountains and eggs).

Changes

The following lines will incorporate the mountain and eggs images into the game’s assets:

function preload() {
    ...
    this.load.image('mountain', 'assets/mountain.png');
    this.load.image('eggs', 'assets/eggs.png');
    ...
}
  • By loading the mountain image, the game can render mountains at various positions and scales to enhance the visual aesthetics.
  • By loading the eggs image, the game can place eggs at various locations, adding an additional layer of interactivity or visual interest.

File contents

// Load all the audios, images and sprites
function preload() {
  this.load.audio('jump', 'assets/jump.m4a');
  this.load.audio('hit', 'assets/hit.m4a');
  this.load.audio('reach', 'assets/reach.m4a');

  this.load.image('ground', 'assets/ground.png');
  this.load.image('mountain', 'assets/mountain.png');
  this.load.image('cloud', 'assets/cloud.png');
  this.load.image('eggs', 'assets/eggs.png');
  this.load.image('dino-idle', 'assets/dino-idle.png');
  this.load.image('dino-hurt', 'assets/dino-hurt.png');
  this.load.image('restart', 'assets/restart.png');
  this.load.image('game-over', 'assets/game-over.png');

  this.load.image('obsticle-1', 'assets/cactuses_small_1.png');
  this.load.image('obsticle-2', 'assets/cactuses_small_2.png');
  this.load.image('obsticle-3', 'assets/cactuses_small_3.png');
  this.load.image('obsticle-4', 'assets/cactuses_big_1.png');
  this.load.image('obsticle-5', 'assets/cactuses_big_2.png');
  this.load.image('obsticle-6', 'assets/cactuses_big_3.png');

  this.load.spritesheet('dino', 'assets/dino-run.png', { frameWidth: 88, frameHeight: 94 });
  this.load.spritesheet('enemy-bird', 'assets/enemy-bird.png', { frameWidth: 92, frameHeight: 77 });
}

// Set the volume of each audio
function initSounds() {
  this.jumpSound = this.sound.add('jump', { volume: 0.75 });
  this.hitSound = this.sound.add('hit', { volume: 0.75 });
  this.reachSound = this.sound.add('reach', { volume: 0.75 });
}

// Define all the animations from the previously loaded spritesheets
function createAnims() {
  this.anims.create({
    key: 'dino-waiting',
    frames: this.anims.generateFrameNumbers('dino', { start: 0, end: 1 }),
    frameRate: 1,
    repeat: -1
  })

  this.anims.create({
    key: 'dino-run',
    frames: this.anims.generateFrameNumbers('dino', { start: 2, end: 3 }),
    frameRate: 10,
    repeat: -1
  })

  this.anims.create({
    key: 'enemy-bird-anim',
    frames: this.anims.generateFrameNumbers('enemy-bird', { start: 0, end: 1 }),
    frameRate: 6,
    repeat: -1
  })
}

Adding sky and ground colors, and some mountains (create.js)

The new version of the game includes adding colors to the sky and the ground, enhancing the overall visual appeal and creating a more immersive environment. We will also introduce a new function to add mountains to the game, calling it from the main create function immediately after creating the clouds to ensure the mountains appear in a higher layer.

Changes

The createWorld function will add color to the sky and ground by specifying color codes as parameters when creating the rectangles:

function createWorld() {
  this.sky = this.add.rectangle(0, 0, WIDTH, HEIGHT, 0x87CEEB).setOrigin(0, 0);
  this.belowGround = this.add.rectangle(0, HEIGHT / 1.5, WIDTH, HEIGHT / 3, 0xD2B48C).setOrigin(0, 0);
  ...
}

The sky will be a blue rectangle (0x87CEEB) spanning the entire screen, positioned at the top (setOrigin(0, 0)), while the ground will be made of a brown rectangle (0xD2B48C) occupying the lower third of the screen, starting at half the screen’s height.

The next step involves displaying the mountains by introducing the createMountains function and integrating it into the main create function. These mountains will serve as background elements to enhance the visual appeal and depth of the game environment. The function will position the mountains randomly along the x-axis and consistently along the y-axis, while also scaling them variably, contributing to a dynamic and immersive gaming experience:

function createMountains() {
  this.mountains = this.add.group();
  for (let i = 0; i < 3; i++) {
    const x = Phaser.Math.Between(0, WIDTH);
    const y = HEIGHT / 1.5;
    const scale = Phaser.Math.FloatBetween(1.0, 4.0);
    this.mountains.add(this.add.image(x, y, 'mountain').setScale(scale).setOrigin(0, 1));
  }
}
  • Group creation: The function begins by creating a new group to hold all the mountain images. This allows for easier management and manipulation of the mountains as a single entity.
  • Mountain positioning and scaling:
    • Random x position: Each mountain is positioned at a random location along the x-axis (Phaser.Math.Between(0, WIDTH)), which ensures that the mountains are spread across the screen.
    • Fixed y position: All mountains are placed at a fixed y-coordinate (HEIGHT / 1.5), which situates them in the lower half of the screen, simulating a horizon.
    • Random scaling: Each mountain is assigned a random scale (Phaser.Math.FloatBetween(1.0, 4.0)), adding visual diversity and depth to the scene. Larger mountains appear closer to the player, while smaller ones seem further away.
  • Image Addition: The mountains are added to the group with their respective positions and scales, and their origin is set to the bottom-left corner (setOrigin(0, 1)), ensuring they appear grounded at the correct height.

The last change inside the create.js file will consist of integrating createMountains into the main create function:

function create() {
  ...
  createWorld.call(this);
  createClouds.call(this);
  createMountains.call(this);
  ...
}

The order of the functions calls (createWorld, createClouds, and createMountains) is crucial for maintaining a coherent and visually appealing game environment. Each step builds upon the previous one, ensuring that all elements are correctly layered and contribute to a dynamic and immersive game world. By establishing the foundation first, enhancing the background second, and adding depth third, the game achieves a realistic and engaging visual structure that enhances the player’s experience:

  • createWorld: Sets up the basic game world, including the sky, ground, and obstacle framework. This is the first step because it establishes the fundamental structure of the game. The sky and ground serve as the canvas upon which all other game elements are placed. Without this setup, there would be no context for the other elements to exist within.
  • createClouds: Adds clouds to the sky. Adding clouds after the basic world setup ensures that they are correctly layered above the sky but below the ground and obstacles. This layering is essential for maintaining the visual hierarchy and realism in the game. Clouds enhance the background without interfering with the main gameplay elements.
  • createMountains: This new function call integrates the mountains into the game, adding visual depth and enhancing the background scenery. Integrating mountains after the clouds ensures that they are correctly layered. Mountains should appear behind the ground but in front of the sky and clouds. This order maintains the natural layering one would expect in a real-world scene. Mountains add depth to the environment, making the game world feel more expansive and visually rich.

File contents

// Score text on the right side of the screen
function createScore() {
  this.scoreText = this.add.text(WIDTH - 25, 25, "", TEXT_STYLE).setOrigin(1, 0);
  this.highScoreText = this.add.text(0, 25, "", TEXT_STYLE).setOrigin(1, 0).setAlpha(0.75);
}

// Five clouds at random positions
function createClouds() {
  this.clouds = this.add.group();
  for (let i = 0; i < 5; i++) {
    const x = Phaser.Math.Between(0, WIDTH);
    const y = Phaser.Math.Between(0, HEIGHT / 2);
    const scale = Phaser.Math.FloatBetween(1.0, 3.0);
    this.clouds.add(this.add.image(x, y, 'cloud').setScale(scale).setOrigin(0, 0));
  }
}

// Three mountains at random X positions
function createMountains() {
  this.mountains = this.add.group();
  for (let i = 0; i < 3; i++) {
    const x = Phaser.Math.Between(0, WIDTH);
    const y = HEIGHT / 1.5;
    const scale = Phaser.Math.FloatBetween(1.0, 4.0);
    this.mountains.add(this.add.image(x, y, 'mountain').setScale(scale).setOrigin(0, 1));
  }
}

// Dino starts running and the the ground is created
function createGround() {
  this.startText.destroy();
  this.dino.setVelocityX(100);
  this.dino.play('dino-run', 1);

  const interval = setInterval(() => {
    this.ground.width += (WIDTH / 100);
    if (this.ground.width >= WIDTH) {
      clearInterval(interval);
      this.dino.setVelocityX(0);
      this.scoreText.setAlpha(1);
      this.isGameRunning = true;
    }
  }, 50);
}

// Dino waiting animation, keyboard initilization and starting text
function createDino() {
  this.dino = this.physics.add.sprite(0, HEIGHT / 1.5 - 16, 'dino-idle');
  this.dino.setSize(50, 75).setGravityY(5000).setOrigin(0, 1);
  this.dino.play('dino-waiting', true);

  this.physics.add.collider(this.dino, this.belowGround);
  this.physics.add.collider(this.dino, this.obsticles, () => stopGame.call(this));

  this.cursors = this.input.keyboard.createCursorKeys();
  this.spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

  this.startText = this.add.text(WIDTH - 15, HEIGHT / 1.5, "Press to play", TEXT_STYLE).setOrigin(1);
}

// Game over screen and pause until player starts the game again
function createGameOverScreen() {
  this.gameOverShadow = this.add.image(2, 2, 'game-over').setTintFill(0xFFFFFF);
  this.gameOverText = this.add.image(0, 0, 'game-over');
  
  this.restart = this.add.image(0, 50, 'restart').setInteractive();
  this.gameOverScreen = this.add.container(WIDTH / 2, HEIGHT / 2 - 50);
  this.gameOverScreen.add([this.gameOverShadow, this.gameOverText, this.restart]);

  this.input.keyboard.on('keydown_ENTER', () => startGame.call(this));
  this.restart.on('pointerdown', () => startGame.call(this));
}

// Define the physical ground and obsticles group
function createWorld() {
  this.sky = this.add.rectangle(0, 0, WIDTH, HEIGHT, 0x87CEEB).setOrigin(0, 0);
  this.belowGround = this.add.rectangle(0, HEIGHT / 1.5, WIDTH, HEIGHT / 3, 0xD2B48C).setOrigin(0, 0);
  this.ground = this.add.tileSprite(0, HEIGHT / 1.5, 88, 16, 'ground').setOrigin(0, 0);
  this.physics.add.existing(this.belowGround);
  this.belowGround.body.setImmovable();
  this.obsticles = this.physics.add.group();
  this.eggs = this.physics.add.group();
}

// Create all the elements in the game and initilize the score
function create() {
  initVariables.call(this);
  initSounds.call(this);
  createAnims.call(this);
  createWorld.call(this);
  createClouds.call(this);
  createMountains.call(this);
  createScore.call(this);
  createDino.call(this);

  setInterval(() => updateScore.call(this), 100);
}

Adding the eggs and moving all the items (update.js)

The update.js file has been expanded to include new game mechanics such as handling eggs and dynamically moving items across the screen at different speeds.

Changes

We will use a new function placeEgg that will randomly position eggs on the screen:

function placeEgg() {
  const x = Phaser.Math.Between(WIDTH, WIDTH * 2);
  const y = Phaser.Math.Between(HEIGHT / 1.5 + 50, HEIGHT);
  const scale = (y / HEIGHT) * 1.5; // The closer to the bottom of the screen, the bigger the egg
  this.eggs.add(this.add.image(x, y, 'eggs').setScale(scale).setOrigin(0, 1));
}
  • x is set to a random position between the current screen width and twice the width, ensuring eggs appear off-screen to the right initially.
  • y is a random position between just below the ground level and the bottom of the screen.
  • The scale of the egg depends on its y position: eggs closer to the bottom are scaled larger.
  • Adds the egg image to the eggs group with the specified scale and origin, in order to move all the eggs at the same time when the game is running.

Because we will have more items on the screen to be moved, we will use a new function moveItems to handle the movement of each group of items (like clouds, obstacles, or eggs) across the screen:

function moveItems(items, speed, destroy) {
  items.getChildren().forEach(item => {
    item.x -= speed;
    if (item.getBounds().right < 0) {
      if (destroy) item.destroy();
      else item.x = WIDTH;
    }
  });
}
  • items.getChildren().forEach(item => {...}) iterates through each item in the group.
  • Each item’s x position is decremented by speed, making it move to the left.
  • If an item moves off-screen (its right boundary is less than 0), it either gets destroyed (if destroy is true) or is repositioned to the right edge of the screen (if destroy is false).

The update function is responsible for continuously updating the screen while the game is running. It displays the eggs at the same time obstacles appear and moves all items (clouds, mountains, obstacles, and eggs) at different speeds to create a sense of depth. This makes items appear closer or farther away from the player, enhancing the gameplay experience:

  • Displaying Eggs and Obstacles: This function will control the timing of when eggs and obstacles appear on the screen. Both will be placed at intervals to create challenges for the player.
  • Moving Items at Different Speeds: The function will move different items (clouds, mountains, obstacles, and eggs) across the screen at varying speeds. This will create a parallax effect, making items appear closer or farther away from the player based on their speed. For instance, clouds might move slowly to appear in the distance, while obstacles and eggs will move faster to simulate being on the ground.
function update() {
    if (this.respawnTime >= OBSTICLE_TIME) {
      ...
      placeEgg.call(this);
    }

    moveItems(this.clouds, this.gameSpeed / 20, false);
    moveItems(this.mountains, this.gameSpeed / 10, false);
    moveItems(this.obsticles, this.gameSpeed, true);
    moveItems(this.eggs, this.gameSpeed, true);
    ...
}

The code above will create a sense of depth and immersion by simply adjusting the speeds at which different items move:

  • The respawnTime variable determines when new obstacles and eggs are placed on the screen. When respawnTime reaches a certain threshold (e.g., 750), the function calls placeObsticle and placeEgg to add new items to the game.
  • Moving Items:
    • Clouds: Moved slowly (e.g., cloud.x -= 0.5) to give a distant background effect.
    • Mountains: Moved at a moderate speed to appear mid-distance.
    • Obstacles and Eggs: Moved at the game speed (this.gameSpeed) to appear on the ground and provide immediate challenges for the player.

File contents

// Restart the game after dino is hurt
function startGame() {
  this.dino.setVelocityY(0);
  this.physics.resume();
  this.anims.resumeAll();
  this.obsticles.clear(true, true);
  this.gameOverScreen.destroy();
  this.isGameRunning = true;
}

// On game over a sound is played and all physics are paused
// The game over screen will be shown and mobile devices will vibrate
function stopGame() {
  initVariables.call(this);
  updateHighScore.call(this);
  this.hitSound.play();
  this.physics.pause();
  this.anims.pauseAll();
  this.dino.setTexture('dino-hurt');
  createGameOverScreen.call(this);
  if (window.navigator.vibrate) window.navigator.vibrate(50);
}

// Update the score and increase the game speed
function updateScore() {
  if (!this.isGameRunning) return;

  this.score++;
  this.gameSpeed += 0.01;

  if (this.score % 100 == 0) {
    this.reachSound.play();

    this.tweens.add({
      targets: this.scoreText,
      duration: 100,
      repeat: 3,
      alpha: 0,
      yoyo: true
    })
  }

  this.scoreText.setText(("0000" + this.score).slice(-5));
}

// Update the high score in case is higher than the previous one
function updateHighScore() {
  this.highScoreText.x = this.scoreText.x - this.scoreText.width - 30;

  const highScore = this.highScoreText.text.substr(this.highScoreText.text.length - 5);
  const newScore = Number(this.scoreText.text) > Number(highScore) ? this.scoreText.text : highScore;

  this.highScoreText.setText(`HI ${newScore}`);
}

// Place a new obsticle on the screen (either cactuses or birds)
function placeObsticle() {
  let obsticle;
  const obsticleNum = Phaser.Math.Between(1, 7);

  if (obsticleNum > 6) {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5 - Phaser.Math.Between(20, 50), 'enemy-bird');
    obsticle.body.height /= 1.5
    obsticle.play('enemy-bird-anim', 1);
  } else {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5 + 16, `obsticle-${obsticleNum}`)
  }

  obsticle.setOrigin(0, 1).setImmovable();
}

// Three eggs hidden at the beginning
function placeEgg() {
  const x = Phaser.Math.Between(WIDTH, WIDTH * 2);
  const y = Phaser.Math.Between(HEIGHT / 1.5 + 50, HEIGHT);
  const scale = (y / HEIGHT) * 1.5; // The closest to the bottom of the screen, the bigger the egg
  this.eggs.add(this.add.image(x, y, 'eggs').setScale(scale).setOrigin(0, 1));
}

// Move all the images on the screen belonging to a specific group of items
function moveItems(items, speed, destroy) {
  items.getChildren().forEach(item => {
    item.x -= speed;
    if (item.getBounds().right < 0) {
      if (destroy) item.destroy();
      else item.x = WIDTH;
    }
  })
}

// Update the screen (obsticles, ground, clouds and dino) and check the keyboard and mouse (or touch screen) continuously
// Dino will start running and jump if the user taps the screen or presses either the arrow up key or the spacebar  
function update() {
  let jump = this.cursors.up.isDown || this.spaceBar.isDown || this.input.activePointer.isDown;

  if (this.isGameRunning) {
    this.ground.tilePositionX += this.gameSpeed;

    this.respawnTime += this.gameSpeed;
    if (this.respawnTime >= OBSTICLE_TIME) {
      this.respawnTime = 0;
      placeObsticle.call(this);
      placeEgg.call(this);
    }

    moveItems(this.clouds, this.gameSpeed / 20, false);
    moveItems(this.mountains, this.gameSpeed / 10, false);
    moveItems(this.obsticles, this.gameSpeed, true);
    moveItems(this.eggs, this.gameSpeed, true);

    if (this.dino.body.onFloor()) {
      if (jump) {
        this.jumpSound.play();
        this.dino.anims.stop();
        this.dino.setVelocityY(-1600);
        this.dino.setTexture('dino', 0);
      }
      else {
        this.dino.play('dino-run', true);
      }
    }
  }
  else if (jump && (this.ground.width < WIDTH)) {
    createGround.call(this);
  }
}

Proposed exercise: Using your own assets

Create your own improved version of the Dino game using your own assets (images, sprites and sounds). You can also change any other things you like.

Do not forget to create all the files required to run the code in this unit:

  • styles.css
  • index.html
  • preload.js
  • create.js
  • update.js
  • config.js
  • assets folder

You may find many free assets in OpenGameArt, and some new versions of the Dino game in github:

Enjoy the game!

You can enjoy playing this wonderful game online here.

Phaser. Unit 12. Super Mario Bros recreation with random level generation

Introduction

Super Mario Phaser is a modern web-based recreation of the classic Super Mario Bros game, built using the Phaser framework. This project is designed to bring the beloved nostalgic experience of the original game to today’s web browsers, allowing players to enjoy it directly online.

One of the standout features of Super Mario Phaser is its random level generation. This means that every time you play, the levels are different, providing a fresh and challenging experience with each playthrough. This dynamic element ensures that the game remains exciting and unpredictable, offering new adventures and obstacles for players to overcome every time they start a new game.

Additionally, we will provide options to customize the control keys, allowing players to configure their preferred inputs. We will also include settings to adjust or mute the sound, as well as control the volume of both music and sound effects, enhancing the overall gaming experience.

For more information about the original project, you can visit its GitHub repository.

Level generation

In this unit, we will create a simplified version of the original Super Mario Phaser project. Our goal is to enable easy customization of level generation, even for those without extensive programming knowledge.

To achieve this, we will use a matrix template where each cell specifies a type of block. By implementing a nested loop, we can efficiently render all the cells in the matrix, generating the level layout. This approach allows for straightforward modification and understanding of how different blocks are placed within the game environment.

For example, if we want to build a platform consisting of several blocks with some coins, we can use a matrix to define the layout. Each cell will represent a specific element (‘B’ stands for ‘block’ and ‘C’ stands for ‘coin’). Here is an example of how the matrix might look:

[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', 'C', 'C', 'C', 'C', 'C', 'C', ' ', ' '],
[' ', ' ', 'B', 'B', 'B', 'B', 'B', 'B', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],

In this matrix, we have a design where coins are positioned directly above a row of blocks. Specifically, the fifth row of the matrix places coins in the middle columns, while the sixth row directly beneath it contains a continuous platform of blocks. This layout allows for a straightforward and clear way to design and visualize the level structure. The empty spaces around the blocks and coins indicate that these elements are floating in the air, creating a typical platforming challenge for the player.

The assets

/assets/scenery/sign.png
/assets/others/loading.gif
/assets/entities/mario.png
/assets/entities/mario-grown.png
/assets/entities/mario-fire.png
/assets/scenery/overworld/cloud1.png
/assets/scenery/castle.png
/assets/scenery/overworld/bush1.png
/assets/scenery/overworld/fence.png
/assets/scenery/overworld/floor-bricks.png
/assets/scenery/underground/floor-bricks.png
/assets/entities/overworld/goomba.png
La imagen tiene un atributo ALT vacío; su nombre de archivo es goomba.png
/assets/entities/underground/goomba.png
/assets/blocks/overworld/mistery-block.png
/assets/blocks/underground/mistery-block.png
/assets/collectibles/overworld/fire-flower.png
/assets/collectibles/underground/fire-flower.png
/assets/collectibles/coin.png
/assets/collectibles/underground/ground-coin.png

Loading the assets (js/main/preload.js)

The provided code inside the “preload.js” file includes functions for preloading assets, initializing sounds, and creating animations.

The preload function is responsible for loading all necessary game assets before the game starts. It sets up a visual progress bar and percentage text to inform the user about the loading progress. Various assets such as images, spritesheets, fonts, and sound files are loaded. The function also includes logic to determine the style of the level (overworld or underground) randomly and loads the appropriate assets accordingly. Additionally, it loads several plugins required for the game.

The initSounds function initializes the game’s audio elements. It iterates over a list of sound assets, creates audio objects for each sound, and assigns them to the game instance. This function ensures that all sounds are ready to be played during the game, with background music starting to loop once the sounds are initialized.

The createAnimations function defines various animations for the game’s characters and objects. It creates both single-frame and multi-frame animations using the loaded spritesheets. Single-frame animations are static poses, while multi-frame animations include movements like running, walking, and item interactions. These animations are essential for bringing the game characters and elements to life, providing visual feedback and enhancing the gaming experience.

Overall, the following code prepares the game environment by loading all assets, initializing audio, and setting up animations:

function preload() {
    // Setup progress box, bar, and percent text
    const progressBox = this.add.graphics().fillStyle(0x222222, 1)
        .fillRoundedRect(screenWidth / 2.48, screenHeight / 2 * 1.05, screenWidth / 5.3, screenHeight / 20.7, 10);
    const progressBar = this.add.graphics();
    const percentText = this.make.text({
        x: this.cameras.main.width / 2,
        y: this.cameras.main.height / 2 * 1.25,
        text: '0%',
        style: {
            font: `${screenWidth / 96}px pixel_nums`,
            fill: '#ffffff'
        }
    }).setOrigin(0.5, 0.5);

    // Update progress bar and text on progress
    this.load.on('progress', (value) => {
        percentText.setText(value * 99 >= 99 ? 'Generating world...' : `Loading... ${parseInt(value * 99)}%`);
        progressBar.clear().fillStyle(0xffffff, 1)
            .fillRoundedRect(screenWidth / 2.45, screenHeight / 2 * 1.07, screenWidth / 5.6 * value, screenHeight / 34.5, 5);
    });

    // Clean up on load complete
    this.load.on('complete', () => {
        [progressBar, progressBox, percentText].forEach(item => item.destroy());
        loadingGif.forEach(gif => gif.style.display = 'none');
    });

    // Load Fonts and Plugins
    this.load.bitmapFont('carrier_command', 'assets/fonts/carrier_command.png', 'assets/fonts/carrier_command.xml');
    const plugins = ['rexvirtualjoystickplugin', 'rexcheckboxplugin', 'rexsliderplugin', 'rexkawaseblurpipelineplugin'];
    plugins.forEach(plugin => {
        this.load.plugin(plugin, `https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/${plugin}.min.js`, true);
    });

    // Determine level style
    isLevelOverworld = Phaser.Math.Between(0, 100) <= 50;
    const levelStyle = isLevelOverworld ? 'overworld' : 'underground';

    // Utility functions for loading assets
    const loadSpriteSheet = (path, frameWidth, frameHeight) => {
        this.load.spritesheet(path.split('/').pop(), `assets/${path}.png`, { frameWidth, frameHeight });
    };
    const loadImage = (path) => {
        this.load.image(path.split('/').pop(), `assets/${path}.png`);
    };

    // Load entity sprites
    [{ path: 'mario', frameWidth: 18, frameHeight: 16 },
    { path: 'mario-grown', frameWidth: 18, frameHeight: 32 },
    { path: 'mario-fire', frameWidth: 18, frameHeight: 32 },
    { path: `${levelStyle}/goomba`, frameWidth: 16, frameHeight: 16 },
    { path: 'koopa', frameWidth: 16, frameHeight: 24 },
    { path: 'shell', frameWidth: 16, frameHeight: 15 },
    { path: 'fireball', frameWidth: 8, frameHeight: 8 },
    { path: 'fireball-explosion', frameWidth: 16, frameHeight: 16 }]
        .forEach(item => loadSpriteSheet(`entities/${item.path}`, item.frameWidth, item.frameHeight));

    // Load collectibles
    [{ path: 'coin', frameWidth: 16, frameHeight: 16 },
    { path: 'underground/ground-coin', frameWidth: 10, frameHeight: 14 },
    { path: `${levelStyle}/fire-flower`, frameWidth: 16, frameHeight: 16 }]
        .forEach(item => loadSpriteSheet(`collectibles/${item.path}`, item.frameWidth, item.frameHeight));

    // Load animated blocks
    [{ path: `${levelStyle}/brick-debris`, frameWidth: 8, frameHeight: 8 },
    { path: `${levelStyle}/mistery-block`, frameWidth: 16, frameHeight: 16 },
    { path: 'overworld/custom-block', frameWidth: 16, frameHeight: 16 }]
        .forEach(item => loadSpriteSheet(`blocks/${item.path}`, item.frameWidth, item.frameHeight));

    // Load animated hud
    [{ path: 'npc', frameWidth: 16, frameHeight: 24 }]
        .forEach(hud => loadSpriteSheet(`hud/${hud.path}`, hud.frameWidth, hud.frameHeight));

    // Load mushrooms
    ['live', 'super']
        .forEach(item => loadImage(`collectibles/${item}-mushroom`));

    // Load normal scenery
    ['castle', 'flag-mast', 'final-flag', 'sign']
        .forEach(item => loadImage(`scenery/${item}`));

    // Load overworld scenery
    ['cloud1', 'cloud2', 'mountain1', 'mountain2', 'fence', 'bush1', 'bush2']
        .forEach(item => loadImage(`scenery/overworld/${item}`));

    // Load specific scenery
    ['floor-bricks']
        .forEach(item => loadImage(`scenery/${levelStyle}/${item}`));
    ['start-floor-bricks']
        .forEach(item => loadImage(`scenery/overworld/${item}`));

    // Load tubes
    ['horizontal', 'horizontal-final', 'vertical-small', 'vertical-medium', 'vertical-large', 'vertical-extra-large']
        .forEach(item => loadImage(`scenery/${item}-tube`));

    // Load HUD
    ['gear', 'settings-bubble']
        .forEach(item => loadImage(`hud/${item}`));

    // Load static blocks
    ['block', 'empty-block', 'immovable-block']
        .forEach(item => loadImage(`blocks/${levelStyle}/${item}`));
    ['construction-block']
        .forEach(item => loadImage(`blocks/underground/${item}`));
    

    // Load sounds (music and effects)
    this.sounds = [
        { path: 'music/overworld/overworld.mp3', volume: 0.15 },
        { path: 'music/underground/underground.mp3', volume: 0.15 },
        { path: `music/${levelStyle}/hurry-up.mp3`, volume: 0.15 },
        { path: 'music/game-over.mp3', volume: 0.3 },
        { path: 'music/win.wav', volume: 0.3 },
        { path: 'effects/jump.mp3', volume: 0.1 },
        { path: 'effects/coin.mp3', volume: 0.2 },
        { path: 'effects/power-up-appears.mp3', volume: 0.2 },
        { path: 'effects/consume-power-up.mp3', volume: 0.2 },
        { path: 'effects/power-down.mp3', volume: 0.3 },
        { path: 'effects/goomba-stomp.wav', volume: 1 },
        { path: 'effects/flagpole.mp3', volume: 0.3 },
        { path: 'effects/fireball.mp3', volume: 0.3 },
        { path: 'effects/kick.mp3', volume: 0.3 },
        { path: 'effects/time-warning.mp3', volume: 0.2 },
        { path: 'effects/pause.wav', volume: 0.17 },
        { path: 'effects/block-bump.wav', volume: 0.3 },
        { path: 'effects/break-block.wav', volume: 0.5 },
        { path: Phaser.Math.Between(0, 100) < 98 ? 'effects/here-we-go.mp3' : 'effects/cursed/here-we-go.mp3', volume: 0.17 }
    ];

    const generateKey = path => path.split('/').pop().split('.')[0].replace(/-./g, match => match.charAt(1).toUpperCase()) + 'Sound';
    this.sounds.forEach(sound => {
        sound.key = generateKey(sound.path);
        this.load.audio(sound.key, `assets/sound/${sound.path}`);
    });
}

function initSounds() {
    this.sounds.forEach(sound => {
        this[sound.key] = this.sound.add(sound.key, { volume: sound.volume });
    });
    this.overworldSound.play({ loop: -1 });
}

function createAnimations() {
    const singleFrameAnimations = [
        { key: 'idle', target: 'mario', frame: 0 },
        { key: 'hurt', target: 'mario', frame: 4 },
        { key: 'jump', target: 'mario', frame: 5 },
        { key: 'grown-mario-idle', target: 'mario-grown', frame: 0 },
        { key: 'grown-mario-crouch', target: 'mario-grown', frame: 4 },
        { key: 'grown-mario-jump', target: 'mario-grown', frame: 5 },
        { key: 'fire-mario-idle', target: 'mario-fire', frame: 0 },
        { key: 'fire-mario-crouch', target: 'mario-fire', frame: 4 },
        { key: 'fire-mario-jump', target: 'mario-fire', frame: 5 },
        { key: 'fire-mario-throw', target: 'mario-fire', frame: 6 },
        { key: 'goomba-idle', target: 'goomba', frame: 1 },
        { key: 'goomba-hurt', target: 'goomba', frame: 2 },
        { key: 'koopa-idle', target: 'koopa', frame: 1 },
        { key: 'koopa-hurt', target: 'koopa', frame: 0 },
        { key: 'koopa-shell', target: 'koopa', frame: 1 },
        { key: 'fireball-left-down', target: 'fireball', frame: 0 },
        { key: 'fireball-left-up', target: 'fireball', frame: 1 },
        { key: 'fireball-right-down', target: 'fireball', frame: 2 },
        { key: 'fireball-right-up', target: 'fireball', frame: 3 },
        { key: 'fireball-explosion-1', target: 'fireball-explosion', frame: 0 },
        { key: 'fireball-explosion-2', target: 'fireball-explosion', frame: 1 },
        { key: 'fireball-explosion-3', target: 'fireball-explosion', frame: 2 },
    ];

    const multiFrameAnimations = [
        { key: 'run', target: 'mario', start: 3, end: 1, frameRate: 12, repeat: -1 },
        { key: 'grown-mario-run', target: 'mario-grown', start: 3, end: 1, frameRate: 12, repeat: -1 },
        { key: 'fire-mario-run', target: 'mario-fire', start: 3, end: 1, frameRate: 12, repeat: -1 },
        { key: 'goomba-walk', target: 'goomba', start: 0, end: 1, frameRate: 8, repeat: -1 },
        { key: 'koopa-walk', target: 'koopa', start: 0, end: 1, frameRate: 8, repeat: -1 },
        { key: 'coin-default', target: 'coin', start: 0, end: 3, frameRate: 10, repeat: -1 },
        { key: 'ground-coin-default', target: 'ground-coin', start: 2, end: 0, frameRate: 5, repeat: -1, repeatDelay: 5 },
        { key: 'mistery-block-default', target: 'mistery-block', start: 2, end: 0, frameRate: 5, repeat: -1, repeatDelay: 5 },
        { key: 'custom-block-default', target: 'custom-block', start: 2, end: 0, frameRate: 5, repeat: -1, repeatDelay: 5 },
        { key: 'brick-debris-default', target: 'brick-debris', start: 0, end: 3, frameRate: 4, repeat: -1 },
        { key: 'fire-flower-default', target: 'fire-flower', start: 0, end: 3, frameRate: 10, repeat: -1 },
        { key: 'npc-default', target: 'npc', start: 0, end: 1, frameRate: 2, repeat: -1, repeatDelay: 10 },
    ];

    singleFrameAnimations.forEach(({ key, target, frame }) => {
        this.anims.create({
            key,
            frames: [{ key: target, frame }]
        });
    });

    multiFrameAnimations.forEach(({ key, target, start, end, frameRate, repeat, repeatDelay = 0 }) => {
        this.anims.create({
            key,
            frames: this.anims.generateFrameNumbers(target, { start, end }),
            frameRate,
            repeat,
            repeatDelay
        });
    });
}

Drawing the blocks and coins (js/game/structures.js)

The function generateStructure is designed to create and display different elements (blocks, coins, etc.) on the screen based on a predefined matrix layout. The key steps and components are as follows:

  1. Element addition functions: Several helper functions (addBlock, addConstructionBlock, addImmovableBlock, addMisteryBlock, addCoin) are defined to add different types of game elements (blocks, mystery blocks, immovable blocks, coins) to their respective groups and scale them appropriately.
  2. Level layouts: The levels object contains predefined matrices for different types of levels (overworld and underworld). Each matrix defines the placement of blocks (‘B’), mystery blocks (‘M’), immovable blocks (‘I’), coins (‘C’), and construction blocks (‘X’).
  3. Random level selection: A random matrix is selected from the levels object based on whether the level is overworld or underworld.
  4. Drawing the matrix: The drawMatrix function iterates over the selected matrix, calculates the position for each element, and uses the helper functions to add the elements to the screen.

In the following source code, each matrix is used to define the layout of game elements in a clear and structured way, and the function dynamically generates the game level based on these definitions:

function generateStructure(pieceStart) {
    const scale = screenHeight / 345;

    const addBlock = (x, y) =>
        this.blocksGroup.add(this.add.image(x, y, 'block').setScale(scale));

    const addConstructionBlock = (x, y) =>
        this.constructionBlocksGroup.add(this.add.image(x, y, 'construction-block').setScale(scale));

    const addImmovableBlock = (x, y) =>
        this.immovableBlocksGroup.add(this.add.image(x, y, 'immovable-block').setScale(scale));

    const addMisteryBlock = (x, y) =>
        this.misteryBlocksGroup.add(this.add.sprite(x, y, 'mistery-block').setScale(scale));

    const addCoin = (x, y) =>
        this.groundCoinsGroup.add(this.physics.add.sprite(x, y, 'ground-coin').setScale(scale));

    const levels = {
        overworld: [
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'B', 'M', 'B', ' ', ' ', ' ', ' ', ' ', 'B', 'M', 'B', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', 'B', 'B', 'M', 'B', 'B', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', 'B', 'M', 'B', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'B', 'M', 'B', ' ', ' ', ' ', 'B', 'M', 'B', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', 'M', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', 'B', 'M', 'B', 'M', 'B', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', 'B', 'M', 'M', 'B', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', 'B', 'B', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', 'M', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'M', ' ', ' ', 'M', ' ', ' ', 'M', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', 'B', 'B', 'M', 'B', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
        ],
        underworld: [
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'M', 'M', 'M', 'M', 'M', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', 'I', ' ', ' ', ' ', 'M'],
                [' ', ' ', ' ', ' ', 'I', ' ', 'I', ' ', 'I', ' ', ' '],
                [' ', ' ', 'I', ' ', 'I', ' ', 'I', ' ', 'I', ' ', ' '],
                ['I', ' ', 'I', ' ', 'I', ' ', 'I', ' ', 'I', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', 'C', 'C', 'C', 'C', ' ', ' ', ' '], 
                [' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' '], 
                [' ', ' ', 'C', 'X', ' ', ' ', 'X', 'C', 'X', ' '], 
                [' ', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
            ],
            [
                [' ', ' ', 'M', 'M', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', 'M', 'M', ' ', ' ', 'M', 'M', 'M', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', 'C', 'C', 'C', 'C', ' ', ' ', ' '],
                [' ', ' ', ' ', 'B', 'B', 'B', 'B', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', ' '],
                [' ', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
        ]
    };

    const random = Phaser.Math.Between(0, 5);
    const getMatrix = () => isLevelOverworld ? levels.overworld[random] : levels.underworld[random];
    
    const drawMatrix = (matrix) => {
        const blockSize = 16 * scale;
        matrix.forEach((row, rowIndex) => {
            const y = (screenHeight - platformHeight) + (rowIndex * blockSize) - (8.5 * blockSize);
            row.forEach((cell, colIndex) => {
                const x = pieceStart + (colIndex * blockSize) - (4.5 * blockSize);
                
                if (cell === 'B') addBlock(x, y);
                if (cell === 'M') addMisteryBlock(x, y);
                if (cell === 'I') addImmovableBlock(x, y);
                if (cell === 'C') addCoin(x, y);
                if (cell === 'X') addConstructionBlock(x, y);
            });
        });
    };

    const matrix = getMatrix();
    drawMatrix(matrix);

    return isLevelOverworld ? Phaser.Math.Between(1, 3) : 1;
}

Download the source code and the assets

Inside this ZIP file, you will find all the files containing the source code and all the necessary assets to run the game on your own. The structure of the ZIP file is organized as follows:

  • index.html: This is the main HTML file that serves as the entry point for the game.
  • css: This folder includes the CSS file used for styling the HTML elements of the game interface.
  • js: This folder contains all the JavaScript files that comprise the game’s source code, including:
    • main/preload.js: Handles the loading of all game assets, such as images and sprite sheets.
    • game/structures.js: Defines the structure and layout of the game levels.
    • Other JavaScript files that implement the game logic and mechanics.
  • assets: This folder contains all the graphical and audio assets needed for the game, such as images, sprite sheets, and sound files.

This organization ensures that you have a clear structure for all components of the game, making it easy to understand, modify, and run the game on your own system.

Proposed exercise: Customizing the Mario character

You may easily customize the Mario character by simply overwriting the images and defining the new sprite sizes (if changed). You need to update the relevant parts of the preload.js file (under the js/main folder). The source code remains mostly the same unless you need to adjust frame-specific size and animations, which might require changes based on your custom sprite sheet. Here are the specific steps you need to follow:

  1. Update the images: Change the following images. You may replace them by a new ones, or you can update the ones provided using Pixelorama:
  • /assets/entities/mario.png
  • /assets/entities/mario-grown.png
  • /assets/entities/mario-fire.png
  1. Update frame dimensions inside preload.js file: Replace the width and height of the Mario sprites with the new frame dimensions (if changed):
// Load entity sprites
[{ path: 'mario', frameWidth: 18, frameHeight: 16 },
 { path: 'mario-grown', frameWidth: 18, frameHeight: 32 },
 { path: 'mario-fire', frameWidth: 18, frameHeight: 32 },
 ...
  1. Update animations inside preload.js file: Ensure that the animation definitions use the new sprites correctly. You may change the start and end frames, and also the frameRate:
const multiFrameAnimations = [
   { key: 'run', target: 'mario', start: 3, end: 1, frameRate: 12, repeat: -1 },
   { key: 'grown-mario-run', target: 'mario-grown', start: 3, end: 1, frameRate: 12, repeat: -1 },
   { key: 'fire-mario-run', target: 'mario-fire', start: 3, end: 1, frameRate: 12, repeat: -1 },
   ...

By updating these lines, you can ensure that your custom Mario sprite is loaded with the new dimensions and integrated into the game’s existing animations.

Proposed exercise: Defining your own levels

To define your own levels, you need to update the structures.js file (under the js/game folder) to include your custom level designs. Here are the steps and the specific lines of code to add new levels or update the existing ones:

  1. Define new levels: Add new level arrays to the levels.overworld and levels.underworld arrays.
  2. Adjust block types: Use ‘B’ for normal blocks, ‘M’ for mystery blocks, ‘I’ for immovable blocks, ‘C’ for coins, and ‘X’ for construction blocks.
  3. Update random selector: If you add more than six levels, adjust the Phaser.Math.Between(0, 5) to match the number of levels you have.

This is the code you should modify inside the structures.js file:

function generateStructure(pieceStart) {
    // Existing code...

    const levels = {
        overworld: [
            // Existing level definitions
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', 'C', 'M', 'C', ' ', ' ', ' ', ' ', ' ', 'B', 'M', 'B', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', 'B', 'B', 'M', 'B', 'B', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            // Add more custom levels as needed...
        ],
        underworld: [
            // Existing level definitions
            [
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', 'C', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', 'M', 'B', 'M', ' ', 'M', 'B', 'M', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
                [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ],
            // Add more custom levels as needed...
        ]
    };

    // Existing code to select and draw the level
    const random = Phaser.Math.Between(0, 5);
    ...
}

By following these instructions, you can easily define and customize your own levels in the Mario game.

Enjoy the game!

You can enjoy playing this game online here.

Phaser. Unit 11. Emoji generator

Introduction

The main goal of this unit is to provide a fun and simple game where users can add and see their favorite emojis on the screen interacting with them in a dynamic way. These will be the main features:

  1. Emoji Selection: Players can choose from different categories of emojis (like smileys, animals, food, etc.).
  2. Adding Emojis: By clicking on the screen, players can place random emojis from the selected category onto the game area.
  3. Interactive Movement: The emojis have physics applied to them, meaning they can bounce around the screen and respond to movements.
  4. Device Motion Interaction: If played on a device with motion sensors (like a smartphone), the game can respond to tilts and shakes, making the emojis move accordingly.

CSS code (style.css)

The CSS file styles the web page for the game as follows:

  • Full-Screen Game Display (html, body): Sets the width and height to 100%, removes any margins and padding, hides scrollbars, sets the font to Arial, and the text color to white.
  • Top-Positioned Elements: (#reload, #numEmojis, #categorySelect): Positions the reload button, emoji counter, and category dropdown at the top of the page with a small top margin.
  • Specific Element Positioning:
    • #reload: Places the reload button at the top left.
    • #numEmojis: Centers the emoji counter horizontally at the top.
    • #categorySelect: Positions the category dropdown at the top right.
  • Centered Instructions (#instructions): Centers the instructions text both horizontally and vertically in the middle of the screen.

This CSS code ensures the game takes up the entire screen, with the controls easily accessible at the top and the instructions clearly visible in the center:

/* Remove any margin and padding and disable scrollbars to fill the screen with the game */
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
  font-family: arial;
  color: white;
}

/* The reload button, the counter and the dropdown are positioned at the top */
#reload, #numEmojis, #categorySelect {
  position: absolute;
  top: 5px;
  padding: 2px 4px; /* Add some padding for better readability */
  border-radius: 4px; /* Add rounded corners */
}

/* The reload button is positioned at the left */
#reload {
  left: 5px;
}

/* The counter of emojis is positioned at the center */
#numEmojis {
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.75); /* Add a semi-transparent black background */
}

/* The select dropdown is positioned at the right side */
#categorySelect {
  right: 5px;
}

/* The instructions are positioned at the center of the screen */
#instructions {
  position: absolute;
  left: 50%;
  top: 50%;
  text-align: center;
  transform: translate(-50%, -50%);
}

HTML code (index.html)

The HTML code sets up the web page, including a reload button, an emoji counter, a category selection dropdown, and an instruction display. It uses the Phaser game framework for the interactive game elements and includes links to the necessary JavaScript and CSS files:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Emojis</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <!-- 🔄 Button to reload the game -->
  <button id="reload" onclick="reload()">Reload</button>
  
  <!-- 🔢 Display the number of emojis added -->
  <div id="numEmojis">0</div>
  
  <!-- 🔽 Dropdown to select emoji categories -->
  <select id="categorySelect"></select>
  
  <!-- ℹ️ Instructions for the user -->
  <div id="instructions">Click to add emojis 🤪</div>
  
  <!-- 🎮 Container for the game -->
  <div id="gameContainer"></div>
  
  <!-- 📜 Load Phaser library -->
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  
  <!-- 📜 Load game scripts -->
  <script src="create.js"></script>
  <script src="update.js"></script>
  <script src="config.js"></script>
</body>

</html>

Constants and Phaser configuration (config.js)

Inside the config.js file we will define the various categories of emojis, each one containing a specific list. Those categories will be displayed using a dropdown menu to allow user selection.

We will also set up the game configuration, specifying the parent container, the scaling mode to ensure the game resizes correctly, and the physics engine to be used (Matter.js), to achieve a real movement and bouncing of the emojis around the screen.

Also inside this file we will link the game’s lifecycle methods (create and update) to their respective functions defined in other JavaScript files:

// 🎭 Define emoji categories with different types of emojis
const emojiCategories = {
  Smileys: ['😀', '🤩', '😄', '😁', '😆', '😅', '🥰', '🤣', '😊', '😇'],
  Body: ['👄', '👌', '🫀', '🦴', '🫁', '🦷', '🤳', '👋', '👍', '👀'],
  People: ['💂', '🥷', '🧙', '🧛', '🧜', '🧝', '🧟', '🕵', '👲', '🎅'],
  Animals: ['🪰', '🐁', '🐂', '🦂', '🐄', '🐅', '🐝', '🐇', '🐈', '🐉'],
  Travel: ['🚀', '🚁', '🚂', '🚃', '⛵', '🛸', '🚲', '🚒', '🚗', '✈'],
  Activities: ['🏓', '🎯', '⚽', '🪁', '🎲', '🏀', '🎷', '🎸', '🎹', '🎺'],
  Food: ['🌭', '🍕', '🍔', '🍟', '🍗', '🍩', '🍬', '🍜', '🥗', '🧀'],
  Objects: ['☎', '🔔', '👓', '🔍', '💡', '📕', '🪜', '📷', '💎', '🩻']
};

// 📑 Get the category select element from the DOM
let categorySelect = document.getElementById('categorySelect');

// 🎮 Initialize the Phaser game with the specified configuration
let game = new Phaser.Game({
  parent: 'gameContainer', // Container for the game
  type: Phaser.AUTO, // Automatically choose WebGL or Canvas
  scale: {
    mode: Phaser.Scale.RESIZE, // Adjust the game scale when resizing
  },
  physics: {
    default: 'matter' // Use Matter.js physics engine
  },
  scene: {
    create: create, // Call the create function to set up the game
    update: update, // Call the update function to handle game logic
  },
});

Setting up category select dropdown and the game boundaries (create.js)

The create.js file defines the setup function for the emoji game, responsible for initializing various game elements. It creates a group to hold the emoji objects and populates a dropdown menu with all the emoji categories. It sets the boundaries for the physics world to ensure that emojis stay within the game area. The file also includes an event listener for device motion, allowing the game to respond to accelerometer data for added interactivity. This setup ensures the game is ready to handle user input, display emojis, and respond dynamically to both user actions and device movements:

function create() {
  // 👥 Create a group to hold the emoji sprites
  this.emojis = this.add.group();

  // 🏷️ Populate the category select dropdown with emoji categories
  for (const category in emojiCategories) {
    const option = document.createElement('option');
    option.value = option.textContent = category;
    categorySelect.appendChild(option);
  }

  // 📐 Set the boundaries of the game world and
  // 📏 adjust the boundaries when the game is resized
  this.matter.world.setBounds();
  this.scale.on('resize', () => this.matter.world.setBounds());

  // 📱 Handle device motion events to move emojis (special code is added for Apple devices)
  if (DeviceMotionEvent.requestPermission) {
    document.getElementById("instructions").innerHTML +=
      `<br><button onclick="DeviceMotionEvent.requestPermission().then(permission => { 
        if (permission === 'granted') {
          window.addEventListener('devicemotion', (event) => handleMotion.call(game.scene.keys.default, event, -1));
          this.remove();
        }})">Allow device motion</button>`;
  } else {
    window.addEventListener('devicemotion', (event) => handleMotion.call(this, event));
  }
}

Adding emojis and handling device movement (update.js)

The update.js file defines the main update loop and event handling for the emoji game. It includes functionality for detecting when the user clicks or touches the screen, which adds a randomly selected emoji from the chosen category to the game area. Each added emoji has physics properties applied, allowing it to move and interact within the game world. The file also includes a function to handle device motion, applying forces to the emojis based on accelerometer data. Additionally, it provides a function to reload the page, ensuring the game can be reset easily. This file is crucial for managing user interactions and the dynamic behavior of the emojis during gameplay:

function update() {
  const pointer = this.input.activePointer;

  // 🖱️ Check if the mouse button is pressed
  if (pointer.isDown) {
    // 📳 Vibrate the device if supported
    if (navigator.vibrate) navigator.vibrate(25);

    // 🎲 Get a random emoji from the selected category
    const emojisInCategory = emojiCategories[categorySelect.value];
    const randomEmoji = emojisInCategory[Phaser.Math.Between(0, emojisInCategory.length - 1)];

    // 🎨 Add the emoji to the game at the pointer position
    const textEmoji = this.add.text(pointer.worldX, pointer.worldY, randomEmoji, { fontSize: 32 }).setPadding(4);
    const circleEmoji = this.matter.add.circle(pointer.worldX, pointer.worldY, 16, { restitution: 1, friction: 0.25 });

    // 🌀 Add physics to the emoji
    this.matter.add.gameObject(textEmoji, circleEmoji);
    this.matter.setVelocity(circleEmoji, Phaser.Math.Between(-10, 10), Phaser.Math.Between(-10, 10));

    // 🏷️ Add the emoji to the emojis group
    this.emojis.add(textEmoji);
    document.getElementById('numEmojis').innerText++;
  }
}

function handleMotion(event, factor = 1.0) {
  const { x, y } = event.accelerationIncludingGravity;

  // 🏃 Apply motion forces to the emojis based on device movement
  this.emojis.children.iterate(emoji => {
    const body = emoji.body;
    const force = 0.00025 * body.mass * factor;

    this.matter.body.applyForce(body, { x: body.position.x, y: body.position.y }, { x: -x * force, y: y * force });
  });
}

// 🔄 Reload the page
function reload() {
  if (confirm("Reload the page?")) location.reload();
}

Proposed exercise: Using your preferred emojis

Create a new version of the game using your preferred emojis. You can change the existing emojis, or add more emojis to each category or even insert new categories (i.e. drinks, flags, zodiac signs, etc.). Additionally, you can add background music or make any other changes you like.

Do not forget to create all the files required to run the code in this unit:

  • style.css
  • index.html
  • create.js
  • update.js
  • config.js

You may find the whole list of emojis at OpenMoji, and you can just copy and paste any emoji you like into the config.js file.

Enjoy the game!

You can enjoy playing this simple and funny game online here.

Phaser. Unit 10. Halloween merge game

Introduction

In this unit we will develop a game to merge objects inspired by the Suika Game, also known as the Watermelon Game. Although our game will be quite similar, it will feature a Halloween theme, from this specific Suika Clone version:

Halloween

CSS code (styles.css)

We are going to insert some CSS code to remove any margin and padding, as well as the scrollbars, to ensure that the maximum window size is used when the game starts. We will also display an image as the background for the game and we will define some border-radius to smooth the corners of the headstone:

body {
  height: 100vh;
  margin: 0;
  overflow: hidden;
  background-image: url(assets/background.jpg);
  background-size: cover;
}

canvas {
  border-radius: 20px;
}

HTML code (index.html)

The HTML source code inside the “index.html” file must be structured to link all necessary resources and scripts required to run the game:

  • preload.js will be responsible for loading all assets (images, sounds, etc.) before the game starts.
  • create.js will set up initial game objects and game states.
  • update.js will handle the logic that updates the state of the game when something happens (dropper movement, collisions, game over, etc).
  • config.js will contain configuration settings for the game, such as screen dimensions, physics settings, and global variables.

This HTML setup ensures that when the page is loaded in a browser, all necessary components are initialized in the correct order, starting with the loading of the Phaser framework, followed by specific game scripts that define and control the gameplay. This structure provides a clean and organized way to manage different aspects of game development:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Suika Clone</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <div id="gameContainer"></div>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  <script src="preload.js"></script>
  <script src="create.js"></script>
  <script src="update.js"></script>
  <script src="config.js"></script>
</body>

</html>

Constants and Phaser configuration (config.js)

Inside the “config.js” file we will first define a list of game objects, each with a name and a specific radius. These objects include various Halloween-themed items like candies, spiders, and pumpkins. They will be used as the objects to be merged in the game, each one bigger than the previous one.

In the second part of the file, a new Phaser game instance is configured with specific settings such as canvas size, scaling to fit the container while maintaining aspect ratio, and center alignment. The game is set to be transparent and uses the Matter physics engine to manage physical interactions. Essential game functions like preload for asset loading and create for setting up initial game states are also defined:

const objects = [
  { name: "candy", radius: 30 },
  { name: "spider", radius: 35 },
  { name: "eye", radius: 40 },
  { name: "candle", radius: 50 },
  { name: "apple", radius: 65 },
  { name: "hat", radius: 70 },
  { name: "skull", radius: 80 },
  { name: "pumpkin", radius: 90 },
  { name: "cat", radius: 100 },
  { name: "ghost", radius: 110 },
  { name: "cauldron", radius: 120 },
];

new Phaser.Game({
  width: 600,
  height: 1000,
  parent: "gameContainer",
  scale: {
    mode: Phaser.Scale.ScaleModes.FIT,
    autoCenter: Phaser.Scale.Center.CENTER_BOTH
  },
  transparent: true,
  physics: {
    default: "matter",
    matter: {
      debug: false,
    },
  },
  scene: {
    preload: preload,
    create: create
  },
});

Images, music and sounds (assets folder)

We will need some images and sounds. You may use any assets you like, but we are providing the original ones so that you can easily develop the same game. Some examples are shown below, and also a zip file containing all the assets can be downloaded here.

Background and headstone images

Background
Headstone

Score numbers

Objects

Candy
Spider
Eye
Candle
Apple
Hat
Skull
Pumpkin
Cat
Ghost
Cauldron

Background music

Sounds

Match sound (when two objects are merged)
Game over sound (when the stack of objects reaches the ceiling)

Loading the assets (preload.js)

The provided preload function is part of a Phaser game’s lifecycle and is crucial for setting up the initial state of the game as well as loading all necessary assets before the gameplay can begin. This function ensures that all graphical and audio components required for the game are loaded into memory and are accessible during the game to avoid delays and enhance the gaming experience.

In this function, the game starts by initializing essential game state variables such as the score, which is set to zero, and a boolean flag gameOver, set to false, indicating the game is currently active. These are fundamental for tracking gameplay progress and status.

The function then proceeds to load various types of assets:

  1. Images: It loads specific images like the “headstone” and “newgame” graphics from predefined paths within the assets directory. Additionally, it loads a series of images representing numbers (0-9), used for displaying the score on the game interface. Each object defined in the objects array ensures that all visual elements related to gameplay objects are available.
  2. Audio: Three types of audio files are loaded: background music, a sound effect for matching items (used during successful gameplay interactions), and a game-over sound (used to signify the end of the game). This enhances the feedback for the player, contributing to the overall immersive experience of the game.

Overall, the preload function is essential for managing the game’s resources, loading everything needed. This setup allows the game to run more efficiently and provides a seamless player experience by having all necessary assets ready at game start:

function preload() {
  this.score = 0;
  this.gameOver = false;

  this.load.image("headstone", "assets/headstone.png");
  this.load.image("newgame", "assets/new-game.png");

  for (let i = 0; i <= 9; i++) {
    this.load.image(`${i}`, `assets/numbers/${i}.png`);
  }

  for (const object of objects) {
    this.load.image(`${object.name}`, `assets/objects/${object.name}.png`);
  }

  this.load.audio("background-music", "assets/background-music.mp3");
  this.load.audio("match-sound", "assets/match-sound.mp3");
  this.load.audio("game-over-sound", "assets/game-over-sound.mp3");
}

Setting up the headstone with the score, the dropper, and particles and light effects (create.js)

The code inside “create.js” will focus on initializing the game components and configuring interactions in a structured manner within the create function. This function serves as the central setup hub where multiple functions are called to configure specific aspects of the game environment and mechanics.

The game begins with setting up audio elements to enrich the gameplay experience with background music and sound effects for specific actions like matches or game over events. Visual aspects are addressed next, including dynamic lighting effects that follow the cursor, creating an engaging and responsive atmosphere. Additionally, particle systems are configured to visually represent interactions such as collisions, adding a layer of polish to the game dynamics.

Game objects and physical boundaries are established through functions like setupHeadstone, which also configures the visual representation and interaction boundaries within the gameplay area. A crucial gameplay mechanic is introduced with setupCeiling and setupDropper, where objects interact within set limits, and a dropper mechanism is used for object placement, enhancing the interactive component of the game.

Other functionalities include a restart button, allowing players to easily reset the game, thereby improving user experience and engagement. The setupEvents function ties all interactive elements together, enabling the game to respond to user inputs and collisions, ensuring a seamless and dynamic gameplay experience. Overall, this code snippet is fundamental in laying down the mechanics and aesthetic elements of the game, making it ready for engaging and responsive play:

function create() {
  setupAudio.call(this);
  setupLight.call(this);
  setupParticles.call(this);
  setupHeadstone.call(this);
  drawScore.call(this);
  setupCeiling.call(this);
  setupDropper.call(this);
  setupRestartButton.call(this);
  setupEvents.call(this);
}

function setupAudio() {
  this.backgroundMusic = this.sound.add('background-music', { volume: 0.75 }).play({ loop: -1 });
  this.matchSound = this.sound.add('match-sound', { volume: 1.00 });
  this.gameOverSound = this.sound.add('game-over-sound', { volume: 1.00 });
}

// Setup the light at the cursor
function setupLight() {
  this.light = this.lights
    .addLight(this.input.activePointer.x, this.input.activePointer.y, 1000, 0x99ffff, 0.75)
    .setScrollFactor(0);
  this.lights.enable().setAmbientColor(0xdddddd);
}

// Define the particles to be created when a pair of objects collide
function setupParticles() {
  this.particles = this.add.particles(0, 0, objects[0].name, {
    lifespan: 1000,
    speed: { min: 200, max: 350 },
    scale: { start: 0.1, end: 0 },
    rotate: { start: 0, end: 360 },
    alpha: { start: 1, end: 0 },
    gravityY: 200,
    emitting: false
  });
}

function setupHeadstone() {
  // Draw the image of the headstone
  this.add
    .nineslice(0, 0, "headstone")
    .setOrigin(0)
    .setDisplaySize(+this.game.config.width, +this.game.config.height)
    .setPipeline("Light2D")
    .setDepth(-2);

  // Define a box where the score will be drawn at the top of the headstone
  this.scoreBox = this.add.renderTexture(
    +this.game.config.width / 2, 150,
    +this.game.config.width, 100
  ).setScale(0.8);

  // Create a grup of objects to put all the falling objects
  this.objects = this.add.group();

  // Define the limits of the headstone where the objects will be located
  this.matter.world.setBounds(
    65, 0,
    +this.game.config.width - 130, +this.game.config.height - 1
  );
}

// Draw a line to check when the objects reach the top of the headstone
function setupCeiling() {
  this.ceiling = this.matter.add.rectangle(
    +this.game.config.width / 2, 100,
    +this.game.config.width, 200
  );
  this.ceiling.isStatic = true;

  const line = this.add
    .rectangle(160, 200, +this.game.config.width - 320, 2, 0xccccff)
    .setOrigin(0)
    .setAlpha(0.1)
    .setDepth(-2);
  line.postFX.addShine();
  line.postFX.addGlow();
}

// Configure the dropper with the first object and add a glow effect
function setupDropper() {
  this.dropper = this.add.image(this.input.activePointer.x, 0, objects[0].name);
  const glow = this.dropper.postFX.addGlow(0x99ddff);
  this.tweens.addCounter({
    yoyo: true, repeat: -1, from: 1, to: 3, duration: 1000,
    onUpdate: tween => glow.outerStrength = tween.getValue()
  });
  updateDropper.call(this, objects[0]);
}

// When the game is over a button is shown to restart the game
function setupRestartButton() {
  const centerX = this.game.config.width / 2;
  const centerY = this.game.config.height / 2;
  const button = this.add.image(centerX, centerY, "newgame")
    .setScale(0.4)
    .setInteractive({ useHandCursor: true })
    .setVisible(false);

  button.postFX.addGlow(0x000000, 0.75);

  const tweenOptions = scale => ({ targets: button, scale, ease: "Linear", duration: 100 });

  button.on("pointerover", () => this.tweens.add(tweenOptions(0.5)));
  button.on("pointerout", () => this.tweens.add(tweenOptions(0.4)));
  button.on("pointerup", () => restart.call(this));
  this.restartButton = button;
}

// Configure all the events that will be used in the game (dropper and objects collisions)
function setupEvents() {
  this.input.on("pointermove", pointer => moveDropper.call(this, pointer));
  this.input.on("pointerdown", pointer => moveDropper.call(this, pointer));
  this.input.on("pointerup", () => nextObject.call(this));
  this.matter.world.on('collisionstart', event => handleCollisions.call(this, event));
}

Updating the dropper and controlling the objects collisions (update.js)

The “update.js” file will include all the functions to define the dropper mechanism and to implement controls for managing object collisions in the game.

The functions setDropperX and moveDropper handle the positioning of a draggable game object, referred to as the “dropper,” which players can move horizontally. The position is carefully constrained within predefined game boundaries to ensure it does not move outside the playable area. The updateDropper function updates the visual representation of the dropper based on game events, such as selecting different objects to be dropped next, and also includes visual effects like shining to highlight active elements.

Gameplay interaction is further expanded through the addObject function, which introduces new game objects into the scene at specified positions, simulating physical properties like friction and bounce. The nextObject function controls the sequence of objects being introduced, ensuring that gameplay progresses logically and consistently. The scoring system is integrated into various game actions, with the drawScore function rendering the current score visually in a dedicated area, providing immediate feedback on player performance.

Collision handling is crucial in this game setup, as seen in the handleCollisions function. This function manages the effects when two objects of the same type collide, including updating the score, playing sound effects, and possibly escalating to a higher level of complexity by introducing new, more challenging objects. The game’s flow is controlled by the gameOver function, which triggers when certain game-ending conditions are met, handling tasks such as displaying restart options and stopping all ongoing game sounds.

Finally, the restart function allows players to reset the game to its initial state, providing a fresh start and ensuring that the game can be played repeatedly without needing to reload the entire game.

Overall, these functions collectively manage the core mechanics of the game, including object manipulation, scoring, event handling, and game state transitions, creating a dynamic and interactive gaming experience:

function setDropperX(x) {
  const padding = 65;
  const radius = this.dropper.displayWidth / 2;
  const maxWidth = this.game.config.width - radius - padding;
  x = Math.max(radius + padding, Math.min(x, maxWidth));
  this.dropper.setX(x);
}

// Moves the dropper when the event "pointermove" happens
function moveDropper(pointer) {
  setDropperX.call(this, pointer.x);
  this.light.setPosition(pointer.x, pointer.y);
}

// Updates the image of the dropper when the event "pointerup" happens
function updateDropper(object) {
  this.dropper
    .setTexture(object.name)
    .setName(object.name)
    .setDisplaySize(object.radius * 2, object.radius * 2)
    .setY(object.radius + 205);
  setDropperX.call(this, this.input.activePointer.x);

  // Check which object is the same as the one in the dropper and make it shine
  this.objects.getChildren().forEach((gameObject) => {
    if (gameObject instanceof Phaser.GameObjects.Image) {
      gameObject.postFX.clear();

      if (gameObject.name === object.name) gameObject.postFX.addShine();
    }
  });
}

// Renders the score inside the box defined previously
function drawScore() {
  this.scoreBox.clear();
  const chars = this.score.toString().split("");

  const textWidth = chars.reduce((acc, c) => acc + this.textures.get(c).get().width, 0);

  let x = (this.scoreBox.width - textWidth) / 2;

  chars.forEach(char => {
    this.scoreBox.drawFrame(char, undefined, x, 0);
    x += this.textures.get(char).get().width;
  });
}

function addObject(x, y, object) {
  this.objects.add(this.matter.add
    .image(x, y, object.name)
    .setName(object.name)
    .setDisplaySize(object.radius * 2, object.radius * 2)
    .setCircle(object.radius)
    .setFriction(0.005)
    .setBounce(0.2)
    .setDepth(-1)
    .setOnCollideWith(this.ceiling, () => gameOver.call(this)));
}

// Creates and puts a new object on the headstone
function nextObject() {
  if (!this.dropper.visible || this.gameOver) return;

  this.dropper.setVisible(false);
  this.time.delayedCall(500, () => this.dropper.setVisible(!this.gameOver));

  addObject.call(this,
    this.dropper.x, this.dropper.y,
    objects.find(object => object.name === this.dropper.name)
  );

  updateDropper.call(this, objects[Math.floor(Math.random() * 5)]);
}

// Function executed when two objects of the same type collide
function handleCollisions(event) {
  for (const { bodyA, bodyB } of event.pairs) {
    if (bodyA.gameObject?.name === bodyB.gameObject?.name) {
      if (navigator.vibrate) navigator.vibrate(50);
      
      const objectIndex = objects.findIndex(object => object.name === bodyA.gameObject.name);
      if (objectIndex === -1) return;

      this.score += (objectIndex + 1) * 2;
      drawScore.call(this);

      bodyA.gameObject.destroy();
      bodyB.gameObject.destroy();

      this.particles
        .setTexture(objects[objectIndex].name)
        .emitParticleAt(bodyB.position.x, bodyB.position.y, 10);

      this.matchSound.play();
      
      const newObject = objects[objectIndex + 1];
      if (!newObject) return;

      addObject.call(this, bodyB.position.x, bodyB.position.y, newObject);
      return;
    }
  }
}

function gameOver() {
  this.gameOver = true;
  this.restartButton.setVisible(true);
  this.dropper.setVisible(false);
  this.game.sound.stopAll();
  this.gameOverSound.play({ loop: -1 });
}

function restart() {
  this.score = 0;
  this.gameOver = false;
  this.game.sound.stopAll();
  this.scene.restart();
}

Proposed exercise: Using your own assets

Create a new version of the game using your own assets (images, and sounds), and even including more objects. You can also change any other things you like.

Do not forget to create all the files required to run the code in this unit:

  • styles.css
  • index.html
  • preload.js
  • create.js
  • update.js
  • config.js
  • assets folder

You may find many free assets in OpenGameArt.

Enjoy the game!

You can enjoy playing this wonderful game online here.

CSS. Unidad 4. Aplicando estilos a listas.

Introducción

Las listas se comportan como cualquier otro texto en su mayor parte, pero hay algunas propiedades CSS específicas de las listas que debes conocer y algunas recomendaciones a tener en cuenta. Este artículo te lo explica.

Un ejemplo sencillo de lista

Para empezar, veamos un ejemplo sencillo de una lista. A lo largo de esta unidad veremos listas no ordenadas, listas ordenadas y listas de descripciones. Todas tienen características de estilo similares, pero algunas propiedades son particulares del tipo de lista.

Utilizaremos el siguiente código HTML:

<h2>Ingredienetes (lista no ordenada)</h2>

<ul>
  <li>Hummus</li>
  <li>Pan pita</li>
  <li>Ensalada verde</li>
  <li>Queso halloumi</li>
</ul>

<h2>Ingredientes (lista descriptiva)</h2>

<dl>
  <dt>Hummus</dt>
  <dd>Una salsa espesa hecha generalmente de garbanzos, tahini, jugo de limón, sal, ajo y otros ingredientes.</dd>
  <dt>Pan pita</dt>
  <dd>Un pan plano suave y ligeramente leudado.</dd>
  <dt>Queso halloumi</dt>
  <dd>Un queso semiduro, sin madurar, en salmuera con un punto de fusión superior al habitual, generalmente hecho de leche de cabra/oveja.</dd>
  <dt>Ensalada verde</dt>
  <dd>Esa cosa verde y saludable que muchos de nosotros solo usamos para adornar kebabs.</dd>
</dl>

<h2>Receta (lista ordenada)</h2>

<ol>
  <li>Tostar el pan pita, dejar enfriar y cortar por el borde.</li>
  <li>Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.</li>
  <li>Lavar y picar la ensalada.</li>
  <li>Rellenar el pan pita con ensalada, hummus y queso halloumi frito.</li>
</ol>

Los valores predeterminados son los siguientes:

  • Los elementos <ul> y <ol> tienen un margin superior e inferior de 16px (1em) y un padding-left de 40px (2.5em.)
  • Los elementos de lista <li> no tienen valores de espacio predeterminados.
  • El elemento <dl> tiene un margin superior e inferior de 16px (1em), pero no tiene ningún padding establecido.
  • Los elementos <dd> tienen un margin-left de 40px (2.5em).
  • Los elementos de referencia <p> que hemos incluido tienen un margin superior e inferior de 16px (1em), al igual que los diferentes tipos de lista.

Ejercicio propuesto: Listas con los estilos por defecto

Crea una página web con la lista del ejemplo anterior y observa el resultado en tu navegador. Se debería mostrar con los estilos por defecto, ya que no estamos utilizando CSS todavía.

Deberías obtener un resultado parecido al siguiente:

Ingredientes (lista no ordenada)

  • Hummus
  • Pan pita
  • Ensalada verde
  • Queso halloumi

Ingredientes (lista descriptiva)

Hummus
Una salsa espesa hecha generalmente de garbanzos, tahini, jugo de limón, sal, ajo y otros ingredientes.
Pan pita
Un pan plano suave y ligeramente leudado.
Queso halloumi
Un queso semiduro, sin madurar, en salmuera con un punto de fusión superior al habitual, generalmente hecho de leche de cabra/oveja.
Ensalada verde
Esa cosa verde y saludable que muchos de nosotros solo usamos para adornar kebabs.

Receta (lista ordenada)

  1. Tostar el pan pita, dejar enfriar y cortar por el borde.
  2. Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.
  3. Lavar y picar la ensalada.
  4. Rellenar el pan pita con ensalada, hummus y queso halloumi frito.

Estilos básicos de las listas

Al crear listas, es necesario ajustar el diseño para que mantengan los mismos espaciados verticales (a veces denominados ritmos verticales) que el resto de elementos circundantes, como párrafos e imágenes.

El CSS que vamos a utilizar para aplicar estilo al texto y al espaciado de texto es el siguiente:

/* Estilos generales */
html {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 10px;
}

h2 {
  font-size: 2rem;
}

ul, ol, dl, p {
  font-size: 1.5rem;
}

li, p {
  line-height: 1.5;
}

/* Estilos para las listas de descripciones */
dd, dt {
  line-height: 1.5;
}

dt {
  font-weight: bold;
}

dd {
  margin-bottom: 1.5rem;
}
  • La primera regla establece un tipo de letra y un tamaño de letra base de 10px. Estos valores se heredan para toda la página.
  • Las reglas 2 y 3 establecen tamaños de letra relativos para los títulos, diferentes tipos de listas (que heredan los hijos de los elementos de listas), y párrafos. Esto significa que todos los párrafos y todas las listas tendrán el mismo tamaño de letra y el mismo espaciado superior e inferior, lo que ayudará a mantener el ritmo vertical constante.
  • La regla 4 establece el mismo interlineado (line-height) en los párrafos y los elementos de las listas, de modo que todos los párrafos y todos los elementos individuales de las listas tendrán el mismo espaciado entre las líneas. Esto también ayudará a mantener el ritmo vertical consistente.
  • Las reglas 5 y 6 se aplican a las listas de descripciones. Establecemos la misma altura de interlineado (line-height) en los términos y las descripciones de la lista de descripciones, así como hicimos con los párrafos y los elementos de la lista. De esta forma mantenemos un estilo coherente. También establecemos que los términos de las descripciones tengan un estilo de negrita, para que destaquen visualmente.

Ejercicio propuesto: Listas con tus propios estilos

Crea una página web con las listas y estilos del ejemplo anterior, y cambia o añade los estilos que quieras. A continuación enlaza el fichero CSS desde el fichero HTML, y comprueba el resultado en tu navegador. No olvides validar tanto el código HTML como el CSS.

El resultado obtenido podría ser similar al siguiente:

Ingredientes (lista no ordenada)

  • Hummus
  • Pan pita
  • Ensalada verde
  • Queso halloumi

Ingredientes (lista descriptiva)

Hummus
Una salsa espesa hecha generalmente de garbanzos, tahini, jugo de limón, sal, ajo y otros ingredientes.
Pan pita
Un pan plano suave y ligeramente leudado.
Queso halloumi
Un queso semiduro, sin madurar, en salmuera con un punto de fusión superior al habitual, generalmente hecho de leche de cabra/oveja.
Ensalada verde
Esa cosa verde y saludable que muchos de nosotros solo usamos para adornar kebabs.

Receta (lista ordenada)

  1. Tostar el pan pita, dejar enfriar y cortar por el borde.
  2. Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.
  3. Lavar y picar la ensalada.
  4. Rellenar el pan pita con ensalada, hummus y queso halloumi frito.

Estilos específicos de listas

Ahora que hemos analizado el espaciado general de las listas, exploremos algunas propiedades específicas de las listas. Para empezar, debes conocer tres propiedades que pueden establecerse en los elementos <ul> o <ol>:

  • list-style-type: Establece el tipo de viñetas para la lista, por ejemplo, viñetas cuadradas o circulares para una lista no ordenada; números, letras, o números romanos para una lista ordenada.
  • list-style-position: Establece si las viñetas aparecen dentro de los elementos de la lista o fuera de ellos, antes del inicio de cada elemento.
  • list-style-image: Te permite usar una imagen personalizada para la viñeta, en lugar de un simple cuadrado o círculo.

El estilo de la viñeta

Como hemos comentado, la propiedad list-style-type te permite establecer qué tipo de viñeta usar. En nuestro ejemplo, hemos establecido que se usen números romanos en mayúsculas para la lista ordenada, con:

ol {
  list-style-type: upper-roman;
}
  1. Tostar el pan pita, dejar enfriar y cortar por el borde.
  2. Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.
  3. Lavar y picar la ensalada.
  4. Rellenar el pan pita con ensalada, hummus y queso halloumi frito.

Puedes encontrar más opciones si echas un vistazo a la página de referencia de list-style-type.

La posición de la viñeta

La propiedad list-style-position establece si las viñetas aparecen dentro de los elementos de la lista, o fuera de ellos antes del inicio de cada elemento. El valor por defecto es outside, que provoca que las viñetas se sitúen fuera de los elementos de lista, como se observa arriba.

Si estableces el valor en inside, las viñetas se ubican dentro de las líneas:

ol {
  list-style-type: upper-roman;
  list-style-position: inside;
}
  1. Tostar el pan pita, dejar enfriar y cortar por el borde.
  2. Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.
  3. Lavar y picar la ensalada.
  4. Rellenar el pan pita con ensalada, hummus y queso halloumi frito.

Usando una imagen personalizada como viñeta

La propiedad list-style-image te permite usar una imagen personalizada para tu viñeta. La sintaxis es muy simple:

ul {
  list-style-image: url("https://mdn.github.io/learning-area/css/styling-text/styling-lists/star.svg");
}

Sin embargo, esta propiedad es un poco limitada por lo que respecta al control de la posición, el tamaño, etc., de las viñetas. En algunos casos puede resultar más conveniente usar la propiedad background, tal como haremos nosotros. En nuestro ejemplo vamos a definir el estilo a la lista no ordenada utilizando esta última propiedad. Para ello añadiremos el siguiente código al que ya teníamos previamente:

ul {
  padding-left: 2rem;
  list-style-type: none;
}

ul li {
  padding-left: 2rem;
  background-image: url("https://mdn.github.io/learning-area/css/styling-text/styling-lists/star.svg");
  background-position: 0 0;
  background-size: 1.6rem 1.6rem;
  background-repeat: no-repeat;
}
  • Hummus
  • Pan pita
  • Ensalada verde
  • Queso halloumi

Aquí hemos hecho lo siguiente:

  • Reducir el valor de la propiedad padding-left del elemento <ul> desde su valor predeterminado de 40 px hasta 20 px. A continuación, establecer la misma cantidad para los elementos de la lista. De este modo, todos los elementos de la lista siguen alineados con los elementos de la lista ordenada y las descripciones, pero los elementos de lista tienen algo de relleno (padding) para poder insertar las imágenes de fondo. Si no hiciéramos esto, las imágenes de fondo se solaparían con el texto de los elementos de la lista y quedaría un aspecto desordenado.
  • Establecer la propiedad list-style-type en none, para que no aparezca la viñeta predeterminada. En lugar de ello, vamos a utilizar las propiedades background para cambiar las viñetas.
  • Insertar una viñeta en cada elemento de la lista no ordenada. Las propiedades relevantes son las siguientes:
    • background-image: Proporciona la ruta que apunta al archivo de imagen que quieres usar como viñeta.
    • background-position: Define en qué lugar del elemento seleccionado va a aparecer la imagen; en este caso le decimos 0 0, que significa que la viñeta va a aparecer en el extremo superior izquierdo de cada elemento de lista.
    • background-size: Establece el tamaño de la imagen de fondo. En teoría queremos que las viñetas sean del mismo tamaño que los elementos de lista (o solo un poco menores o mayores). Utilizamos un tamaño de 1.6rem (16px), que encaja muy bien con el área de relleno de 20px que hemos elegido para que quepa la viñeta; 16 px más 4 px de espacio entre la viñeta y el texto del elemento de lista funciona bien.
    • background-repeat: Por defecto, las imágenes de fondo se repiten hasta rellenar todo el espacio de fondo disponible. En este caso solo queremos una copia de la imagen, de modo que establecemos el valor de esta propiedad en no-repeat.

Ejercicio propuesto: Juntándolo todo

Utilizando el código del ejercicio anterior, cambia las viñetas que aparecen por defecto para utilizar una imagen, y cambia también la numeración por defecto para utilizar cualquier otro estilo que te guste. Finalmente comprueba el resultado en tu navegador y valida el código HTML y CSS.

Un posible resultado podría ser el siguiente:

Ingredientes (lista no ordenada)

  • Hummus
  • Pan pita
  • Ensalada verde
  • Queso halloumi

Ingredientes (lista descriptiva)

Hummus
Una salsa espesa hecha generalmente de garbanzos, tahini, jugo de limón, sal, ajo y otros ingredientes.
Pan pita
Un pan plano suave y ligeramente leudado.
Queso halloumi
Un queso semiduro, sin madurar, en salmuera con un punto de fusión superior al habitual, generalmente hecho de leche de cabra/oveja.
Ensalada verde
Esa cosa verde y saludable que muchos de nosotros solo usamos para adornar kebabs.

Receta (lista ordenada)

  1. Tostar el pan pita, dejar enfriar y cortar por el borde.
  2. Freír el queso halloumi en una sartén antiadherente poco profunda hasta que esté dorado por ambos lados.
  3. Lavar y picar la ensalada.
  4. Rellenar el pan pita con ensalada, hummus y queso halloumi frito.

Propiedad abreviada list-style

Es importante mencionar la existencia de la propiedad abreviada list-style, que nos permite configurar las tres propiedades del ejemplo anterior en una sola línea. Por ejemplo, observa el CSS siguiente:

ul {
  list-style-type: disc;
  list-style-image: url(example.png);
  list-style-position: inside;
}

Se podría reemplazarse por esto:

ul {
  list-style: disc url(example.png) inside;
}

Los valores pueden escribirse en cualquier orden, y puedes usar uno, dos o los tres (los valores por defecto que se utilizan para las propiedades que no están incluidas son discnone y outside). Si se especifican tanto type como image, el valor de type se usa como una segunda opción en el caso de que no sea posible cargar la imagen por cualquier motivo.

Test

Comprueba tus conocimientos con este test sobre estilos de listas.

MS Paint remake

Classic MS Paint, REVIVED + ✨Extras

A pixel-perfect web-based MS Paint remake and more. JS Paint recreates every tool and menu of MS Paint to a high degree of fidelity:

It supports even little-known features, themes, additional file types, and accessibility features like Eye Gaze Mode and Speech Recognition (more information on github).

Ah yes, good old Paint. Not the one with the ribbons or the new skeuomorphic one with the interface that can take up nearly half the screen. (And not the even newer Paint 3D.)

Windows 95, 98, and XP were the golden years of Paint. You had a tool box and a color box, a foreground color and a background color, and that was all you needed.

Things were simple. But we want to undo more than three actions. We want to edit transparent images. We can’t just keep using the old Paint. So that’s why the author is making JS Paint. He wants to bring good old Paint into the modern era.

Current improvements

  • Open source (MIT licensed)
  • Cross-platform
  • Mobile friendly
    • Touch support: use two fingers to pan the view, and pinch to zoom
    • Click/tap the selected colors area to swap the foreground and background colors
    • View > Fullscreen to toggle fullscreen mode, nice for small screens
  • Web features
    • File > Load From URL… to open an image from the Web.
    • File > Upload to Imgur to upload the current image to Imgur.
    • Paste supports loading from URLs.
    • You can create links that will open an image from the Web in JS Paint. For example, this link will start with an isometric grid as a template: https://paint.fernandoruizrico.com/#load:https://i.imgur.com/zJMrWwb.png
    • Rudimentary multi-user collaboration support. Start up a session at https:/paint.fernandoruizrico.com/#session:multi-user-test and send the link to your friends! It isn’t seamless; actions by other users interrupt what you’re doing, and visa versa. Sessions are not private, and you may lose your work at any time. If you want better collaboration support, follow the development of Mopaint.
  • Extras > Themes to change the look of the app. Dark mode included.
  • Eye Gaze Mode, for use with an eye tracker, head tracker, or other coarse input device, accessible from Extras > Eye Gaze Mode. With just a webcam, you can try it out with Enable Viacam (head tracker) or GazePointer (eye tracker).
  • Speech Recognition Mode. Using your voice you can select tools and colors, pan the view (“scroll down and to the left”, or “go southwest”, etc.), explore the menus (but you can activate any menu item without opening the menus first), interact with windows (including scrolling the history view with “scroll up”/”scroll down” etc.), dictate text with the Text tool, and even tell the application to sketch things (for instance, “draw a house”)
  • Create an animated GIF from the current document history. Accessible from the Extras menu or with Ctrl+Shift+G. It’s pretty nifty, you should try it out! You might want to limit the size of the image though.
  • Load and save many different palette formats with Colors > Get Colors and Colors > Save Colors. (I made a library for this: AnyPalette.js.)
    • You can also drag and drop palette files into the app to load.

Editing Features

  • Use Alt+Mousewheel to zoom in and out
  • Edit transparent images! To create a transparent image, go to Image > Attributes… and select Transparent, then OK, and then Image > Clear Image or use the Eraser tool. Images with any translucent pixels will open in Transparent mode.
  • You can crop the image by making a selection while holding Ctrl
  • Keyboard shortcuts for rotation: Ctrl+. and Ctrl+, (< and >)
  • Rotate by any arbitrary angle in Image > Flip/Rotate
  • In Image > Stretch/Skew, you can stretch more than 500% at once
  • Zoom to an arbitrary scale in View > Zoom > Custom…
  • Zoom to fit the canvas within the window with View > Zoom > Zoom To Window
  • Non-contiguous fill: Replace a color in the entire image by holding Shift when using the fill tool

Miscellaneous Improvements

  • Vertical Color Box mode, accessible from Extras > Vertical Color Box
  • You can use the Text tool at any zoom level (and it previews the exact pixels that will end up on the canvas).
  • Spellcheck is available in the textbox if your browser supports it.
  • Resize handles are easier to grab than in Windows 10’s Paint.
  • Omits some Thumbnail view bugs, like the selection showing in the wrong place.
  • Unlimited undos/redos (as opposed to a measly 3 in Windows XP, or a measly 50 in Windows 7)
  • Undo history is nonlinear, which means if you undo and do something other than redo, the redos aren’t discarded. Instead, a new branch is created in the history tree. Jump to any point in history with Edit > History or Ctrl+Shift+Y
  • Automatically keeps a backup of your image. Only one backup per image tho, which doesn’t give you a lot of safety. Remember to save with File > Save or Ctrl+S! Manage backups with File > Manage Storage.
JS Paint drawing of JS Paint on a phone

Limitations

A few things with the tools aren’t done yet. See TODO.md

Full clipboard support in the web app requires a browser supporting the Async Clipboard API w/ Images, namely Chrome 76+ at the time of writing.

In other browsers you can still can copy with Ctrl+C, cut with Ctrl+X, and paste with Ctrl+V, but data copied from JS Paint can only be pasted into other instances of JS Paint. External images can be pasted in.

Supported File Formats

Image Formats

⚠️ Saving as JPEG will introduce artifacts that cause problems when using the Fill tool or transparent selections.

⚠️ Saving in some formats will reduce the number of colors in the image.

💡 Unlike in MS Paint, you can use Edit > Undo to revert color or quality reduction from saving. This doesn’t undo saving the file, but allows you to then save in a different format with higher quality, using File > Save As.

💡 Saving as PNG is recommended as it gives small file sizes while retaining full quality.

File ExtensionNameReadWriteRead PaletteWrite Palette
.pngPNG🔜
.bmp, .dibMonochrome Bitmap🔜
.bmp, .dib16 Color Bitmap🔜
.bmp, .dib256 Color Bitmap🔜
.bmp, .dib24-bit BitmapN/AN/A
.tif, .tiff, .dng, .cr2, .nefTIFF (loads first page)
.pdfPDF (loads first page)
.webpWebP🌐🌐
.gifGIF🌐🌐
.jpeg, .jpgJPEG🌐🌐N/AN/A
.svgSVG (only default size)🌐
.icoICO (only default size)🌐

Capabilities marked with 🌐 are currently left up to the browser to support or not. If “Write” is marked with 🌐, the format will appear in the file type dropdown but may not work when you try to save. For opening files, see Wikipedia’s browser image format support table for more information.

Capabilities marked with 🔜 are coming soon, and N/A of course means not applicable.

“Read Palette” refers to loading the colors into the Colors box automatically (from an indexed color image), and “Write Palette” refers to writing an indexed color image.

Color Palette Formats

With Colors > Save Colors and Colors > Get Colors you can save and load colors in many different formats, for compatibility with a wide range of programs.

If you want to add extensive palette support to another application, I’ve made this functionality available as a library:  AnyPalette.js

File ExtensionNameProgramsReadWrite
.palRIFF PaletteMS Paint for Windows 95 and Windows NT 4.0
.gplGIMP PaletteGimpInkscapeKritaKolourPaintScribusCinePaintMyPaint
.acoAdobe Color SwatchAdobe Photoshop
.aseAdobe Swatch ExchangeAdobe PhotoshopInDesign, and Illustrator
.txtPaint.NET PalettePaint.NET
.actAdobe Color TableAdobe Photoshop and Illustrator
.pal, .psppalettePaint Shop Pro PalettePaint Shop Pro (Jasc Software / Corel)
.hplHomesite PaletteAllaire Homesite / Macromedia ColdFusion
.csColorSchemerColorSchemer Studio
.palStarCraft PaletteStarCraft
.wpeStarCraft Terrain PaletteStarCraft
.sketchpaletteSketch PaletteSketch
.splSkencil PaletteSkencil (formerly called Sketch)
.socStarOffice ColorsStarOfficeOpenOfficeLibreOffice
.colorsKolourPaint Color CollectionKolourPaint
.colorsPlasma Desktop Color SchemeKDE Plasma Desktop
.themeWindows ThemeWindows Desktop
.themepackWindows ThemeWindows Desktop
.css, .scss, .stylCascading StyleSheetsWeb browsers / web pages
.html, .svg, .jsany text files with CSS colorsWeb browsers / web pages

Did you know?

  • There’s a black and white mode with patterns instead of colors in the palette, which you can get to from Image > Attributes…
  • You can drag the color box and tool box around if you grab them by the right place. You can even drag them out into little windows. You can dock the windows back to the side by double-clicking on their title bars.
  • In addition to the left-click foreground color and the right-click background color, there’s a third color you can access by holding Ctrl while you draw. It starts out with no color so you’ll need to hold Ctrl and select a color first. The fancy thing about this color slot is you can press and release Ctrl to switch colors while drawing.
  • You can apply image transformations like Flip/Rotate, Stretch/Skew or Invert (in the Image menu) either to the whole image or to a selection. Try scribbling with the Free-Form Select tool and then doing Image > Invert
  • These Tips and Tricks from a tutorial for MS Paint also work in JS Paint:
    •  Brush Scaling (+ & - on the number pad to adjust brush size)
    •  “Custom Brushes” (hold Shift and drag the selection to smear it)
    •  The ‘Stamp’ “Tool” (hold Shift and click the selection to stamp it)
    •  Image Scaling (+ & - on the number pad to scale the selection by factors of 2)
    •  Color Replacement (right mouse button with Eraser to selectively replace the foreground color with the background color)
    •  The Grid (Ctrl+G & Zoom to 4x+)
    •  Quick Undo (Pressing a second mouse button cancels the action you were performing. I also made it redoable, in case you do it by accident!)
    •  Scroll Wheel Bug (Hmm, let’s maybe not recreate this?)

Phaser 3 game examples written in TypeScript

Play the games

Alpha adjust

Alpha adjust

Asteroid

Asteroid

Blockade

Blockade

Blocks

Blocks

Breakout

Breakout

Candy Crush

Candy Crush

Clocks

Clocks

Coin runner

Coin runner

Endless runner

Endless runner

Flappy Bird

Flappy Bird

Snake

Snake

Space Invaders

Space Invaders

Super Mario Land

Super Mario Land

Tank

Tank

Run the games in your computer

Since all the games are open source, you can run all the games in your own computer. Just follow these steps:

  1. Install Nodejs (in case you don’t have it yet).
  2. Download the zip file containing the source code of the game you like.
  3. Open a terminal inside the project folder.
  4. Execute “npm install”.
  5. Execute “npm run dev”.
  6. Open the address “http://localhost:8080/” on your browser.

You can download the source code from the following links:

More information

You will find more information on github.

Phaser. Unit 9. Drawing maps from plain text

Introduction

In this unit we will define a simple Phaser game that can be used as a template to define an unlimited number of levels, enemies and objects. The map will be drawn from a simple array of tiles where several characters will be used to choose the positions of each item.

We will also use npm and webpack to provide a friendly development environment, so that the application will be automatically compiled and restarted each time a file is changed. A web server is also provided by webpack to test the application, and a production version of the code can be also obtained to upload it to any public server.

Some features

These are some of the features to be implemented:

  • Show several types of animated objects that can be collected individually.
  • Keep several counters with the number of the items collected.
  • Show several types of animated enemies that can be killed just by jumping over them.
  • Move automatically the enemies from left to right and viceversa using spritesheets that contain only a single direction.
  • Draw the map using plain characters that are matched againt tiles, objects, enemies, player, and goal.
  • Easily define an unlimited number of levels and let the player jump from one level to the other just by reaching a goal in the map.
  • Display a minimap automatically generated from the real sized map.
  • Play a different background music for each level, and a different sound when either objects are collected or enemies are killed.
  • Allow the user to play the game on mobile phones and tablets with touch controls to jump and also to activate both left and right movements.
  • Implement PWA features to let the users install the application in mobile devices and also to lock the screen in landscape mode.
  • Adjust the game automatically to the size of the window where the game is displayed, and also after the window is resized on desktop computers.

Loading the assets (src/scripts/scenes/preloadScene.ts)

Inside this file we will load all the assets (both audio and images) so that they are ready to be played or displayed after the game is started:

import Levels from "../components/levels/levels"

export default class PreloadScene extends Phaser.Scene {
  constructor() {
    super({ key: 'PreloadScene' })
  }

  preload() {
    const images = ['tile-left', 'tile-middle', 'tile-right', 'tile-single', 'controls', 'background', 'goal']
    images.forEach(img => {
      this.load.image(img, `assets/img/${img}.png`)
    })

    this.load.spritesheet('player', 'assets/img/player.png', { frameHeight: 165, frameWidth: 120 })
    this.load.spritesheet('coin', 'assets/img/coin.png', { frameHeight: 42, frameWidth: 42 })
    this.load.spritesheet('key', 'assets/img/key.png', { frameHeight: 176, frameWidth: 176 })
    this.load.spritesheet('bee', 'assets/img/bee.png', { frameHeight: 100, frameWidth: 128 })
    this.load.spritesheet('slime', 'assets/img/slime.png', { frameHeight: 68, frameWidth: 112 })
    // @ts-ignore
    this.load.spine('boy', 'assets/spine/boy.json', 'assets/spine/boy.atlas')

    const audios = ['bee', 'slime', 'coin', 'key', 'door', 'hurt', 'music']
    audios.forEach(audio => {
      this.load.audio(audio, `assets/audio/${audio}.mp3`)
    })

    for(let i=0; i<Levels.length; i++) {
      this.load.audio(`level${i}`, `assets/audio/level${i}.mp3`)
    }
  }

  create() {
    this.scene.start('MainScene')
  }
}

Drawing the map and placing everything on it

We will use plain characters to represent the player, the goal, the objects, the enemies and the ground:

  • P: Player
  • G: Goal (the door to jump to the next level)
  • Objects:
    • C: Coin
    • K: Key (at least 1 key is required to open the door to the next level)
  • Enemies:
    • B: Bee (a flying enemy)
    • S: Slime (an enemy crawling on the ground)
  • [//////]: The ground

Defining the levels (src/scripts/components/levels/levels.ts)

As it can be seen in the “levels.ts” file, the map of each level can be easily defined using simple characters. We can also place all the enemies, objects, player and goal at any position in the map:

type Level = string[]

// prettier-ignore
const level0: Level = [
  '   K    S       ',
  '  [/////]       ',
  '                ',
  '   P         G  ',
  '[/////]   [////]',
]

// prettier-ignore
const level1: Level = [
  '               B  K                   ',
  '               [/////]                ',
  '              C     C                 ',
  '   S        B            S    S    G  ',
  '[//]       [////]   [////]   [//////] ',
  '    C   C                             ',
  ' P        B                           ',
  '[///]  [//]                           ',
]

// prettier-ignore
const level2: Level = [
  ' P                     [//]       G   ',
  '[/]    C   B        C           [/]   ',
  '         [//]     [//]                ',
  '      S                     B  B      ',
  ' C  [//]      B            [///]      ',
  '            [///]     []  C        C  ',
  '[]        K                           ',
  '         [/]            [/]      [/]  '
]

// prettier-ignore
const level3: Level = [
  '       C                                    ',
  '                      C                 C   ',
  '      [///]                 C      [////]   ',
  '           [///]       S  [//]  K           ',
  '                   [///]      [///]         ',
  '             S                              ',
  '         [///]           C              G   ',
  '      C            C               [////]   ',
  '  P                     S  B                ',
  '[//////]       [///]  [////]                '
]

// prettier-ignore
const level4: Level = [
  '                        C  C         S                                  ',
  '                                 [//////]      K     B               G  ',
  '                  C    [/////]                [///////]          [////] ',
  '                                                                        ',
  '  P      S     B    B              C       S     S         S            ',
  '[////////]  [///////]             [/]    [////////////////////]         '
]

const Levels: Level[] = [level0, level1, level2, level3, level4]
export default Levels

Matching characters to textures (src/scripts/components/map/map.ts)

In case we want to add new enemies or objects, we will have to match a new character against the new texture:

import Levels from '../levels/levels'
export class Map {
  info: TilesConfig[]
  size: MapSize

  public static calcCurrentLevel(currentLevel: number) {
    const MAX_LEVELS = Levels.length
    return currentLevel % MAX_LEVELS
  }

  constructor(currentLevel: number) {
    const TILE_SIZE = 96
    const config: any = {
      '[': {
        type: 'tile',
        texture: 'tile-left'
      },
      '/': {
        type: 'tile',
        texture: 'tile-middle'
      },
      ']': {
        type: 'tile',
        texture: 'tile-right'
      },
      'G': {
        type: 'goal',
        texture: 'goal'
      },
      'C': {
        type: 'object',
        texture: 'coin'
      },
      'K': {
        type: 'object',
        texture: 'key'
      },      
      'S': {
        type: 'enemy',
        texture: 'slime'
      },
      'B': {
        type: 'enemy',
        texture: 'bee'
      },
      'P': {
        type: 'player',
        texture: 'player'
      }
    }

    const map = Levels[Map.calcCurrentLevel(currentLevel)]

    // the player can jump a bit higher than the map's height
    const paddingTop = 4 * TILE_SIZE

    this.size = {
      x: 0,
      y: 0,
      width: map[0].length * TILE_SIZE,
      height: map.length * TILE_SIZE + paddingTop
    }
    this.info = []

    map.forEach((row, y) => {
      for (let i = 0; i < row.length; i++) {
        const tile = row.charAt(i)
        const x = i
        if (tile !== ' ') {
          let info = { ...config[tile.toString()], x: x * TILE_SIZE, y: y * TILE_SIZE + paddingTop }
          this.info.push(info)
        }
      }
    })
  }
}

Enemies (src/scripts/components/enemies/enemiesGroup.ts)

All the tiles in the map are read and the enemies are created depending on the texture field:

import BeeSprite from './bee'
import SlimeSprite from './slime'

export default class EnemiesGroup extends Phaser.GameObjects.Group {
  tiles: TilesConfig[]
  TILE_SIZE = 96
  constructor(scene: Phaser.Scene, tilesConfig: TilesConfig[]) {
    super(scene)

    this.tiles = tilesConfig.filter(tile => tile.type === 'tile')
    let enemyTypes = tilesConfig.filter(tile => tile.type === 'enemy')

    let enemies: Array<BeeSprite | SlimeSprite> = []
    enemyTypes.forEach(enemy => {
      switch (enemy.texture) {
        case 'bee':
          enemies.push(new BeeSprite(scene, enemy.x, enemy.y))
          break
        case 'slime':
          enemies.push(new SlimeSprite(scene, enemy.x, enemy.y))
          break
      }
    })
    this.addMultiple(enemies)
  }

  update() {
    // check if the enemy should change its direction
    // @ts-ignore
    this.children.iterate((enemy: BeeSprite | SlimeSprite) => {
      if (enemy.dead) return

      let enemyIsMovingRight = enemy.body.velocity.x >= 0

      let hasGroundDetection = this.tiles.filter(tile => {
        let enemyPositionX = enemyIsMovingRight ? enemy.body.right : enemy.body.left
        let x = enemyPositionX + 32 > tile.x && enemyPositionX - 32 < tile.x + this.TILE_SIZE
        let y =
          enemy.body.bottom + this.TILE_SIZE / 2 > tile.y &&
          enemy.body.bottom + this.TILE_SIZE / 2 < tile.y + this.TILE_SIZE
        return x && y
      })

      if (hasGroundDetection.length === 0) {
        //@ts-ignore
        enemy.body.setVelocityX(enemy.body.velocity.x * -1)
        enemy.setFlipX(!enemyIsMovingRight)
      }
    }, null)
  }
}

The bee (src/scripts/components/enemies/bee.ts)

Frames 0 and 1 represent the usual movement, while frame number 2 is used when the bee is dead:

Bee enemy (assets/img/bee.png)
Dead bee (assets/audio/bee.mp3)
import EnemyClass from './enemyClass'

export default class BeeSprite extends EnemyClass {

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'bee')
    scene.add.existing(this)
    scene.physics.add.existing(this)

    scene.anims.create({
      key: 'fly',
      frames: scene.anims.generateFrameNumbers('bee', { start: 0, end: 1 }),
      frameRate: 8,
      repeat: -1
    })
    this.play('fly')

    //@ts-ignore
    this.body.setVelocityX(-120)
    this.setOrigin(0.5, 1)
    this.body.setSize(80, 135)
    this.body.setOffset((this.width - 80) / 2, 30)

    this.audio = scene.sound.add('bee', { volume: 1.0 })
  }

  update() { }

  kill() {
    if (this.dead) return

    this.body.setSize(80, 40)

    this.removeEnemy(2) // Frame number 2 is used when the bee is dead
  }
}

The slime (src/scripts/components/enemies/slime.ts)

Frames from 0 to 4 represent the usual movement, while frame number 5 is used when the slime is dead:

Slime enemy (assets/img/slime.png)
Dead slime (assets/audio/slime.mp3)
import EnemyClass from './enemyClass'

export default class SlimeSprite extends EnemyClass {

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'slime')
    scene.add.existing(this)
    scene.physics.add.existing(this)

    scene.anims.create({
      key: 'crawl',
      frames: scene.anims.generateFrameNumbers('slime', { start: 0, end: 4 }),
      frameRate: 6,
      yoyo: true,
      repeat: -1
    })
    this.play('crawl')

    //@ts-ignore
    this.body.setVelocityX(-60)
    this.setOrigin(0.5, 1)
    this.setScale(1)
    this.body.setSize(this.width - 40, this.height - 20)
    this.body.setOffset(20, 20)

    this.audio = scene.sound.add('slime', { volume: 1.0 })
  }

  update() { }

  kill() {
    if (this.dead) return

    this.removeEnemy(5) // Frame number 5 is used when the bee is dead
  }
}

Killing the enemies (src/scripts/scenes/mainScene.ts)

The player kills an enemy when it jumps over it. As a reward, it will get 5 points:

... 
   this.physics.add.overlap(this.player, this.enemiesGroup, (player: Player, enemy: EnemySprite) => {
      if (enemy.dead) return
      if (enemy.body.touching.up && player.body.touching.down) {
        this.score += 5
        scoreText.update(this.score, this.keys)
        player.killEnemy()
        enemy.kill()
      } else {
        player.kill()
      }
    })
...

Objects (src/scripts/components/objects/objectsGroup.ts)

All the tiles in the map are read and the objects are created depending on the texture field:

import CoinSprite from './coin'
import KeySprite from './key'

export default class ObjectsGroup extends Phaser.GameObjects.Group {
  constructor(scene: Phaser.Scene, tilesConfig: TilesConfig[]) {
    super(scene)

    let objectTypes = tilesConfig.filter(tile => tile.type === 'object')

    let objects: Array<CoinSprite | KeySprite> = []
    objectTypes.forEach(object => {
      switch (object.texture) {
        case 'coin':
          objects.push(new CoinSprite(scene, object.x, object.y))
          break
        case 'key':
          objects.push(new KeySprite(scene, object.x, object.y))
          break
      }
    })

    this.addMultiple(objects)
  }
}

The coin (src/scripts/components/objects/coin.ts)

All the frames are selected to create the animation:

The coin (assets/img/coin.png)
Collecting coin (assets/audio/coin.mp3)
import ObjectClass from "../objects/objectClass";

export default class CoinSprite extends ObjectClass {

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'coin')
    scene.add.existing(this)
    scene.physics.add.existing(this)

    this.setImmovable()
    this.setScale(1.5)
    // @ts-ignore
    this.body.setAllowGravity(false)

    scene.anims.create({
      key: 'spincoin',
      frames: scene.anims.generateFrameNames('coin'),
      frameRate: 16,
      repeat: -1
    })
    this.play('spincoin')

    this.audio = scene.sound.add('coin', { volume: 1.0 })
  }
}

The key (src/scripts/components/objects/key.ts)

All the frames are selected to create the animation:

The key (assets/img/key.png)
Collecting key (assets/audio/key.mp3)
import ObjectClass from "./objectClass";

export default class CoinSprite extends ObjectClass {

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'key')
    scene.add.existing(this)
    scene.physics.add.existing(this)

    this.setImmovable()
    this.setScale(0.7)
    // @ts-ignore
    this.body.setAllowGravity(false)

    scene.anims.create({
      key: 'spinkey',
      frames: scene.anims.generateFrameNames('key'),
      frameRate: 10,
      repeat: -1
    })
    this.play('spinkey')

    this.audio = scene.sound.add('key', { volume: 1.0 })
  }
}

Collecting objects (src/scripts/scenes/mainScene.ts)

The counters of coins and keys are incremented when any of these objects is collected:

...
    this.physics.add.overlap(this.player, objectsGroup, (player: Player, object: ObjectClass) => {
      if (object.collecting) return
      if (object instanceof CoinSprite) this.score++
      else if (object instanceof KeySprite) this.keys++
      scoreText.update(this.score, this.keys)
      object.collect()
    })
...

The player (src/scripts/components/player/player.ts)

The get a more realistic movement, the player is animated using the “spine” plugin. The full documentation about this plugin can be found here, and some examples can be found here.

Player animation using spine plugin (assets/spine/boy.png)
Player killed (assets/audio/hurt.mp3)
import Controls from '../controls/controls'
import PlayerSpine from './playerSpine'

export default class Player extends Phaser.Physics.Arcade.Sprite {
  private _dead: boolean = false
  private _halt: boolean = false
  private mapSize: MapSize
  playerSpine: PlayerSpine
  audioHurt: any

  constructor(scene: Phaser.Scene, player: TilesConfig, mapSize: MapSize, level: number) {
    super(scene, player.x, player.y, player.texture)
    scene.add.existing(this)
    scene.physics.add.existing(this)

    this.scene = scene
    this.mapSize = mapSize

    this.setVisible(false)

    this.setOrigin(0, 1)
    this.setDragX(1500)
    this.body.setSize(70, 132)
    this.body.setOffset(25, 24)

    let theSkin = level % 2 == 0 ? 'blue' : 'green'
    this.playerSpine = new PlayerSpine(scene, this.body.center.x, this.body.bottom)
    this.playerSpine.setSkin(theSkin)

    this.audioHurt = scene.sound.add('hurt', { volume: 1.0 })
  }

  kill() {
    this._dead = true

    this.audioHurt.play()

    // animate the camera if the player dies
    this.scene.cameras.main.shake(500, 0.025)
    this.scene.time.addEvent({
      delay: 500,
      callback: () => this.scene.scene.restart()
    })
  }

  killEnemy() {
    this.playerSpine.spine.customParams.isKilling = true
    this.setVelocityY(-600)
  }

  halt() {
    this.body.enable = false
    this._halt = true
  }

  update(cursors: any, controls: Controls) {
    if (this._halt || this._dead) return

    // check if out of camera and kill
    if (this.body.right < this.mapSize.x || this.body.left > this.mapSize.width || this.body.top > this.mapSize.height)
      this.kill()

    // controls left & right
    if (cursors.left.isDown || controls.leftIsDown) {
      this.setVelocityX(-500)
      this.playerSpine.spine.setScale(-1, 1)
    } else if (cursors.right.isDown || controls.rightIsDown) {
      this.setVelocityX(550)
      this.playerSpine.spine.setScale(1, 1)
    }
    // controls up
    if ((cursors.up.isDown || cursors.space.isDown || controls.upIsDown) && this.body.blocked.down) {
      this.setVelocityY(-1250)
    }

    // update spine animation
    this.playerSpine.update(this)
  }
}

The goal (src/scripts/components/goal/goalSprite.ts)

When the user collides with the door, an achievement sound will be played, and it will jump to the next level.

The goal (assets/img/goal.png)
Next level (assets/audio/door.mp3)
export default class GoalSprite extends Phaser.Physics.Arcade.Sprite {
  private _loadNextLevel: boolean = false;
  private audio: any;

  constructor(scene: Phaser.Scene, tilesConfig: TilesConfig) {
    super(scene, tilesConfig.x, tilesConfig.y + 14, 'goal')
    scene.add.existing(this)
    scene.physics.add.existing(this)

    this.setImmovable(true)
    // @ts-ignore
    this.body.setAllowGravity(false)
    this.setOrigin(0, 0.5)

    this.audio = scene.sound.add('door', { volume: 1.0 })
  }

  get loadNextLevel() {
    return this._loadNextLevel
  }

  nextLevel(scene: Phaser.Scene, level: number, score: number, keys: number) {
    if (this._loadNextLevel) return
    this._loadNextLevel = true

    this.audio.play()

    scene.cameras.main.fadeOut()
    scene.time.addEvent({
      delay: 2000,
      callback: () => {
        scene.scene.restart({ level: level += 1, score: score, keys: keys })
      }
    })
  }
}

Jumping to the next level (src/scripts/scenes/mainScene.ts)

After colliding with the door, the user will only jump to the next level in case it collected previously at least one key:

...
    this.physics.add.overlap(this.player, this.goal, (player: Player, goal: GoalSprite) => {
      if (!this.keys) return
      player.halt()
      this.keys--
      goal.nextLevel(this, this.level, this.score, this.keys)
    })
...

Progressive Web App (PWA)

This game is 100% PWA ready, so that it can be installed and executed in your mobile device as it was an app from the Play Store. You can easily personalize its settings by following these steps:

  • Replace both icons in /pwa/icons with your own.
    • One is 512×512 the other 192×192.
  • Add your own favicon.ico to /src.
  • Adjust these parameters in the manifest.json file in /pwa:
    • short_name: Max. 12 characters.
    • name: The full game name.
    • orientation: “landscape” or “portrait”.
    • background_color: color of the splash screen.
    • theme_color: color of the navbar – has to match the theme-color in the index.html file.
  • You can leave the sw.js (serviceWorker) file in /pwa as it is.
  • Change the gameName in /webpack/webpack.common.js

Read more about PWA on developers.google.com.

Running the game

You can get the game up and running just following these steps:

  1. Install nodejs (in case you do not have installed it yet). Select the version recommended for most users and just click next on each option to use the default settings.
  2. Download the code. You can download from this link a single zip file containing the full game template and all the assets.
  3. Uncompress the zip file, and open a terminal from inside the project folder.
  4. Execute “npm install” to install all the dependencies requested by the project.
  5. Execute “npm run start” to compile and execute the code. After this command is completed, a new tab will be opened in your default browser. The game will also be refreshed automatically when the source code changes.
  6. Execute “npm run build” to get a production version. After executing this command, all the html, css and js files, and also all the assets required to run the game will be put inside the “dist” folder, ready to be uploaded to any public web server. For example, you could make your game available to anyone just by creating a new project in https://replit.com and uploading all the files inside “dist” to the root folder of the replit project.

Play the game on your browser

You have also a version ready to be played online here.

Install the game in your phone

Since all the PWA functionality is included in this project, you can install your application in any mobile device (Chrome browser and Android operating system are the preferred options for this purpose). You will get the same behaviour as the apps in the play store: an icon will be created after carrying out the installation from your browser, a loading screen will be shown when the game is started, and the game will run in fullscreen and landscape mode.

When you open the address of the game in your browser, a message to install the application is displayed after a few seconds:

Installing the app

Or you can also install the game manually just by selecting the option “Instalar aplicación” from the browser menu:

Installing the app

Bibliography

More information about the template used to build this game can be found on github.

Animated sprite editors and pixel art tools

Introduction

You will find in this post several free and open-source 2D sprite editors and pixel art tools that can be used on your browser to create fancy pictures and animations.

Pixelorama

Pixelorama is an awesome free and open source pixel art editor, proudly created with the Godot engine, by Orama Interactive. Whether you want to make animated pixel art, game graphics, tiles and any kind of pixel art you want, Pixelorama has you covered with its variety of tools and features (you can find online documentation here). Free to use for everyone:

Pixelorama

Current features

  • A variety of different tools to help you draw, with the ability to map a different tool in each left and right mouse buttons.
  • Are you an animator? Pixelorama has its own animation timeline just for you! You can work at an individual cel level, where each cel refers to a unique layer and frame. Supports onion skinning, cel linking, motion drawing and frame grouping with tags.
  • Custom brushes, including random brushes.
  • Create or import custom palettes.
  • Import images and edit them inside Pixelorama. If you import multiple files, they will be added as individual animation frames. Importing spritesheets is also supported.
  • Export your gorgeous art as PNG, as a single file, a spritesheet or multiple files, or GIF file.
  • Pixel perfect mode for perfect lines, for the pencil, eraser & lighten/darken tools.
  • Autosave support, with data recovery in case of a software crash.
  • Horizontal & vertical mirrored drawing.
  • Tile mode for pattern creation.
  • Rectangular & isometric grid types.
  • Scale, rotate and apply multiple image effects to your drawings.
  • Multi-language localization support! See the Crowdin page for more details.

You can find more information on github.

Piskel

Piskel is an easy-to-use sprite editor. It can be used to create game sprites, animations, pixel-art, etc.:

Piskel

The Piskel editor is purely built in JavaScript, HTML and CSS, using also the following libraries :

  • spectrum : awesome standalone colorpicker.
  • gifjs : generate animated GIFs in javascript, using webworkers.
  • supergif : modified version of SuperGif to parse and import GIFs.
  • jszip : create, read and edit .zip files with Javascript.
  • canvas-toBlob : shim for canvas toBlob.
  • jquery : used sporadically in the application.
  • bootstrap-tooltip : nice tooltips.

You can find more information on github.

Pixel Art to CSS

Did you know that you can create pixel art using CSS? Pixel Art to CSS is an online editor that helps you with that task. Combining the power of both box-shadow and keyframes CSS properties, you will get CSS code ready to use in your site. Furthermore, you can download your work in different formats such as a static image, animated GIF or sprite like image.

Pixel Art to CSS aims to be an intuitive tool by its simplicity, however it is equipped with a wide range of features: customize your color palette, go back and forth in time, modify animation settings, save or load your projects, among others.

Pixel Art to CSS

This application has been built with the following technologies:

  • React: Library to build the UI.
  • Redux: Implements a Flux like architecture.
  • ImmutableJS: Helps to keep the data immutable aiming to avoid side effects.
  • PostCSS: Handle the app CSS.
  • NodeJS + Express: Optional server side to build an universal application, create and serve the generated drawings.

You can find more information on gihub.

Pixel art

A bit old, and not fully functional, but still, another interesting browser based pixel art maker and animation tool:

Pixel art

You will find more information on github.

Phaser. Unit 7. Dino game clone (part I)

Introduction

In this unit we will build a clone of Google Chrome “secret” Dino game (the original game can be revealed by entering “chrome://dino” in the address bar). We will use simple web technologies such as HTML, CSS, JavaScript and Phaser 3, so that any user can play on any device just using any browser (not only Chrome).

The result is a game which looks almost the same as the original one using less than 10% lines of code. As it can be seen from the Chromium source code, the original game is made of more than 4000 lines of code, whereas the one in this unit has been developed using around 300 lines of code. This means that Phaser could be a much better option than simple JavaScript when thinking about game development.

Some features

This version of the game clones almost every feature of the original one:

  • The speed of the game is increased gradually.
  • Both cactuses and birds will appear randomly to hurt you.
  • Two scores are shown: current and highest.
  • Each time the user reaches 100 points, the score blinks and a sound is played.
  • It can be played on desktop computers using both the mouse and the keyboard:
    • By clicking on the screen to jump or to restart the game.
    • Using the up arrow key or the space bar to jump, the down arrow key to duck under the enemy, and the enter key to restart the game.
  • It can be played on mobile devices by touching the screen, the same way as any single button game.
  • The game is automatically adjusted to fit the screen size, even if it changes after the page is loaded (for example, in case the user rotates the device).
  • Vibration is activated on mobile devices when the game over screen appears.

CSS code (styles.css)

We are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that the whole window size is used when the game is started:

html, body {
  margin: 0px;
  padding: 0px;
  overflow: hidden;
}

HTML code (index.html)

We are going to use a single HTML file to link all the files in the project. This is going to be the “index.html” file:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="description" content="Dino game clone using Phaser">
  <meta name="keywords" content="HTML, CSS, JavaScript, Phaser">
  <meta name="author" content="Fernando Ruiz Rico">
  
  <title>Dino Phaser</title>

  <link href="styles.css" rel="stylesheet">
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  <script src="preload.js"></script>
  <script src="create.js"></script>
  <script src="update.js"></script>
  <script src="config.js"></script>
</body>

</html>

Constants and Phaser configuration (config.js)

We are going to define some constants (i.e. screen width and height and text styles) so that we can use them inside every JavaScript file. We will also define a function to set the initial values of some variables before the game is started, together with the usual Phaser configuration, which must be performed as usual.

Moreover we are adding a piece of code to check whether the screen is being resized (i.e. when the device is rotated or the window size is adjusted) so that the whole game is reloaded to fit the new values of the screen width and height.

// Fill the whole width and height of the screen
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

// Time in milliseconds for each obsticle (lower = faster)
const OBSTICLE_TIME = 750;

// Text style to be used when printing texts (scores, etc.)
const TEXT_STYLE = { fill: "#535353", font: '900 35px Courier', resolution: 10 };

// Variables to be initialized each time the game is restarted
function initVariables() {
  this.isGameRunning = false;
  this.gameSpeed = 10;
  this.respawnTime = 0;
  this.score = 0;
}

// Phaser initialization
let game = new Phaser.Game({
  type: Phaser.AUTO,
  width: WIDTH,
  height: HEIGHT,
  pixelArt: true,
  transparent: true,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: { preload: preload, create: create, update: update }
});

// Reload the game when the device orientation changes or the user resizes the window
let resizing = false;
window.addEventListener('resize', () => {
  if (resizing) clearTimeout(resizing);
  resizing = setTimeout(() => window.location.reload(), 500);
});

The assets

We will need some images, sprites and sounds. You may use any assets you like, but we are providing the original ones so that you can easily develop the same game as the one available in Google Chrome. Some examples are shown below, and also a zip file containing all the assets can be downloaded here.

Images

Cloud
Cactuses
Game over
Restart

Sprites

Dino waiting and running
Bird enemy

Sounds

Hit
Jump
Reach

Loading all the assets (preload.js)

So that we can use all those images, sprites and sounds, they have to be loaded previously. That is the purpose of the “preload()” function.

Inside the “initSounds()” and “createAnims()” we will set the volume of the sounds and the frames of the animations respectively.

// Load all the audios, images and sprites
function preload() {
  this.load.audio('jump', 'assets/jump.m4a');
  this.load.audio('hit', 'assets/hit.m4a');
  this.load.audio('reach', 'assets/reach.m4a');

  this.load.image('ground', 'assets/ground.png');
  this.load.image('dino-idle', 'assets/dino-idle.png');
  this.load.image('dino-hurt', 'assets/dino-hurt.png');
  this.load.image('restart', 'assets/restart.png');
  this.load.image('game-over', 'assets/game-over.png');
  this.load.image('cloud', 'assets/cloud.png');

  this.load.image('obsticle-1', 'assets/cactuses_small_1.png');
  this.load.image('obsticle-2', 'assets/cactuses_small_2.png');
  this.load.image('obsticle-3', 'assets/cactuses_small_3.png');
  this.load.image('obsticle-4', 'assets/cactuses_big_1.png');
  this.load.image('obsticle-5', 'assets/cactuses_big_2.png');
  this.load.image('obsticle-6', 'assets/cactuses_big_3.png');

  this.load.spritesheet('dino', 'assets/dino-run.png', { frameWidth: 88, frameHeight: 94 });
  this.load.spritesheet('enemy-bird', 'assets/enemy-bird.png', { frameWidth: 92, frameHeight: 77 });
}

// Set the volume of each audio
function initSounds() {
  this.jumpSound = this.sound.add('jump', { volume: 0.75 });
  this.hitSound = this.sound.add('hit', { volume: 0.75 });
  this.reachSound = this.sound.add('reach', { volume: 0.75 });
}

// Define all the animations from the previously loaded spritesheets
function createAnims() {
  this.anims.create({
    key: 'dino-waiting',
    frames: this.anims.generateFrameNumbers('dino', { start: 0, end: 1 }),
    frameRate: 1,
    repeat: -1
  })

  this.anims.create({
    key: 'dino-run',
    frames: this.anims.generateFrameNumbers('dino', { start: 2, end: 3 }),
    frameRate: 10,
    repeat: -1
  })

  this.anims.create({
    key: 'enemy-bird-anim',
    frames: this.anims.generateFrameNumbers('enemy-bird', { start: 0, end: 1 }),
    frameRate: 6,
    repeat: -1
  })
}

Creating the scores, the clouds, the dino, and the game over message (create.js)

  • Scores: Two boxes are used to print both the current and highest scores on the right hand side of the screen.
  • Clouds: A loop will be used to create 3 clouds at once printing the same image on random positions.
  • Dino: The dino will collide with the ground below, and it will jump using the keys defined here (in our case we will use both the up cursor key and the space bar). The mouse functionality is already available by default.
  • Game over: A message will be shown when the user collides with either the birds or the cactuses. After the user clicks on it, this message will be hidden using the “setAlpha(0)” function call.
// Score text on the right side of the screen
function createScore() {
  this.scoreText = this.add.text(WIDTH - 25, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1);
  this.highScoreText = this.add.text(0, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1).setAlpha(0.75);
}

// Three clouds hidden at the beginning
function createClouds() {
  this.clouds = this.add.group();
  for (let i = 0; i < 3; i++) {
    const x = Phaser.Math.Between(0, WIDTH);
    const y = Phaser.Math.Between(HEIGHT / 3, HEIGHT / 2);
    this.clouds.add(this.add.image(x, y, 'cloud'));
  }
  this.clouds.setAlpha(0);
}

// Dino starts running and the the ground is created
function createGround() {
  this.startText.destroy();
  this.dino.setVelocityX(100);
  this.dino.play('dino-run', 1);

  const interval = setInterval(() => {
    this.ground.width += (WIDTH / 100);
    if (this.ground.width >= WIDTH) {
      clearInterval(interval);
      this.dino.setVelocityX(0);
      this.scoreText.setAlpha(1);
      this.clouds.setAlpha(1);
      this.isGameRunning = true;
    }
  }, 50);
}

// Dino waiting animation, keyboard initilization and starting text
function createDino() {
  this.dino = this.physics.add.sprite(0, HEIGHT / 1.5, 'dino-idle').setSize(50).setGravityY(5000).setOrigin(0, 1);
  this.dino.play('dino-waiting', true);

  this.physics.add.collider(this.dino, this.belowGround);
  this.physics.add.collider(this.dino, this.obsticles, () => stopGame.call(this));

  this.cursors = this.input.keyboard.createCursorKeys();
  this.spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

  this.startText = this.add.text(WIDTH - 15, HEIGHT / 1.5, "Press to play", TEXT_STYLE).setOrigin(1);
}

// Game over screen and pause until player starts the game again
function createGameOverScreen() {
  this.gameOverText = this.add.image(0, 0, 'game-over');
  this.restart = this.add.image(0, 50, 'restart').setInteractive();
  this.gameOverScreen = this.add.container(WIDTH / 2, HEIGHT / 2 - 50);
  this.gameOverScreen.add([this.gameOverText, this.restart]);

  this.input.keyboard.on('keydown_ENTER', () => startGame.call(this));
  this.restart.on('pointerdown', () => startGame.call(this));
}

// Define the physical ground and obsticles group
function createWorld() {
  this.ground = this.add.tileSprite(0, HEIGHT / 1.5, 88, 26, 'ground').setOrigin(0, 1);
  this.belowGround = this.add.rectangle(0, HEIGHT / 1.5, WIDTH, HEIGHT / 3).setOrigin(0, 0);
  this.physics.add.existing(this.belowGround);
  this.belowGround.body.setImmovable();
  this.obsticles = this.physics.add.group();
}

// Create all the elements in the game and initilize the score
function create() {
  initVariables.call(this);
  initSounds.call(this);
  createAnims.call(this);
  createWorld.call(this);
  createClouds.call(this);
  createScore.call(this);
  createDino.call(this);

  setInterval(() => updateScore.call(this), 100);
}

Controlling the player and displaying the birds and the cactuses (update.js)

  • Starting the game and building the ground: The “createGround()” method will build the ground progresively, just once, when the game is started the very first time (after the user taps the screen or presses either the up arrow key or the space bar).
  • Restarting the game: After the game over message appears, the user may click on the screen to start a new game. This is done inside the “startGame()” method, which clears the message using the “destroy()” method call, restarts the score and resumes all the physics.
  • Updating the scores: The function “updateScore()” will not only update the current score, but it will also increment the game speed, and it will play a sound each time the user reaches 100 points, making the numbers blink at the same time. The “updateHighScore()” function will keep a record of the highest score each time a game is over.
  • Placing and moving the birds and the cactuses: The “placeObsticle()” function will create either birds or cactuses randomly, and the “update()” function will move everything to the left except the dino, which will always remain in the same position.
// Restart the game after dino is hurt
function startGame() {
  this.dino.setVelocityY(0);
  this.physics.resume();
  this.anims.resumeAll();
  this.obsticles.clear(true, true);
  this.gameOverScreen.destroy();
  this.isGameRunning = true;
}

// On game over a sound is played and all physics are paused
// The game over screen will be shown and mobile devices will vibrate
function stopGame() {
  initVariables.call(this);
  updateHighScore.call(this);
  this.hitSound.play();
  this.physics.pause();
  this.anims.pauseAll();
  this.dino.setTexture('dino-hurt');
  createGameOverScreen.call(this);
  if (window.navigator.vibrate) window.navigator.vibrate(50);
}

// Update the score and increase the game speed
function updateScore() {
  if (!this.isGameRunning) return;

  this.score++;
  this.gameSpeed += 0.01;

  if (this.score % 100 == 0) {
    this.reachSound.play();

    this.tweens.add({
      targets: this.scoreText,
      duration: 100,
      repeat: 3,
      alpha: 0,
      yoyo: true
    })
  }

  this.scoreText.setText(("0000" + this.score).slice(-5));
}

// Update the high score in case is higher than the previous one
function updateHighScore() {
  this.highScoreText.x = this.scoreText.x - this.scoreText.width - 30;

  const highScore = this.highScoreText.text.substr(this.highScoreText.text.length - 5);
  const newScore = Number(this.scoreText.text) > Number(highScore) ? this.scoreText.text : highScore;

  this.highScoreText.setText(`HI ${newScore}`);
}

// Place a new obsticle on the screen (either cactuses or birds)
function placeObsticle() {
  let obsticle;
  const obsticleNum = Phaser.Math.Between(1, 7);

  if (obsticleNum > 6) {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5 - Phaser.Math.Between(20, 50), 'enemy-bird');
    obsticle.body.height /= 1.5
    obsticle.play('enemy-bird-anim', 1);
  } else {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5, `obsticle-${obsticleNum}`)
  }

  obsticle.setOrigin(0, 1).setImmovable();
}

// Update the screen (obsticles, ground, clouds and dino) and check the keyboard and mouse (or touch screen) continuously
// Dino will start running and jump if the user taps the screen or presses either the arrow up key or the spacebar  
function update() {
  let jump = this.cursors.up.isDown || this.spaceBar.isDown || this.input.activePointer.isDown;

  if (this.isGameRunning) {
    this.ground.tilePositionX += this.gameSpeed;

    this.respawnTime += this.gameSpeed;
    if (this.respawnTime >= OBSTICLE_TIME) {
      this.respawnTime = 0;
      placeObsticle.call(this);
    }

    this.obsticles.getChildren().forEach(obsticle => {
      obsticle.x -= this.gameSpeed;
      if (obsticle.getBounds().right < 0) {
        obsticle.destroy();
      }
    })

    this.clouds.getChildren().forEach(cloud => {
      cloud.x -= 0.5;
      if (cloud.getBounds().right < 0) {
        cloud.x = WIDTH;
      }
    })

    if (this.dino.body.onFloor()) {
      if (jump) {
        this.jumpSound.play();
        this.dino.anims.stop();
        this.dino.setVelocityY(-1600);
        this.dino.setTexture('dino', 0);
      }
      else {
        this.dino.play('dino-run', true);
      }
    }
  }
  else if (jump && (this.ground.width < WIDTH)) {
    createGround.call(this);
  }
}

Proposed exercise: Using your own assets

Create a new version of the Dino game using your own assets (images, sprites and sounds). You can also change any other things you like.

Do not forget to create all the files required to run the code in this unit:

  • styles.css
  • index.html
  • preload.js
  • create.js
  • update.js
  • config.js
  • assets folder

You may find many free assets in OpenGameArt, and some new versions of the Dino game in github:

Enjoy the game!

You can enjoy playing this wonderful game online here.