├── .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 | 
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 |
--------------------------------------------------------------------------------