Building a Phaser 3 Game with ECS and React
I recently decided to play around with Phaser 3 again with the help of the good people at Ourcade!
One thing I tasked myself with was to rebuild the Beginning ECS in Phaser 3 tutorial but have it work with ReactJS!
Ourcade is a playful #gamedev community for open-minded and optimistic learners and developers. 🎮🕹👾🤗
Looking to learn how to use an Entity Component System(ECS) in your Phaser 3 games?
Check out this Ourcade video that uses bitECS–the same library currently being used in the development of Phaser 4.
So before you continue with my tutorial, ensure you understand how the base project was built with Phaser 3, Parcel, and BitECS. I urge you to go check it out if you haven’t already.
The aim of this article is to show how you can use the tank game with ReactJS. The details regarding Entity Component System in Phaser have been omitted since it is covered in Beginning ECS in Phaser 3.
Before we dive in, all the code that goes along with this post is in this repository. This tutorial uses the latest version of Phaser (v3.55.2) as of 21st, Feb 2023.
Getting Started
Ensure you have node and npm installed on your system
navigate to your project directory where you want to save your work
// To create a typescript react app scaffold run the following Command
npx create-react-app <project-name> --template typescript
// Install Libraries to be used in creating phaser game within react
npm install --save phaser bitecs regenerator-runtime
Make Sure Your folder Structure is similar to the below screen-shot
No need for any setup in package.json as create-react-app does that for you
Setup your Phaser Configuration File
// src/PhaserGame.ts
import Phaser from 'phaser';
import { Bootstrap, Game } from './scenes'
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
parent: 'phaser-container',
backgroundColor: '#282c34',
scale: {
mode: Phaser.Scale.ScaleModes.RESIZE,
width: window.innerWidth,
height: window.innerHeight,
},
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: true
},
},
scene: [Bootstrap, Game],
}
export default new Phaser.Game(config)
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(<App />);
/* src/index.css */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
import your Phaser Configuration in App.tsx entry file, and ensure the div id is the same as the parent name in your Phaser Game Config, this allows phaser to convert the div tag to a canvas/WebGL depending on your browser
// src/App.tsx
import './App.css';
import './PhaserGame';
function App() {
return (<div id='phaser-container' className='App'></div>);
}
export default App;
/* src/App.css */
.App {
text-align: center;
}
Now that this has been done you don’t have to worry about anything other than creating your Game scenes, components, and systems.
Setup your scenes in this manner
// src/scenes/Bootstrap.ts
import Phaser from 'phaser'
export class Bootstrap extends Phaser.Scene
{
constructor()
{
super('bootstrap')
}
init()
{
}
preload()
{
// This Loads my assets created with texture packer
this.load.multiatlas('tankers', 'assets/tanker-game.json', 'assets');
}
create()
{
this.createNewGame();
}
update() {
}
private createNewGame()
{
// this launches the game scene
this.scene.launch('game');
}
}
I always like to create an entry point for my folder where I export all the files in that folder there just like below
// src/scenes/index.ts
export * from './Bootstrap';
export * from './Game';
This enables me to import this in any other file in the following Format
import { Bootstrap} from './scenes'
if you notice I don’t have to add ‘./scenes/Bootstrap’ because it has specified ‘./scenes/’ in the index.ts as the entry point for all files in the scenes folder for me
Now the Game Scene
// src/scenes/Game.ts
import Phaser from 'phaser'
import {
createWorld,
addEntity,
addComponent,
System,
IWorld,
} from 'bitecs'
import { Position, Rotation, Velocity, Sprite, Player, CPU, Input, ArcadeSprite, ArcadeSpriteStatic } from '../components'
import { createSpriteSystem, createMovementSystem, createPlayerSystem, createCPUSystem, createArcadeSpriteSystem, createArcadeSpriteStaticSystem } from '../systems'
enum Textures
{
TankBlue = 0,
TankRed = 1,
TankGreen = 2,
TankSand = 3,
TankDark = 4,
TreeBrownLarge = 5,
TreeBrownSmall = 6,
TreeGreenLarge = 7,
TreeGreenSmall = 8,
}
const TextureKeys = [
'tank_blue.png',
'tank_red.png',
'tank_green.png',
'tank_sand.png',
'tank_dark.png',
'treeBrown_large.png',
'treeBrown_small.png',
'treeGreen_large.png',
'treeGreen_small.png'
];
export class Game extends Phaser.Scene
{
private world?: IWorld
private spriteSystem?: System
private spriteStaticSystem?: System
private movementSystem?: System
private playerSystem?: System
private cpuSystem?: System
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys
constructor()
{
super('game')
}
init()
{
this.cursors = this.input.keyboard.createCursorKeys();
}
create()
{
const { width, height } = this.scale
this.world = createWorld();
const tank = addEntity(this.world);
addComponent(this.world, Position, tank)
Position.x[tank] = 200
Position.y[tank] = 200
addComponent(this.world, Rotation, tank)
addComponent(this.world, Velocity, tank)
addComponent(this.world, Input, tank)
// addComponent(this.world, Sprite, tank)
addComponent(this.world, ArcadeSprite, tank)
ArcadeSprite.texture[tank] = Textures.TankBlue
addComponent(this.world, Player, tank)
//TODO: Create Large Tree
const largeTree = addEntity(this.world)
addComponent(this.world, Position, largeTree)
addComponent(this.world, ArcadeSpriteStatic, largeTree)
Position.x[largeTree] = 400
Position.y[largeTree] = 400
ArcadeSpriteStatic.texture[largeTree] = Textures.TreeGreenLarge
//TODO: Create Small Tree
const smallTree = addEntity(this.world)
addComponent(this.world, Position, smallTree)
addComponent(this.world, ArcadeSpriteStatic, smallTree)
Position.x[smallTree] = 300
Position.y[smallTree] = 200
ArcadeSpriteStatic.texture[smallTree] = Textures.TreeBrownSmall
//TODO: Create random CPU Tanks
for (let i = 0; i < 5; i++) {
const cpuTank = addEntity(this.world);
addComponent(this.world, Position, cpuTank)
Position.x[cpuTank] = Phaser.Math.Between(width * 0.25, width * 0.75)
Position.y[cpuTank] = Phaser.Math.Between(height * 0.25, height *0.75)
addComponent(this.world, Rotation, cpuTank)
Rotation.angle[cpuTank] = 0;
addComponent(this.world, Velocity, cpuTank)
Velocity.x[cpuTank] = 0
Velocity.y[cpuTank] = 0
addComponent(this.world, ArcadeSprite, cpuTank)
ArcadeSprite.texture[cpuTank] = Phaser.Math.Between(1, 4)
addComponent(this.world, CPU, cpuTank)
CPU.timeBetweenActions[cpuTank] = Phaser.Math.Between(100,500)
addComponent(this.world, Input, cpuTank)
}
const spriteGroup = this.physics.add.group()
const spriteStaticGroup = this.physics.add.staticGroup()
this.physics.add.collider(spriteGroup, spriteStaticGroup)
this.physics.add.collider(spriteGroup, spriteGroup)
// this.spriteSystem = createSpriteSystem(this, TextureKeys)
this.spriteSystem = createArcadeSpriteSystem(spriteGroup, TextureKeys)
this.spriteStaticSystem = createArcadeSpriteStaticSystem(spriteStaticGroup, TextureKeys)
this.movementSystem = createMovementSystem()
this.playerSystem = createPlayerSystem(this.cursors)
this.cpuSystem = createCPUSystem(this)
console.log(Velocity);
}
update() {
if(!this.world) return
this.playerSystem?.(this.world)
this.cpuSystem?.(this.world)
this.movementSystem?.(this.world)
this.spriteSystem?.(this.world);
this.spriteStaticSystem?.(this.world);
}
}
If you haven't gone through this tutorial video: Beginning ECS in Phaser 3
I urge you to go back and check it out
The aim of this was to show you how to rebuild the tank game with ReactJS
Every other thing regarding Entity Component System in Phaser has been covered in Beginning ECS in Phaser 3
If you found this helpful please let me know what you think in the comments, You are welcome to suggest changes.
Thank you for the time spent reading this article.
You can follow me on all my social handles @officialyenum and please subscribe and 👏 would mean a lot thanks