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.
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.
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 wantBootScene
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.
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.
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
Container
s don't ship with asetFlipX()
function likeSprite
s 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.
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).
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! :)