PhaserJS Platformer Aim & Shoot Mechanics

PhaserJS Platformer Aim & Shoot Mechanics

Using independently rotated arms and raycasting.

Today I'll be teaching how to create a mouse-based aim and shoot use game mechanic in PhaserJS. This is useful if you're looking to create a platformer-shooter, run and gun style game.

Play the demo

This article assumes reasonable familiarity with JavaScript and at least some knowledge of PhaserJS (version 3).

Clone the repo for this project to follow along commit by commit.

After cloning the repo, enter it using your favourite terminal program, install dependencies, and test it locally like so:

cd phaserjs-mouse-aim
npm install
npm run start

This will open a new tab in your browser and direct it to localhost:8080, where your copy of the game should be running happily.

Pro tip: this tutorial is broken into commits rather than steps. At the beginning of each commit (step) of this tutorial is a git checkout command with the commit's unique hash. Use this command to follow along with the source code locally.

With all that said and done, let's dive in!

Commit 1: scene architecture & assets.

commit1.png

git checkout e169e23b35e8e419921a331c151cbb077e877925

The first thing we're going to do in this tutorial is to load up my (world class) example assets and architect our scene structure.

The assets we will be loading are as follows:

  • The player's body and legs in one sprite.
  • The player's head in one sprite.
  • The player's left arm and gun in one sprite.
  • The player's right arm in one sprite.

The reason I'm breaking the body parts of our player into separate Sprite game objects is so that the right arm, left arm, and head can be rotated independently.

The independent movement of the player's arms and head will enable us to point them at the mouse, regardless of the player's position on the screen.

Working in Phaser is awesome because there aren't really any hard and fast rules on how to structure your project's files. That said, in this tutorial I'm going to organise our game's scenes as follows:

  • /src/scenes/BootScene.js: Used to load assets, you could put a preloader here, configure animations or anything else you deem "boot-time-ish"
  • /src/scenes/GameScene.js: Used to contain the actual game logic, in bigger games you'd want to split your game into multiple scenes for levels, title screen, etc. but in this tutorial we'll keep it simple

In this first commit, the state of our code is as follows:

/src/scenes/BootScene.js

import { Scene } from 'phaser';

// Files to preload
import playerBodyPNG from '../assets/player-body.png';
import playerBodyJSON from '../assets/player-body.json';
import playerGunArmPNG from '../assets/player-gun-arm.png';
import playerGunArmJSON from '../assets/player-gun-arm.json';
import playerArm from '../assets/player-arm.png';
import playerHead from '../assets/player-head.png';

class BootScene extends Scene {
  constructor() {
    super('scene-boot');
  }

  preload() {
    // Preload our assets
    this.load.aseprite('player-body', playerBodyPNG, playerBodyJSON);
    this.load.aseprite('player-gun-arm', playerGunArmPNG, playerGunArmJSON);
    this.load.image('player-arm', playerArm);
    this.load.image('player-head', playerHead);
  }

  create() {
    // Create our animations
    this.anims.createFromAseprite('player-body');
    this.anims.createFromAseprite('player-gun-arm');

    this.scene.start('scene-game');
  }
}

export default BootScene;

In the first iteration of BootScene.js, we load all of our game's files into memory, then parse the Aseprite animation information for our two spritesheets, and finally change to the game scene.

/src/scenes/GameScene.js

import { Scene } from 'phaser';

class GameScene extends Scene {
  constructor() {
    super('scene-game');
  }

  create() {
    this.add.image(100, 100, 'player-head').setScale(2);

    this.cameras.main.setBackgroundColor(0x2299CC);
  }
}

export default GameScene;

In the first iteration of GameScene.js, we simply add one of our image assets (the player's head) to the game world at an X, Y position of 100, 100. We then scale it up to 2 and set the main camera's background colour to a light blue so we can see our guy's head a little easier.

/src/index.js

import Phaser from 'phaser';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';

const config = {
    type: Phaser.AUTO,
    parent: 'game',
    width: 1600,
    height: 840,
    scene: [
        BootScene,
        GameScene
    ]
};

const game = new Phaser.Game(config);

In the first iteration, our index.js file imports both the boot scene and the game scene from their respective files and initialises a fresh Phaser game with them.

Pro tip: the order of the scenes in the scene array matters. In our case we want BootScene to fire first, so we put it at the top. It's worth noting too, that if you're running multiple scenes in parallel (when making a game HUD for example) scenes at the bottom of the array will render above scenes at the top of the array.

At this stage our game will preload and parse our images and spritesheets, then switch to a fresh game scene in which we simply display the player's head to confirm our preloading worked.

Commit 2: add the Player class and assemble his body parts.

commit2.gif

git checkout 6437aff1579e551d537fb050ac8982c1019cacfa

In the second commit, I created a Player class which extends Phaser's built-in Container game object, and contains all the bits of the player we loaded in the previous commit.

After a bit of good ol' fiddling around, I set the origin points and coordinates of each part of the player so that they all line up nicely and he just looks like one, solid character.

/src/sprites/Player.js

import { GameObjects } from "phaser";

const { Container } = GameObjects;

class Player extends Container {
  constructor(scene, x, y) {
    super(scene, x, y, []);

    // Create a reference to our Player instance's parent scene
    this.scene = scene;
    this.scene.add.existing(this);

    // Add his body
    this.legsTorso = this.scene.add.sprite(0, 0, 'player-body');

    // Add and position his head
    this.head = this.scene.add.sprite(3, -23, 'player-head');
    this.head.setOrigin(0.5, 1);

    // Add, position, and angle his left arm
    this.leftArm = this.scene.add.sprite(-2, -20, 'player-gun-arm');
    this.leftArm.setOrigin(0.5, 0);
    this.leftArm.setAngle(-45);
    this.leftArm.play('gun-arm-idle');

    // Add, position, and angle his right arm
    this.rightArm = this.scene.add.sprite(-2, -19, 'player-arm');
    this.rightArm.setOrigin(0.5, 0);
    this.rightArm.setAngle(-45);

    // Add all the body parts to this container
    this.add([
      this.leftArm,
      this.legsTorso,
      this.head,
      this.rightArm
    ]);

    // Scale him up to 200%
    this.setScale(2);
  }

  // The preUpdate function runs every frame
  preUpdate() {
    // Play his run animation (and ignore if already playing)
    this.legsTorso.play('player-run', true);
  }
}

export default Player;

In this iteration, the Player class is a simple container with his torso/legs sprite, head, left arm, and right arm that is scaled up to 200% size, and plays his running animation.

/src/scenes/GameScene.js

import { Scene } from 'phaser';
import Player from '../sprites/Player';

class GameScene extends Scene {
  constructor() {
    super('scene-game');
  }

  create() {
    this.player = new Player(this, 300, 300);

    this.cameras.main.setBackgroundColor(0x2299CC);
  }
}

export default GameScene;

In this iteration, we import the Player class and add him to the scene, assigning him to this.player (in the game scene).

By the end of commit 2, we have a simple player rendering on the screen as if he's just one sprite (but we know he's multiple sprites).

Commit 3: add a map, physics, and controls.

commit3.gif

git checkout f7996ad752b57f9f5bd09460bf4d1a9748522181

In this commit, we load a map (JSON file from Tiled and an extruded tileset) and add it to our game scene, enable physics in our Player class, and create some basic control logic leveraging Phaser's arcade physics.

Pro tip: extruding a tileset is necessary to avoid visual "tearing" between map tiles when the camera moves.

Here's the files we hit in this commit:

/src/index.js

import Phaser from 'phaser';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';

const config = {
    type: Phaser.AUTO,
    parent: 'game',
    width: 1600,
    height: 840,
    // Enable and configure arcade physics engine
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 500 }
        }
    },
    scene: [
        BootScene,
        GameScene
    ]
};

const game = new Phaser.Game(config);

The only change I made here was adding configuration for Phaser's physics engine, in our case arcade.

/src/scenes/BootScene.js

import { Scene } from 'phaser';

// Files to preload
import playerBodyPNG from '../assets/player-body.png';
import playerBodyJSON from '../assets/player-body.json';
import playerGunArmPNG from '../assets/player-gun-arm.png';
import playerGunArmJSON from '../assets/player-gun-arm.json';
import playerArm from '../assets/player-arm.png';
import playerHead from '../assets/player-head.png';
import map from '../assets/map.json';
import tiles from '../assets/tiles-ex.png';

class BootScene extends Scene {
  constructor() {
    super('scene-boot');
  }

  preload() {
    // Preload our assets
    this.load.aseprite('player-body', playerBodyPNG, playerBodyJSON);
    this.load.aseprite('player-gun-arm', playerGunArmPNG, playerGunArmJSON);
    this.load.image('player-arm', playerArm);
    this.load.image('player-head', playerHead);
    this.load.tilemapTiledJSON('map', map);
    this.load.image('tiles', tiles);
  }

  create() {
    // Create our animations
    this.anims.createFromAseprite('player-body');
    this.anims.createFromAseprite('player-gun-arm');

    this.scene.start('scene-game');
  }
}

export default BootScene;

The difference to our boot scene is we're adding two calls in preload() to load in our Tiled tilemap JSON, as well as the extruded tileset.

/src/scenes/GameScene.js

import { Scene } from 'phaser';
import Player from '../sprites/Player';

class GameScene extends Scene {
  constructor() {
    super('scene-game');
  }

  create() {
    // Add our loaded tilemap
    this.map = this.add.tilemap('map');

    // Assign tiles to a tileset image from map (accounting for default extrusion)
    const tiles = this.map.addTilesetImage('tiles', 'tiles', 32, 32, 1, 2);

    // Create a ground layer from our map, and set it's collision property
    this.ground = this.map.createLayer('ground', tiles);
    this.ground.setCollisionByProperty({ collides: true });

    // Loop through all the objects in our 'sprites' object layer
    this.map.getObjectLayer('sprites').objects.forEach(({x, y, name}) => {
      // When we find one named player, add the player at it's x, y
      if (name === 'player') {
        this.player = new Player(this, x, y);
      }
    });

    // Make it so the player collides with the ground / walls
    this.physics.add.collider(this.player, this.ground);

    // Configure the camera so it:
    //   - Follows the player
    //   - Is zoomed in by 200%
    //   - Is bounded by our map's real width and height
    //   - Has a blue background
    this.cameras.main.startFollow(this.player);
    this.cameras.main.setZoom(2);
    this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
    this.cameras.main.setBackgroundColor(0x2299CC);
  }
}

export default GameScene;

In our game scene, we start by creating the world's map from the data we preloaded. We setup collisions and spawn the player based on it's position denoted in a Tiled object layer and configure the camera such that it follows the player, is zoomed in, and is bounded by the width and height of our map in pixels (not tiles).

/src/sprites/Player.js

import { GameObjects, Input } from "phaser";

const { Container } = GameObjects;

class Player extends Container {
  constructor(scene, x, y) {
    super(scene, x, y, []);

    // Create a reference to our Player instance's parent scene, and enable physics
    this.scene = scene;
    this.scene.add.existing(this);
    this.scene.physics.world.enable(this);

    // Add his body
    this.legsTorso = this.scene.add.sprite(0, 0, 'player-body');

    // Add and position his head
    this.head = this.scene.add.sprite(3, -23, 'player-head');
    this.head.setOrigin(0.5, 1);

    // Add, position, and angle his left arm
    this.leftArm = this.scene.add.sprite(-2, -20, 'player-gun-arm');
    this.leftArm.setOrigin(0.5, 0);
    this.leftArm.setAngle(-45);
    this.leftArm.play('gun-arm-idle');

    // Add, position, and angle his right arm
    this.rightArm = this.scene.add.sprite(-2, -19, 'player-arm');
    this.rightArm.setOrigin(0.5, 0);
    this.rightArm.setAngle(-45);

    // Add all the body parts to this container
    this.add([
      this.leftArm,
      this.legsTorso,
      this.head,
      this.rightArm
    ]);

    // Set the bounding box offset and size (this takes some fiddling)
    this.body.setOffset(-15, -52);
    this.body.setSize(this.legsTorso.displayWidth, this.legsTorso.displayHeight + this.head.displayHeight);

    // Create a helper object for W(S)AD keys, speed, and jump force variables
    this.cursors = this.scene.input.keyboard.addKeys({
      up: Input.Keyboard.KeyCodes.W,
      left: Input.Keyboard.KeyCodes.A,
      right: Input.Keyboard.KeyCodes.D
    });
    this.speed = 200;
    this.jumpForce = 450;
  }

  // The preUpdate function runs every frame
  preUpdate() {
    const {speed, jumpForce} = this;
    const {up, left, right} = this.cursors;
    const isGrounded = this.body.blocked.down;

    // Keyboard control logic
    if (left.isDown) {
      this.body.setVelocityX(-speed);
      this.setFlipX(true);
    }
    else if (right.isDown) {
      this.body.setVelocityX(speed);
      this.setFlipX(false);
    }
    else {
      this.body.setVelocityX(0);
    }

    if (isGrounded && up.isDown) {
      this.body.setVelocityY(-jumpForce);
    }

    // Animation logic
    if (this.body.velocity.x !== 0) {
      this.legsTorso.play('player-run', true);
    }
    else {
      this.legsTorso.play('player-idle', true);
    }
  }

  setFlipX(flip) {
    // Go over all sprites in the container and apply flip
    this.list.forEach((child) => {
      child.setFlipX(flip);
    });

    // Invert the body part x offsets depending on flip
    if (flip) {
      this.head.setPosition(-3, -23);

      this.leftArm.setPosition(2, -20);
      this.leftArm.setAngle(45);

      this.rightArm.setPosition(2, -19);
      this.rightArm.setAngle(45);
    }
    else {
      this.head.setPosition(3, -23);

      this.leftArm.setPosition(-2, -20);
      this.leftArm.setAngle(-45);

      this.rightArm.setPosition(-2, -19);
      this.rightArm.setAngle(-45);
    }
  }
}

export default Player;

We start by enabling world physics (the arcade physics we configured before) on our player's instance in the game scene. After that we mess around with his body's bounding box (hit box) until it fits him nicely (this can take a bit of fiddling to get these numbers right, I usually start by setting the size to the torso width, and torso + head height).

After that, we create some variables to store keyboard helpers (used later in the preUpdate() function), along with a movement speed, and jump force. In preUpdate() we add some basic keyboard control logic (including a call to a custom-defined setFlipX() function), and simple animation logic which plays his running animation if he's moving on the x axis.

Finally we define a custom setFlipX() function which iterates through all the container's children and flips each individual sprite. It then will invert the x offsets we defined for each body part, depending on if the flip argument is true or false.

Pro tip: PhaserJS Containers don't ship with a setFlipX() function like Sprites do, however we can add our own easily enough, as illustrated above.

And that's it for commit 3. We now have a simple game where our special Container sprite can run and jump around a simple tilemap world.

Commit 4: Aiming with the mouse.

commit4.gif

git checkout 93798d57a8c4002b068b9c467b594b0f0528e8cf

The only file we touch in commit 4 is the Player class. The only change is the addition of a "pointermove" event, detailed below...

/src/sprites/Player.js

import { GameObjects, Input, Math as pMath } from "phaser";

const { Container } = GameObjects;

class Player extends Container {
  constructor(scene, x, y) {
    super(scene, x, y, []);

    // Create a reference to our Player instance's parent scene, and enable physics
    this.scene = scene;
    this.scene.add.existing(this);
    this.scene.physics.world.enable(this);

    // Add his body
    this.legsTorso = this.scene.add.sprite(0, 0, 'player-body');

    // Add and position his head
    this.head = this.scene.add.sprite(3, -23, 'player-head');
    this.head.setOrigin(0.5, 1);

    // Add, position, and angle his left arm
    this.leftArm = this.scene.add.sprite(-2, -20, 'player-gun-arm');
    this.leftArm.setOrigin(0.5, 0.17);
    this.leftArm.play('gun-arm-idle');

    // Add, position, and angle his right arm
    this.rightArm = this.scene.add.sprite(-2, -19, 'player-arm');
    this.rightArm.setOrigin(0.5, 0.17);

    // Add all the body parts to this container
    this.add([
      this.leftArm,
      this.legsTorso,
      this.head,
      this.rightArm
    ]);

    // Set the bounding box offset and size (this takes some fiddling)
    this.body.setOffset(-15, -52);
    this.body.setSize(this.legsTorso.displayWidth, this.legsTorso.displayHeight + this.head.displayHeight);

    // Mouse controls
    this.aimAngle = 0;

    // Hook into the scene's pointermove event
    this.scene.input.on('pointermove', ({worldX, worldY}) => {
      // Calculate the angle between the player and the world x/y of the mouse, and offset it by Pi/2
      this.aimAngle = (pMath.Angle.Between(this.x, this.y, worldX, worldY) - Math.PI / 2);

      // Assign the rotation (in radians) to arms
      this.leftArm.setRotation(this.aimAngle);
      this.rightArm.setRotation(this.aimAngle);

      // If the mouse is to the left of the player...
      if (worldX < this.x) {
        // Flip the player to face left
        this.setFlipX(true);

        // Offset the head's angle and divide it by 2 to lessen the "strength" of rotation
        let headAngle = (this.aimAngle - Math.PI / 2) / 2;
        if (headAngle <= -2) {
          headAngle += 3.1; // No idea why we need to do this, sorry. Try commenting it out to see what happens lol
        }
        this.head.setRotation(headAngle);
      }
      // If the mouse is to the right of the player...
      else {
        // Flip the player to face right
        this.setFlipX(false);

        // Same as above but without the weird broken angle to account for ¯\_(ツ)_/¯
        this.head.setRotation((this.aimAngle + Math.PI / 2) / 2);
      }
    });

    // Create a helper object for W(S)AD keys, speed, and jump force variables
    this.cursors = this.scene.input.keyboard.addKeys({
      up: Input.Keyboard.KeyCodes.W,
      left: Input.Keyboard.KeyCodes.A,
      right: Input.Keyboard.KeyCodes.D
    });
    this.speed = 200;
    this.jumpForce = 450;
  }

  // The preUpdate function runs every frame
  preUpdate() {
    const {speed, jumpForce} = this;
    const {up, left, right} = this.cursors;
    const isGrounded = this.body.blocked.down;

    // Keyboard control logic
    if (left.isDown) {
      this.body.setVelocityX(-speed);
    }
    else if (right.isDown) {
      this.body.setVelocityX(speed);
    }
    else {
      this.body.setVelocityX(0);
    }

    if (isGrounded && up.isDown) {
      this.body.setVelocityY(-jumpForce);
    }

    // Animation logic
    if (this.body.velocity.x !== 0) {
      this.legsTorso.play('player-run', true);
    }
    else {
      this.legsTorso.play('player-idle', true);
    }
  }

  setFlipX(flip) {
    // Go over all sprites in the container and apply flip
    this.list.forEach((child) => {
      child.setFlipX(flip);
    });

    // Invert the body part x offsets depending on flip
    if (flip) {
      this.head.setPosition(-3, -23);
      this.leftArm.setPosition(2, -20);
      this.rightArm.setPosition(2, -19);
    }
    else {
      this.head.setPosition(3, -23);
      this.leftArm.setPosition(-2, -20);
      this.rightArm.setPosition(-2, -19);
    }
  }
}

export default Player;

The "pointermove" event added to Player calculates the angle between him and the mouse. It then transforms that angle (in radians) and applies it to the arms, transforms it some more, then applies it to his head.

The old calls to setRotation() were also removed to prevent conflicts with the setAngle() calls.

Pro tip: never mix radian and degree angles in Phaser. It gets confused.

So that wraps up commit 4, now we have a game where you can run around, jump, and aim with the mouse.

Commit 5: shooting (with raycasting).

commit5.gif

git checkout c466b2a01c2094662ba421292b6d320e42981161

In the final commit, we look at introducing a PhaserJS raycaster plugin to the game and use it to draw lines (bullet streaks) between the tip of the player's gun and either:

  • The object (tile) the "bullet" hits
  • An arbitrary point far enough away that the "bullet" shoots off-screen

3 files are changed in this commit, they are as follows:

/src/index.js

import Phaser from 'phaser';
import PhaserRaycaster from 'phaser-raycaster/dist/phaser-raycaster';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';

const config = {
    type: Phaser.AUTO,
    parent: 'game',
    width: 1600,
    height: 840,
    // Enable and configure arcade physics engine
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 500 }
        }
    },
    plugins: {
        scene: [
            {
                key: 'PhaserRaycaster',
                plugin: PhaserRaycaster,
                mapping: 'raycasterPlugin'
            }
        ]
    },
    scene: [
        BootScene,
        GameScene
    ]
};

const game = new Phaser.Game(config);

Here we're importing the PhaserRaycaster plugin and adding it to our game config.

/src/scenes/BootScene.js

import { Scene } from 'phaser';

// Files to preload
import playerBodyPNG from '../assets/player-body.png';
import playerBodyJSON from '../assets/player-body.json';
import playerGunArmPNG from '../assets/player-gun-arm.png';
import playerGunArmJSON from '../assets/player-gun-arm.json';
import playerArm from '../assets/player-arm.png';
import playerHead from '../assets/player-head.png';
import map from '../assets/map.json';
import tiles from '../assets/tiles-ex.png';
import sparkPNG from '../assets/spark.png';
import sparkJSON from '../assets/spark.json';

class BootScene extends Scene {
  constructor() {
    super('scene-boot');
  }

  preload() {
    // Preload our assets
    this.load.aseprite('player-body', playerBodyPNG, playerBodyJSON);
    this.load.aseprite('player-gun-arm', playerGunArmPNG, playerGunArmJSON);
    this.load.image('player-arm', playerArm);
    this.load.image('player-head', playerHead);
    this.load.tilemapTiledJSON('map', map);
    this.load.image('tiles', tiles);
    this.load.aseprite('spark', sparkPNG, sparkJSON);
  }

  create() {
    // Create our animations
    this.anims.createFromAseprite('player-body');
    this.anims.createFromAseprite('player-gun-arm');
    this.anims.createFromAseprite('spark');

    this.scene.start('scene-game');
  }
}

export default BootScene;

In our boot scene, our final commit sees us simply preloading and parsing animation data for a "spark" spritesheet (which we'll use in the next file).

/src/sprites/Player.js

import { GameObjects, Input, Math as pMath } from "phaser";

const { Container } = GameObjects;

class Player extends Container {
  constructor(scene, x, y) {
    super(scene, x, y, []);

    // Create a reference to our Player instance's parent scene, and enable physics
    this.scene = scene;
    this.scene.add.existing(this);
    this.scene.physics.world.enable(this);

    // Init raycasting
    const bulletRaycast = this.scene.raycasterPlugin.createRaycaster();
    bulletRaycast.mapGameObjects(this.scene.ground, true, {
      collisionTiles: [1, 2, 3]
    });
    this.bulletRay = bulletRaycast.createRay();

    // Add his body
    this.legsTorso = this.scene.add.sprite(0, 0, 'player-body');

    // Add and position his head
    this.head = this.scene.add.sprite(3, -23, 'player-head');
    this.head.setOrigin(0.5, 1);

    // Add, position, and angle his left arm
    this.leftArm = this.scene.add.sprite(-2, -20, 'player-gun-arm');
    this.leftArm.setOrigin(0.5, 0.17);
    this.leftArm.play('gun-arm-idle');

    // Add, position, and angle his right arm
    this.rightArm = this.scene.add.sprite(-2, -19, 'player-arm');
    this.rightArm.setOrigin(0.5, 0.17);

    // Add all the body parts to this container
    this.add([
      this.leftArm,
      this.legsTorso,
      this.head,
      this.rightArm
    ]);

    // Set the bounding box offset and size (this takes some fiddling)
    this.body.setOffset(-15, -52);
    this.body.setSize(this.legsTorso.displayWidth, this.legsTorso.displayHeight + this.head.displayHeight);

    // Mouse controls
    this.aimAngle = 0;

    // Hook into the scene's pointermove event
    this.scene.input.on('pointermove', ({worldX, worldY}) => {
      // Calculate the angle between the player and the world x/y of the mouse, and offset it by Pi/2
      this.aimAngle = (pMath.Angle.Between(this.x, this.y, worldX, worldY) - Math.PI / 2);

      // Assign the rotation (in radians) to arms
      this.leftArm.setRotation(this.aimAngle);
      this.rightArm.setRotation(this.aimAngle);

      // If the mouse is to the left of the player...
      if (worldX < this.x) {
        // Flip the player to face left
        this.setFlipX(true);

        // Offset the head's angle and divide it by 2 to lessen the "strength" of rotation
        let headAngle = (this.aimAngle - Math.PI / 2) / 2;
        if (headAngle <= -2) {
          headAngle += 3.1; // No idea why we need to do this, sorry. Try commenting it out to see what happens lol
        }
        this.head.setRotation(headAngle);
      }
      // If the mouse is to the right of the player...
      else {
        // Flip the player to face right
        this.setFlipX(false);

        // Same as above but without the weird broken angle to account for ¯\_(ツ)_/¯
        this.head.setRotation((this.aimAngle + Math.PI / 2) / 2);
      }
    });

    this.bulletGfx = this.scene.add.graphics();

    // Hook into pointerdown (click) event
    this.scene.input.on('pointerdown', ({}) => {
      this.leftArm.play('gun-arm-shoot');

      // Create a new vector and apply the left arm's angle
      const vector = new pMath.Vector2();
      vector.setToPolar(this.leftArm.rotation + Math.PI / 2, 35);

      // Set the bullet ray so it starts at the end of the vector, and has the same angle as the left arm
      this.bulletRay.setOrigin(this.x + vector.x, this.y + vector.y - 28);
      this.bulletRay.setAngle(this.leftArm.rotation + Math.PI / 2);

      // Cast the ray, and get any intersection
      const intersection = this.bulletRay.cast();

      // Start the line's end x/y at an arbitrary point far enough away that it's off-screen
      let endX = vector.x * 300;
      let endY = vector.y * 300;

      // If there's an intersection it means we hit something
      if (intersection) {
        // Reassign the end x/y to the intersection's position
        endX = intersection.x;
        endY = intersection.y;

        if (intersection.object) {
          // intersection.object contains the sprite or tile that you hit
          // Use this to apply damage, break things, etc.

          // Add a spark to where the ray intersects the tile
          const spark = this.scene.add.sprite(intersection.x, intersection.y, 'spark');
          // Set it's depth to 10 so it renders above everything else
          spark.setDepth(10);
          // Give it a random angle so it looks a little different on each hit
          spark.setAngle(pMath.Between(0, 360));
          // Play it's animation, and destroy itself when complete to free up memory
          spark.play('spark-spark');
          spark.on('animationcomplete', () => {
            spark.destroy();
          });
        }
      }

      // Set the bullet line's width, colour, and opacity
      this.bulletGfx.lineStyle(2, 0xFBF236, 1);
      // Draw the line from the end of the gun, to the end x/y we made above
      this.bulletGfx.lineBetween(this.x + vector.x, this.y + vector.y - 28, endX, endY);

      // Set an event 50 milliseconds in the future that clears the line graphic
      this.scene.time.addEvent({
        delay: 50,
        callback: () => {
          this.bulletGfx.clear();
        }
      })

    });

    // Animation reset (clear muzzle flare)
    this.leftArm.on('animationcomplete', ({key}) => {
      if (key === 'gun-arm-shoot') {
        this.leftArm.play('gun-arm-idle');
      }
    });

    // Create a helper object for W(S)AD keys, speed, and jump force variables
    this.cursors = this.scene.input.keyboard.addKeys({
      up: Input.Keyboard.KeyCodes.W,
      left: Input.Keyboard.KeyCodes.A,
      right: Input.Keyboard.KeyCodes.D
    });
    this.speed = 200;
    this.jumpForce = 450;
  }

  // The preUpdate function runs every frame
  preUpdate() {
    const {speed, jumpForce} = this;
    const {up, left, right} = this.cursors;
    const isGrounded = this.body.blocked.down;

    // Keyboard control logic
    if (left.isDown) {
      this.body.setVelocityX(-speed);
    }
    else if (right.isDown) {
      this.body.setVelocityX(speed);
    }
    else {
      this.body.setVelocityX(0);
    }

    if (isGrounded && up.isDown) {
      this.body.setVelocityY(-jumpForce);
    }

    // Animation logic
    if (this.body.velocity.x !== 0) {
      this.legsTorso.play('player-run', true);
    }
    else {
      this.legsTorso.play('player-idle', true);
    }
  }

  setFlipX(flip) {
    // Go over all sprites in the container and apply flip
    this.list.forEach((child) => {
      child.setFlipX(flip);
    });

    // Invert the body part x offsets depending on flip
    if (flip) {
      this.head.setPosition(-3, -23);
      this.leftArm.setPosition(2, -20);
      this.rightArm.setPosition(2, -19);
    }
    else {
      this.head.setPosition(3, -23);
      this.leftArm.setPosition(-2, -20);
      this.rightArm.setPosition(-2, -19);
    }
  }
}

export default Player;

In our Player class, we initialise a new raycaster for the player's gun and tell it to collide with all 3 of our tile types. Further down, we hook into another pointer event, this time the "pointerdown" event (triggers when the mouse is clicked).

In the "pointerdown" event handler, we play the firing animation on this.leftArm, then create and manipulate a new Vector2 object, configure the bullet ray so it lines up with the position and angle of the gun, cast the ray and handle any intersection. Finally we draw a line to represent the streak of the bullet, and clear it 50 milliseconds later.

We also hook into the "animationcomplete" event for this.leftArm so that we can return it to an idle state after the shooting animation completes.

</tutorial>

Thanks for reading! If you found this post useful for a game you're working on, please post a link to it in the comments. If you found a bug with this tutorial yell at me in the comments. If you just wanna say hi, say hi in the comments.

Also you can leave a comment.

A video version of this tutorial should be dropping sometime this week or so.

If you're a Hashnode member, follow me for more HTML5 game dev content! :)