├── .gitignore ├── public └── assets │ ├── tank_blue.png │ ├── tank_red.png │ └── tank_green.png ├── src ├── components │ ├── Player.ts │ ├── Sprite.ts │ ├── Rotation.ts │ ├── Position.ts │ ├── Velocity.ts │ ├── CPU.ts │ └── Input.ts ├── index.html ├── main.ts ├── systems │ ├── player.ts │ ├── cpu.ts │ ├── movement.ts │ └── sprite.ts └── scenes │ └── Game.ts ├── .eslintignore ├── .eslintrc.js ├── tsconfig.json ├── readme.md ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /dist 3 | /node_modules 4 | /.DS_Store 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /public/assets/tank_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ourcade/phaser3-bitecs-getting-started/HEAD/public/assets/tank_blue.png -------------------------------------------------------------------------------- /public/assets/tank_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ourcade/phaser3-bitecs-getting-started/HEAD/public/assets/tank_red.png -------------------------------------------------------------------------------- /public/assets/tank_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ourcade/phaser3-bitecs-getting-started/HEAD/public/assets/tank_green.png -------------------------------------------------------------------------------- /src/components/Player.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Player = defineComponent() 4 | 5 | export default Player 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Phaser3 + Parceljs Template 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/Sprite.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Sprite = defineComponent({ 4 | texture: Types.ui8 5 | }) 6 | 7 | export default Sprite 8 | -------------------------------------------------------------------------------- /src/components/Rotation.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Rotation = defineComponent({ 4 | angle: Types.f32 5 | }) 6 | 7 | export default Rotation 8 | -------------------------------------------------------------------------------- /src/components/Position.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Position = defineComponent({ 4 | x: Types.f32, 5 | y: Types.f32 6 | }) 7 | 8 | export default Position 9 | -------------------------------------------------------------------------------- /src/components/Velocity.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Velocity = defineComponent({ 4 | x: Types.f32, 5 | y: Types.f32 6 | }) 7 | 8 | export default Velocity 9 | -------------------------------------------------------------------------------- /src/components/CPU.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const CPU = defineComponent({ 4 | timeBetweenActions: Types.ui32, 5 | accumulatedTime: Types.ui32 6 | }) 7 | 8 | export default CPU 9 | -------------------------------------------------------------------------------- /src/components/Input.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, Types } from 'bitecs' 2 | 3 | export const Input = defineComponent({ 4 | direction: Types.ui8, 5 | speed: Types.ui8 6 | }) 7 | 8 | export enum Direction 9 | { 10 | None, 11 | Left, 12 | Right, 13 | Up, 14 | Down 15 | } 16 | 17 | export default Input -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser' 2 | 3 | import Game from './scenes/Game' 4 | 5 | const config: Phaser.Types.Core.GameConfig = { 6 | type: Phaser.AUTO, 7 | width: 800, 8 | height: 600, 9 | physics: { 10 | default: 'arcade', 11 | arcade: { 12 | gravity: { y: 200 } 13 | } 14 | }, 15 | scene: [Game] 16 | } 17 | 18 | export default new Phaser.Game(config) 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint' 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended' 11 | ], 12 | rules: { 13 | '@typescript-eslint/explicit-function-return-type': 0, 14 | '@typescript-eslint/ban-ts-ignore': 0, 15 | '@typescript-eslint/no-namespace': { 'allowDeclarations': true }, 16 | '@typescript-eslint/member-delimiter-style': 0, 17 | '@typescript-eslint/no-explicit-any': 0 18 | } 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "es6", 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "noEmit": true, 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "importHelpers": true, 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "sourceMap": true, 16 | "baseUrl": "./src", 17 | "paths": { 18 | "~/*": ["./*"] 19 | }, 20 | "typeRoots": [ 21 | "node_modules/@types", 22 | "node_module/phaser/types" 23 | ], 24 | "types": [ 25 | "phaser" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Entity Component System with Phaser 3 and bitECS 2 | > Example code for getting started with ECS in Phaser 3 3 | 4 | ![License](https://img.shields.io/badge/license-MIT-green) 5 | 6 | ## Overview 7 | 8 | This is the example code for getting started with using ECS (Entity Component System) with the bitECS library in Phaser 3. 9 | 10 | [It corresponds to a 4 part series on YouTube going over the code!](https://www.youtube.com/playlist?list=PLNwtXgWIx3rhz72-UxKLdCDdqFsnwNc_u) 11 | 12 | ## Getting Started 13 | 14 | Clone this repository with git and run: 15 | 16 | ``` 17 | npm install 18 | npm run start 19 | ``` 20 | 21 | Then go to http://localhost:8000 22 | 23 | ## License 24 | 25 | [MIT License](https://github.com/ourcade/phaser3-bitecs-getting-started/blob/master/LICENSE) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ourcade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/systems/player.ts: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser' 2 | import { 3 | defineSystem, 4 | defineQuery, 5 | } from 'bitecs' 6 | 7 | import Velocity from '../components/Velocity' 8 | import Rotation from '../components/Rotation' 9 | import Player from '../components/Player' 10 | import Input, { Direction } from '../components/Input' 11 | 12 | export default function createPlayerSystem(cursors: Phaser.Types.Input.Keyboard.CursorKeys) { 13 | const playerQuery = defineQuery([Player, Velocity, Rotation, Input]) 14 | 15 | return defineSystem((world) => { 16 | const entities = playerQuery(world) 17 | 18 | for (let i = 0; i < entities.length; ++i) 19 | { 20 | const id = entities[i] 21 | if (cursors.left.isDown) 22 | { 23 | Input.direction[id] = Direction.Left 24 | } 25 | else if (cursors.right.isDown) 26 | { 27 | Input.direction[id] = Direction.Right 28 | } 29 | else if (cursors.up.isDown) 30 | { 31 | Input.direction[id] = Direction.Up 32 | } 33 | else if (cursors.down.isDown) 34 | { 35 | Input.direction[id] = Direction.Down 36 | } 37 | else 38 | { 39 | Input.direction[id] = Direction.None 40 | } 41 | } 42 | 43 | return world 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser3-parcel-template", 3 | "version": "1.0.0", 4 | "description": "A typescript template project for Phaser 3 using Parceljs", 5 | "scripts": { 6 | "start": "parcel src/index.html -p 8000", 7 | "build": "parcel build src/index.html --out-dir dist", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx" 10 | }, 11 | "author": "supertommy", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ourcade/phaser3-parcel-template.git" 16 | }, 17 | "homepage": "https://github.com/ourcade/phaser3-parcel-template", 18 | "devDependencies": { 19 | "@typescript-eslint/eslint-plugin": "^2.29.0", 20 | "@typescript-eslint/parser": "^2.29.0", 21 | "eslint": "^6.8.0", 22 | "minimist": ">=1.2.2", 23 | "parcel-plugin-clean-easy": "^1.0.2", 24 | "parcel-plugin-static-files-copy": "^2.4.3", 25 | "typescript": "^3.8.3" 26 | }, 27 | "dependencies": { 28 | "bitecs": "^0.3.10-3", 29 | "phaser": "^3.55.2" 30 | }, 31 | "parcelCleanPaths": [ 32 | "dist" 33 | ], 34 | "staticFiles": { 35 | "staticPath": "public", 36 | "watcherGlob": "**" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/systems/cpu.ts: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser' 2 | import { 3 | defineSystem, 4 | defineQuery, 5 | } from 'bitecs' 6 | 7 | import CPU from '../components/CPU' 8 | import Velocity from '../components/Velocity' 9 | import Rotation from '../components/Rotation' 10 | import Input, { Direction} from '../components/Input' 11 | 12 | export default function createCPUSystem(scene: Phaser.Scene) { 13 | const cpuQuery = defineQuery([CPU, Velocity, Rotation, Input]) 14 | 15 | return defineSystem((world) => { 16 | const entities = cpuQuery(world) 17 | 18 | const dt = scene.game.loop.delta 19 | for (let i = 0; i < entities.length; ++i) 20 | { 21 | const id = entities[i] 22 | 23 | CPU.accumulatedTime[id] += dt 24 | 25 | if (CPU.accumulatedTime[id] < CPU.timeBetweenActions[id]) 26 | { 27 | continue 28 | } 29 | 30 | CPU.accumulatedTime[id] = 0 31 | 32 | switch (Phaser.Math.Between(0, 20)) 33 | { 34 | // left 35 | case 0: 36 | { 37 | Input.direction[id] = Direction.Left 38 | break 39 | } 40 | 41 | // right 42 | case 1: 43 | { 44 | Input.direction[id] = Direction.Right 45 | break 46 | } 47 | 48 | // up 49 | case 2: 50 | { 51 | Input.direction[id] = Direction.Up 52 | break 53 | } 54 | 55 | // down 56 | case 3: 57 | { 58 | Input.direction[id] = Direction.Down 59 | break 60 | } 61 | 62 | default: 63 | { 64 | Input.direction[id] = Direction.None 65 | break 66 | } 67 | } 68 | } 69 | 70 | return world 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/systems/movement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineSystem, 3 | defineQuery, 4 | } from 'bitecs' 5 | 6 | import Position from '../components/Position' 7 | import Velocity from '../components/Velocity' 8 | import Rotation from '../components/Rotation' 9 | import Input, { Direction } from '../components/Input' 10 | 11 | export default function createMovementSystem() { 12 | const movementQuery = defineQuery([Position, Velocity, Input, Rotation]) 13 | 14 | return defineSystem((world) => { 15 | const entities = movementQuery(world) 16 | 17 | for (let i = 0; i < entities.length; ++i) 18 | { 19 | const id = entities[i] 20 | 21 | const direction = Input.direction[id] 22 | const speed = Input.speed[id] 23 | 24 | switch (direction) 25 | { 26 | case Direction.None: 27 | Velocity.x[id] = 0 28 | Velocity.y[id] = 0 29 | break 30 | 31 | case Direction.Left: 32 | Velocity.x[id] = -speed 33 | Velocity.y[id] = 0 34 | Rotation.angle[id] = 180 35 | break 36 | 37 | case Direction.Right: 38 | Velocity.x[id] = speed 39 | Velocity.y[id] = 0 40 | Rotation.angle[id] = 0 41 | break 42 | 43 | case Direction.Up: 44 | Velocity.x[id] = 0 45 | Velocity.y[id] = -speed 46 | Rotation.angle[id] = 270 47 | break 48 | 49 | case Direction.Down: 50 | Velocity.x[id] = 0 51 | Velocity.y[id] = speed 52 | Rotation.angle[id] = 90 53 | break 54 | } 55 | 56 | Position.x[id] += Velocity.x[id] 57 | Position.y[id] += Velocity.y[id] 58 | } 59 | 60 | return world 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/systems/sprite.ts: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser' 2 | import { 3 | defineSystem, 4 | defineQuery, 5 | enterQuery, 6 | exitQuery 7 | } from 'bitecs' 8 | 9 | import Position from '../components/Position' 10 | import Sprite from '../components/Sprite' 11 | import Rotation from '../components/Rotation' 12 | 13 | export default function createSpriteSystem(scene: Phaser.Scene, textures: string[]) { 14 | const spritesById = new Map() 15 | 16 | const spriteQuery = defineQuery([Position, Rotation, Sprite]) 17 | 18 | const spriteQueryEnter = enterQuery(spriteQuery) 19 | const spriteQueryExit = exitQuery(spriteQuery) 20 | 21 | return defineSystem((world) => { 22 | const entitiesEntered = spriteQueryEnter(world) 23 | for (let i = 0; i < entitiesEntered.length; ++i) 24 | { 25 | const id = entitiesEntered[i] 26 | const texId = Sprite.texture[id] 27 | const texture = textures[texId] 28 | 29 | spritesById.set(id, scene.add.sprite(0, 0, texture)) 30 | } 31 | 32 | const entities = spriteQuery(world) 33 | for (let i = 0; i < entities.length; ++i) 34 | { 35 | const id = entities[i] 36 | 37 | const sprite = spritesById.get(id) 38 | if (!sprite) 39 | { 40 | // log an error 41 | continue 42 | } 43 | 44 | sprite.x = Position.x[id] 45 | sprite.y = Position.y[id] 46 | sprite.angle = Rotation.angle[id] 47 | } 48 | 49 | const entitiesExited = spriteQueryExit(world) 50 | for (let i = 0; i < entitiesExited.length; ++i) 51 | { 52 | const id = entitiesEntered[i] 53 | spritesById.delete(id) 54 | } 55 | 56 | return world 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/scenes/Game.ts: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser' 2 | import { 3 | createWorld, 4 | addEntity, 5 | addComponent, 6 | } from 'bitecs' 7 | 8 | import type { 9 | IWorld, 10 | System 11 | } from 'bitecs' 12 | 13 | import Position from '../components/Position' 14 | import Velocity from '../components/Velocity' 15 | import Sprite from '../components/Sprite' 16 | import Rotation from '../components/Rotation' 17 | import Player from '../components/Player' 18 | import CPU from '../components/CPU' 19 | import Input from '../components/Input' 20 | 21 | import createMovementSystem from '../systems/movement' 22 | import createSpriteSystem from '../systems/sprite' 23 | import createPlayerSystem from '../systems/player' 24 | import createCPUSystem from '../systems/cpu' 25 | 26 | enum Textures 27 | { 28 | TankBlue, 29 | TankGreen, 30 | TankRed 31 | } 32 | 33 | export default class Game extends Phaser.Scene 34 | { 35 | private cursors!: Phaser.Types.Input.Keyboard.CursorKeys 36 | 37 | private world!: IWorld 38 | private playerSystem!: System 39 | private cpuSystem!: System 40 | private movementSystem!: System 41 | private spriteSystem!: System 42 | 43 | constructor() 44 | { 45 | super('game') 46 | } 47 | 48 | init() 49 | { 50 | this.cursors = this.input.keyboard.createCursorKeys() 51 | } 52 | 53 | preload() 54 | { 55 | this.load.image('tank-blue', 'assets/tank_blue.png') 56 | this.load.image('tank-green', 'assets/tank_green.png') 57 | this.load.image('tank-red', 'assets/tank_red.png') 58 | } 59 | 60 | create() 61 | { 62 | const { width, height } = this.scale 63 | 64 | this.world = createWorld() 65 | 66 | // create the player tank 67 | const blueTank = addEntity(this.world) 68 | 69 | addComponent(this.world, Position, blueTank) 70 | addComponent(this.world, Velocity, blueTank) 71 | addComponent(this.world, Rotation, blueTank) 72 | addComponent(this.world, Sprite, blueTank) 73 | addComponent(this.world, Player, blueTank) 74 | addComponent(this.world, Input, blueTank) 75 | 76 | Position.x[blueTank] = 100 77 | Position.y[blueTank] = 100 78 | Sprite.texture[blueTank] = Textures.TankBlue 79 | Input.speed[blueTank] = 10 80 | 81 | // create random cpu tanks 82 | for (let i = 0; i < 10; ++i) 83 | { 84 | const tank = addEntity(this.world) 85 | 86 | addComponent(this.world, Position, tank) 87 | Position.x[tank] = Phaser.Math.Between(width * 0.25, width * 0.75) 88 | Position.y[tank] = Phaser.Math.Between(height * 0.25, height * 0.75) 89 | 90 | addComponent(this.world, Velocity, tank) 91 | addComponent(this.world, Rotation, tank) 92 | 93 | addComponent(this.world, Sprite, tank) 94 | Sprite.texture[tank] = Phaser.Math.Between(1, 2) 95 | 96 | addComponent(this.world, CPU, tank) 97 | CPU.timeBetweenActions[tank] = Phaser.Math.Between(0, 500) 98 | 99 | addComponent(this.world, Input, tank) 100 | Input.speed[tank] = 10 101 | } 102 | 103 | // create the systems 104 | this.playerSystem = createPlayerSystem(this.cursors) 105 | this.cpuSystem = createCPUSystem(this) 106 | this.movementSystem = createMovementSystem() 107 | this.spriteSystem = createSpriteSystem(this, ['tank-blue', 'tank-green', 'tank-red']) 108 | } 109 | 110 | update(t: number, dt: number) { 111 | // run each system in desired order 112 | this.playerSystem(this.world) 113 | this.cpuSystem(this.world) 114 | 115 | this.movementSystem(this.world) 116 | 117 | this.spriteSystem(this.world) 118 | } 119 | } 120 | --------------------------------------------------------------------------------