Average grade calculator with HTML+CSS+JavaScript

This project is a simple web application designed to calculate the average grade for eight modules in the course “Technician in Microcomputer Systems and Networks.” Below, each file will be explained and we will see how they are linked together.

File “index.html”

This is the backbone of the application. It defines the content and layout of the web page.

  • Page header:
    • Sets the title as “Average Grade Calculator.”
    • Links the CSS (style.css) for styling and JavaScript (script.js) for interactivity.
  • Main content:
    • A table lists eight modules, each with a grade and two buttons (- and +) to decrease or increase the grade.
    • An average grade is calculated and displayed at the bottom of the table.
  • Key elements:
    • Each grade cell has a unique ID (module1, module2, …, module8) to identify it.
    • Buttons use onclick attributes to trigger JavaScript functions (increaseGrade and decreaseGrade).
<!DOCTYPE html>
<html lang="en">

<head>
  <!-- Define the character encoding for the document -->
  <meta charset="UTF-8">
  <!-- Set the viewport to ensure the page is mobile-friendly -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- Link to an external stylesheet -->
  <link rel="stylesheet" href="style.css">
  <!-- Page title -->
  <title>Average Grade Calculator</title>
</head>

<body>
  <!-- Main heading of the page -->
  <h1>Technician in Microcomputer Systems and Networks - Grade calculator</h1>

  <!-- Table to display modules, grades, and actions -->
  <table>
    <!-- Table header -->
    <thead>
      <tr>
        <th>Module</th>
        <th>Grade</th>
        <th>Actions</th>
      </tr>
    </thead>
    <!-- Table body -->
    <tbody>
      <!-- Each row corresponds to a module -->
      <tr>
        <td>Assembly and Maintenance of Computers</td>
        <td id="module1">0</td>
        <td>
          <!-- Buttons to decrease and increase the grade -->
          <button onclick="decreaseGrade('module1')">-</button>
          <button onclick="increaseGrade('module1')">+</button>
        </td>
      </tr>
      <tr>
        <td>Single-user Operating Systems</td>
        <td id="module2">0</td>
        <td>
          <button onclick="decreaseGrade('module2')">-</button>
          <button onclick="increaseGrade('module2')">+</button>
        </td>
      </tr>
      <tr>
        <td>Office Automation Applications</td>
        <td id="module3">0</td>
        <td>
          <button onclick="decreaseGrade('module3')">-</button>
          <button onclick="increaseGrade('module3')">+</button>
        </td>
      </tr>
      <tr>
        <td>Network Operating Systems</td>
        <td id="module4">0</td>
        <td>
          <button onclick="decreaseGrade('module4')">-</button>
          <button onclick="increaseGrade('module4')">+</button>
        </td>
      </tr>
      <tr>
        <td>Local Networks</td>
        <td id="module5">0</td>
        <td>
          <button onclick="decreaseGrade('module5')">-</button>
          <button onclick="increaseGrade('module5')">+</button>
        </td>
      </tr>
      <tr>
        <td>Computer Safety</td>
        <td id="module6">0</td>
        <td>
          <button onclick="decreaseGrade('module6')">-</button>
          <button onclick="increaseGrade('module6')">+</button>
        </td>
      </tr>
      <tr>
        <td>Network Services</td>
        <td id="module7">0</td>
        <td>
          <button onclick="decreaseGrade('module7')">-</button>
          <button onclick="increaseGrade('module7')">+</button>
        </td>
      </tr>
      <tr>
        <td>Web Applications</td>
        <td id="module8">0</td>
        <td>
          <button onclick="decreaseGrade('module8')">-</button>
          <button onclick="increaseGrade('module8')">+</button>
        </td>
      </tr>
    </tbody>
    <!-- Table footer to display the average grade -->
    <tfoot>
      <tr>
        <td>Average</td>
        <td id="result" colspan="2">0</td>
      </tr>
    </tfoot>
  </table>
  
  <!-- Link to the JavaScript file for functionality -->
  <script src="script.js"></script>
</body>

</html>

File “script.js”

This JavaScript file contains the logic for updating the grades and calculating the average.

  1. Global variable:
    • sum: Tracks the total sum of all grades.
  2. Functions:
    • increaseGrade(moduleId):
      • Checks if the grade is less than 10.
      • Increases the grade by 1 and updates the total sum.
      • Recalculates the average by calling calculateAverage.
    • decreaseGrade(moduleId):
      • Checks if the grade is greater than 0.
      • Decreases the grade by 1 and updates the total sum.
      • Recalculates the average.
    • calculateAverage():
      • Divides the total sum by the number of modules (8).
      • Displays the result in the table footer with two decimal places.
// Variable to store the total sum of grades
let sum = 0;

/**
 * Increases the grade for the given module if it's less than 10.
 * Updates the total sum and recalculates the average.
 */
function increaseGrade(moduleId) {
    // Get the current grade for the module and check if it's below 10
    if (document.getElementById(moduleId).innerText < 10) {
        // Increment the grade for the module
        document.getElementById(moduleId).innerText++;
        // Increment the total sum
        sum++;
        // Recalculate and update the average
        calculateAverage();
    }
}

/**
 * Decreases the grade for the given module if it's greater than 0.
 * Updates the total sum and recalculates the average.
 */
function decreaseGrade(moduleId) {
    // Get the current grade for the module and check if it's above 0
    if (document.getElementById(moduleId).innerText > 0) {
        // Decrement the grade for the module
        document.getElementById(moduleId).innerText--;
        // Decrement the total sum
        sum--;
        // Recalculate and update the average
        calculateAverage();
    }
}

/**
 * Calculates and updates the average grade.
 * Assumes there are 8 modules.
 */
function calculateAverage() {
    // Calculate the average and display it with 2 decimal places
    document.getElementById('result').innerText = (sum / 8).toFixed(2);
}

File “style.css”

This file styles the HTML elements to make the interface visually appealing.

  • Body styling:
    • Sets a clean sans-serif font for readability.
  • Table styling:
    • Makes the table full-width and centers its content.
    • Adds borders around cells for clarity.
    • Header rows (<thead>) have a dark green background with bold, white text for contrast.
  • Buttons:
    • Styled with a green background, rounded corners, and hover effects to darken the color.
/* Global font settings: use a sans-serif font for the entire page */
body {
    font-family: Arial, sans-serif; /* Set a clean sans-serif font */
}

/* Basic styles for the table */
table {
    width: 100%; /* Make the table take up the full width of the window */
    border-collapse: collapse; /* Remove spacing between table cells */
    text-align: center; /* Center align text within table cells */
}

/* Add a border to table headers and cells */
th, td {
    border: 1px solid black; /* Solid black border around cells */
}

/* Table header styles */
thead th {
    background-color: #006b5f; /* Set a dark green background color */
    color: #ffffff; /* Use white text for contrast */
    font-weight: bold; /* Make text bold */
    padding: 15px; /* Add padding for better spacing */
}

/* Table cell styles */
td {
    padding: 12px; /* Add padding inside cells for better readability */
    vertical-align: middle; /* Vertically center align cell content */
    text-align: center; /* Default horizontal alignment is centered */
}

/* Align the text in the first column to the left */
td:first-child {
    text-align: left; /* Left-align only the first cell in each row */
}

/* Button styles */
button {
    background-color: #009879; /* Use a soft green background */
    color: white; /* Set white text for contrast */
    border: none; /* Remove the default border */
    padding: 5px 10px; /* Add padding inside the button */
    cursor: pointer; /* Show a pointer cursor on hover */
    border-radius: 5px; /* Add rounded corners */
}

/* Change button appearance when hovered */
button:hover {
    background-color: #007a63; /* Darken the button background on hover */
}

How the files work together

  1. Initial page load:
    • The grades are set to 0.
    • The average is displayed as 0.
  2. User interaction:
    • Clicking + increases the grade of the selected module (up to 10).
    • Clicking - decreases the grade (down to 0).
  3. Real-time update:
    • The JavaScript functions dynamically adjust the grades and calculate the new average without refreshing the page.

The result

You can click here to see how this average grade calculator works.

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.

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.

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.

Phaser. Unit 6. Using tilemaps to build a space shooter game

The source code and the assets

You can download this zip file to get the source code and all the assets.

Updating the map

As done previously, you can use the tile map editor to update the map. In case you have not done it yet, you can download the editor from this link and install it on your computer.

To update the map you have to go the assets folder and open the file “town.tmx” which contains the map of the example in this unit. Select a layer (on the right corner, at the top) and change anything you like (by adding or removing some objects to or from the map). When you finish, export the map to JSON format using the option “File” -> “Export as…”, and overwrite the file “town.json”, located inside the “assets” folder.

Finally check the results using your browser and move the player to go to each part of the map. You will notice that the camera will follow you on each movement. Do not forget to fully refresh the contents of the browser by pressing Ctrl+F5, since the map and some other contents are usually cached by the browser. Also move the player and check that the map has been updated with the changes you have made.

Frames per second (FPS)

You can update the number of frames per second to choose the best renderization performance for your computer:

const MAX_FPS = 25;

Using the keyboard and the mouse

In this game we are using the keys A (left), S (down), D (right), W (up) to move the player on desktop computers and the joystick for mobile devices:

  let vX = 0, vY = 0;

  // Horizontal movement
  if (wsadKeys.A.isDown || joyStick.left) vX = -speed;
  else if (wsadKeys.D.isDown || joyStick.right) vX = speed;

  // Vertical movement
  if (wsadKeys.W.isDown || joyStick.up) vY = -speed;
  else if (wsadKeys.S.isDown || joyStick.down) vY = speed;

The mouse is used for shooting on desktop computers:

  let pointer = this.input.activePointer;
  if (pointer.isDown && this.sys.game.device.os.desktop) createBullet.call(this, pointer.worldX - player.x, pointer.worldY - player.y);

In case you do not have any mouse, you may use the cursor keys on desktop computers, or the second joystick on mobile devices, to shoot the bullets and to choose the shooting direction:

  let vX = 0, vY = 0;

  // Horizontal bullets
  if (cursors.left.isDown || joyStick2.left) vX = -speed;
  else if (cursors.right.isDown || joyStick2.right) vX = speed;

  // Vertical bullets
  if (cursors.up.isDown || joyStick2.up) vY = -speed;
  else if (cursors.down.isDown || joyStick2.down) vY = speed;

  if (vX || vY) createBullet.call(this, vX, vY);

Enjoy the game!

You may enjoy playing this wonderful game online here.