Developing games for the web

Paulo Ragonha

Tech-lead at Mojang

https://twitter.com/CrisXolt/status/1289402834748768261

Thermal Runway

@pirelenito

@marlonicus

Built on Web tech!

Why the web?

Cross-platform

Amazing tools and libraries

  • TypeScript
  • parcel
  • three.js
  • React
  • ecsy
  • ammojs-typed

Quick to get started

yarn add parcel-bundler --dev
yarn parcel **/*.html
yarn init -y .

The "game loop"

Typical Application

Mostly idle

Game

Villager AI

Fire animation

Hunger

Day/Night cycle

So let's build a game

Sort of...

Let's make a ball move

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>ECS Talk</title>
    <style>
      body {
        background-color: #1b363e;
        margin: 0;
        overflow: hidden;
      }

      #ball {
        position: absolute;
        background: #ffebea;
        width: 40px;
        height: 40px;
        border-radius: 20px;
      }
    </style>
  </head>
  <body>
    <div id="ball"></div>
  </body>
  <script src="./index.ts"></script>
</html>

index.html

const element = document.getElementById('ball')

const gameState = {
  x: 0,
  y: 0,
}

const gameLoop = () => {
  const time = Date.now()

  gameState.x = Math.sin(time / 1000) * 200 + 200
  gameState.y = Math.cos(time / 1000) * 200 + 200

  if (element) {
    element.style.left = `${gameState.x}px`
    element.style.top = `${gameState.y}px`
  }

  window.requestAnimationFrame(gameLoop)
}

gameLoop()

export default {}

index.ts

const gameLoop = () => {
  // do game stuff...

  window.requestAnimationFrame(gameLoop)
}

gameLoop()
const gameLoop = () => {
  // enemy AI
  // user controls
  // physics system
  // play sounds
  // render the world
  // render the UI

  window.requestAnimationFrame(gameLoop)
}

gameLoop()
  
import GameState from './GameState'
import runInput from './runInput'
import runPlayerMovement from './runPlayerMovement'
import runPhysics from './runPhysics'
import runRendering from './runRendering'

const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
import GameState from './GameState'
import runInput from './runInput'
import runPlayerMovement from './runPlayerMovement'
import runPhysics from './runPhysics'
import runRendering from './runRendering'

const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
import GameState from './GameState'

export default function runInput(
  gameState: GameState,
  delta: number,
) {
  const gamepad = navigator.getGamepads()[0]

  if (gamepad) {
    const buttons = gamepad.buttons

    gameState.buttonUpPressed = buttons[12].pressed
    gameState.buttonDownPressed = buttons[13].pressed
    gameState.buttonLeftPressed = buttons[14].pressed
    gameState.buttonRightPressed = buttons[15].pressed
  }
}
import GameState from './GameState'
import runInput from './runInput'
import runPlayerMovement from './runPlayerMovement'
import runPhysics from './runPhysics'
import runRendering from './runRendering'

const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
import GameState from './GameState'

const SPEED = 0.2

export default function runPlayerMovement(
  gameState: GameState,
  delta: number,
) {
  const speed = SPEED * delta

  if (gameState.buttonRightPressed) {
    gameState.x += speed
  }

  if (gameState.buttonLeftPressed) {
    gameState.x -= speed
  }

  if (gameState.buttonDownPressed) {
    gameState.y += speed
  }

  if (gameState.buttonUpPressed) {
    gameState.y -= speed
  }
}
import GameState from './GameState'
import runInput from './runInput'
import runPlayerMovement from './runPlayerMovement'
import runPhysics from './runPhysics'
import runRendering from './runRendering'

const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
import GameState from './GameState'

const GRAVITY = 0.1

export default function runPhysics(
  gameState: GameState,
  delta: number,
) {
  if (gameState.y <= window.innerHeight - 40) {
    gameState.y += GRAVITY * delta
  }
}
import GameState from './GameState'
import runInput from './runInput'
import runPlayerMovement from './runPlayerMovement'
import runPhysics from './runPhysics'
import runRendering from './runRendering'

const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
import GameState from './GameState'

export default function runRendering(
  gameState: GameState,
  delta: number,
) {
  const element = document.getElementById('ball')
  if (!element) return

  element.style.left = `${gameState.x}px`
  element.style.top = `${gameState.y}px`
}

What if I want to add more players?

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 100,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

What if I want to add static objects?

const gameState: GameState = {
  staticObjects: [
    {
      x: 25,
      y: 72,
    },
    {
      x: 18,
      y: 120,
    },
  ],
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 100,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 140,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

We need a better abstraction...

ECS

entity-component-system

Entity

Component

System

  • Container for components
  • Data
  • Facet of an entity
  • Do the work
  • Process entities
  • Modify components

Player

Position

Rendering

Entities and components

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}
const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

PlayerEntity

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

PlayerEntity

PositionComponent

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

PlayerEntity

GamepadComponent

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

PlayerEntity

PositionComponent

GamepadComponent

const gameState: GameState = {
  players: [
    {
      x: 0,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
    {
      x: 60,
      y: 0,
      buttonUpPressed: false,
      buttonDownPressed: false,
      buttonLeftPressed: false,
      buttonRightPressed: false,
    },
  ],
}

PlayerEntity

PositionComponent

GamepadComponent

PlayerEntity

PositionComponent

GamepadComponent

Systems

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

function -> System

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

function -> System

GamepadSystem

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

function -> System

GamepadSystem

PlayerMovementSystem

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

function -> System

GamepadSystem

PlayerMovementSystem

PhysicsSystem

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

function -> System

GamepadSystem

PlayerMovementSystem

PhysicsSystem

RenderingSystem

Entities

Components

Systems

  • Player
  • Position
  • Gamepad
  • Input
  • Physics
  • PlayerMovement
  • Rendering

ECSY

 An Entity Component System for the web

World

Container for entities components and systems

import { World } from 'ecsy'

const world = new World()

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  world.execute(delta, time)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()

previous example

with ECS

import { World } from 'ecsy'

const world = new World()

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  world.execute(delta, time)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()
const gameState: GameState = {
  x: 0,
  y: 0,

  buttonUpPressed: false,
  buttonDownPressed: false,
  buttonLeftPressed: false,
  buttonRightPressed: false,
}

let previousTime = Date.now()

const gameLoop = () => {
  const time = Date.now()
  const delta = time - previousTime

  runInput(gameState, delta)
  runPlayerMovement(gameState, delta)
  runPhysics(gameState, delta)
  runRendering(gameState, delta)

  previousTime = time
  window.requestAnimationFrame(gameLoop)
}

gameLoop()

previous example

with ECS

Nothing will happen

Need to define and add:

  • Entities
  • Components
  • Systems

Entities

import { World } from 'ecsy'
import PlayerComponent from '../components/PlayerComponent'
import RenderableComponent from '../components/RenderableComponent'
import RigidBodyComponent from '../components/RigidBodyComponent'
import GamepadComponent from '../components/GamepadComponent'
import PositionComponent from '../components/PositionComponent'

export default function createPlayerEntity(world: World, index: number = 0) {
  const player = world.createEntity()
  player.addComponent(PlayerComponent)
  player.addComponent(RenderableComponent, { color: playerColors[index] })
  player.addComponent(RigidBodyComponent)
  player.addComponent(GamepadComponent, { index })
  player.addComponent(PositionComponent, { x: index * 60 })
}

const playerColors = ['#BFBEFF', '#BEFFD8', '#F6FFBE', '#FFBECA']
entities/createPlayerEntity.ts
import { World } from 'ecsy'
import PlayerComponent from '../components/PlayerComponent'
import RenderableComponent from '../components/RenderableComponent'
import RigidBodyComponent from '../components/RigidBodyComponent'
import GamepadComponent from '../components/GamepadComponent'
import PositionComponent from '../components/PositionComponent'

export default function createPlayerEntity(world: World, index: number = 0) {
  const player = world.createEntity()
  player.addComponent(PlayerComponent)
  player.addComponent(RenderableComponent, { color: playerColors[index] })
  player.addComponent(RigidBodyComponent)
  player.addComponent(GamepadComponent, { index })
  player.addComponent(PositionComponent, { x: index * 60 })
}

const playerColors = ['#BFBEFF', '#BEFFD8', '#F6FFBE', '#FFBECA']
entities/createPlayerEntity.ts
import createPlayerEntity from './entities/createPlayerEntity'

// ...

const world = new World()
createPlayerEntity(world)
// ...
index.ts

Components

import { Component, Types } from 'ecsy'

export default class PositionComponent extends Component<PositionComponent> {
  x!: number
  y!: number

  static schema = {
    x: { type: Types.Number, default: 0 },
    y: { type: Types.Number, default: 0 },
  }
}
components/PositionComponent.ts
import { Component, Types } from 'ecsy'

export default class PositionComponent extends Component<PositionComponent> {
  x!: number
  y!: number

  static schema = {
    x: { type: Types.Number, default: 0 },
    y: { type: Types.Number, default: 0 },
  }
}
components/PositionComponent.ts
import PositionComponent from './components/PositionComponent'

// ...

const world = new World()
world.registerComponent(PositionComponent)
// ...
index.ts

Systems

EntityA

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityB

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityD

RenderableComponent

RigidBodyComponent

PositionComponent

EntityE

RenderableComponent

RigidBodyComponent

PositionComponent

EntityF

RenderableComponent

RigidBodyComponent

PositionComponent

EntityC

RenderableComponent

PositionComponent

EntityA

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityB

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityD

RenderableComponent

RigidBodyComponent

PositionComponent

EntityE

RenderableComponent

RigidBodyComponent

PositionComponent

EntityF

RenderableComponent

RigidBodyComponent

PositionComponent

EntityC

RenderableComponent

PositionComponent

PhysicsSystem

Query:  [RigidBodyComponent, PositionComponent]

EntityA

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityB

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityD

RenderableComponent

RigidBodyComponent

PositionComponent

EntityE

RenderableComponent

RigidBodyComponent

PositionComponent

EntityF

RenderableComponent

RigidBodyComponent

PositionComponent

EntityC

RenderableComponent

PositionComponent

GamepadSystem

Query:  [GamepadComponent]

EntityA

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityB

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityD

RenderableComponent

RigidBodyComponent

PositionComponent

EntityE

RenderableComponent

RigidBodyComponent

PositionComponent

EntityF

RenderableComponent

RigidBodyComponent

PositionComponent

EntityC

RenderableComponent

PositionComponent

PlayerMovementSystem

Query:  [PlayerComponent, GamepadComponent, PositionComponent]

EntityA

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityB

PlayerComponent

RenderableComponent

RigidBodyComponent

GamepadComponent

PositionComponent

EntityD

RenderableComponent

RigidBodyComponent

PositionComponent

EntityE

RenderableComponent

RigidBodyComponent

PositionComponent

EntityF

RenderableComponent

RigidBodyComponent

PositionComponent

EntityC

RenderableComponent

PositionComponent

RenderingSystem

Query:  [PositionComponent, RenderableComponent]

import { System, Entity } from 'ecsy'
import PositionComponent from '../components/PositionComponent'
import RigidBodyComponent from '../components/RigidBodyComponent'

const GRAVITY = 0.1

export default class PhysicsSystem extends System {
  runGravity(delta: number, entity: Entity) {
    const position = entity.getMutableComponent(PositionComponent)
    if (!position) return

    if (position.y < window.innerHeight - 40) {
      position.y += GRAVITY * delta
    }
  }

  execute(delta: number) {
    this.queries.rigidBodies.results.forEach((entity) => this.runGravity(delta, entity))
  }

  static queries = {
    rigidBodies: {
      components: [RigidBodyComponent, PositionComponent],
    },
  }
}
components/PhysicsComponent.ts
import { System, Entity } from 'ecsy'
import PositionComponent from '../components/PositionComponent'
import RigidBodyComponent from '../components/RigidBodyComponent'

const GRAVITY = 0.1

export default class PhysicsSystem extends System {
  runGravity(delta: number, entity: Entity) {
    const position = entity.getMutableComponent(PositionComponent)
    if (!position) return

    if (position.y < window.innerHeight - 40) {
      position.y += GRAVITY * delta
    }
  }

  execute(delta: number) {
    this.queries.rigidBodies.results.forEach((entity) => this.runGravity(delta, entity))
  }

  static queries = {
    rigidBodies: {
      components: [RigidBodyComponent, PositionComponent],
    },
  }
}
components/PhysicsSystem.ts
import PhysicsSystem from './systems/PhysicsSystem'

// ...

const world = new World()
world.registerSystem(PhysicsSystem)
// ...
index.ts

Systems and
State-Components

import GameState from './GameState'

export default function runRendering(gameState: GameState, delta: number) {
  const element = document.getElementById('ball')
  if (!element) return

  element.style.left = `${gameState.x}px`
  element.style.top = `${gameState.y}px`
}

Never adds or removes

<body>
  <div id="ball"></div>
</body>

RenderingSystemStateComponent

  • element: HTMLDIVElement

PositionComponent

  • x: number

  • y: number

import { SystemStateComponent, Types } from 'ecsy'

export default class RenderingSystemStateComponent extends SystemStateComponent<
  RenderingSystemStateComponent
> {
  element?: HTMLDivElement

  static schema = {
    element: { type: Types.Ref },
  }
}
components/RenderingSystemStateComponent.ts
import { SystemStateComponent, Types } from 'ecsy'

export default class RenderingSystemStateComponent extends SystemStateComponent<
  RenderingSystemStateComponent
> {
  element?: HTMLDivElement

  static schema = {
    element: { type: Types.Ref },
  }
}
components/RenderingSystemStateComponent.ts
import RenderingSystemStateComponent from './state-components/RenderingSystemStateComponent'

// ...

const world = new World()
world.registerComponent(RenderingSystemStateComponent)
// ...
index.ts

RenderingSystem

import { System } from 'ecsy'

export default class RenderingSystem extends System {
  execute() {

  }

  static queries = {
    
  }
}
systems/RenderingSystem.ts
import { System } from 'ecsy'

export default class RenderingSystem extends System {
  execute() {
    this.queries.uninitialized.added?.forEach((e) => this.addRenderable(e))
    this.queries.positions.changed?.forEach((e) => this.updatePosition(e))
    this.queries.initialized.removed?.forEach((e) => this.removeRenderable(e))
  }

  static queries = {
    uninitialized: {
      components: [
        PositionComponent,
        RenderableComponent,
        Not(RenderingSystemStateComponent),
      ],
      listen: { added: true },
    },

    positions: {
      components: [PositionComponent, RenderingSystemStateComponent],
      listen: { changed: true },
    },
    
    initialized: {
      components: [RenderableComponent, RenderingSystemStateComponent],
      listen: { removed: true },
    },
  }
}
import { System } from 'ecsy'

export default class RenderingSystem extends System {
  execute() {
    this.queries.uninitialized.added?.forEach((e) => this.addRenderable(e))
    this.queries.positions.changed?.forEach((e) => this.updatePosition(e))
    this.queries.initialized.removed?.forEach((e) => this.removeRenderable(e))
  }

  addRenderable(entity: Entity) {
    const position = entity.getComponent(PositionComponent)
    const renderable = entity.getComponent(RenderableComponent)
    if (!position || !renderable) return

    const element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.background = renderable.color
    element.style.width = '40px'
    element.style.height = '40px'
    element.style.borderRadius = '20px'
    element.style.left = `${position.x}px`
    element.style.top = `${position.y}px`
    document.body.appendChild(element)

    entity.addComponent(RenderingSystemStateComponent, { element })
  }  
  
  static queries = {
    uninitialized: {
      components: [
        PositionComponent,
        RenderableComponent,
        Not(RenderingSystemStateComponent),
      ],
      listen: { added: true },
    },

    positions: {
      components: [PositionComponent, RenderingSystemStateComponent],
      listen: { changed: true },
    },

    initialized: {
      components: [RenderableComponent, RenderingSystemStateComponent],
      listen: { removed: true },
    },
  }
}
import { System } from 'ecsy'

export default class RenderingSystem extends System {
  execute() {
    this.queries.uninitialized.added?.forEach((e) => this.addRenderable(e))
    this.queries.positions.changed?.forEach((e) => this.updatePosition(e))
    this.queries.initialized.removed?.forEach((e) => this.removeRenderable(e))
  }
  
  updatePosition(entity: Entity) {
    const stateComponent = entity.getComponent(RenderingSystemStateComponent)
    const position = entity.getComponent(PositionComponent)
    if (!position || !stateComponent || !stateComponent.element) return

    stateComponent.element.style.left = `${position.x}px`
    stateComponent.element.style.top = `${position.y}px`
  }

  addRenderable(entity: Entity) {
    const position = entity.getComponent(PositionComponent)
    const renderable = entity.getComponent(RenderableComponent)
    if (!position || !renderable) return

    const element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.background = renderable.color
    element.style.width = '40px'
    element.style.height = '40px'
    element.style.borderRadius = '20px'
    element.style.left = `${position.x}px`
    element.style.top = `${position.y}px`
    document.body.appendChild(element)

    entity.addComponent(RenderingSystemStateComponent, { element })
  }  
  
  static queries = {
    uninitialized: {
      components: [
        PositionComponent,
        RenderableComponent,
        Not(RenderingSystemStateComponent),
      ],
      listen: { added: true },
    },

    positions: {
      components: [PositionComponent, RenderingSystemStateComponent],
      listen: { changed: true },
    },

    initialized: {
      components: [RenderableComponent, RenderingSystemStateComponent],
      listen: { removed: true },
    },
  }
}
import { System } from 'ecsy'

export default class RenderingSystem extends System {
  execute() {
    this.queries.uninitialized.added?.forEach((e) => this.addRenderable(e))
    this.queries.positions.changed?.forEach((e) => this.updatePosition(e))
    this.queries.initialized.removed?.forEach((e) => this.removeRenderable(e))
  }
  
  removeRenderable(entity: Entity) {
    const stateComponent = entity.getComponent(RenderingSystemStateComponent)
    if (!stateComponent) return

    if (stateComponent.element) {
      document.body.removeChild(stateComponent.element)
    }

    entity.removeComponent(RenderingSystemStateComponent)
  }
  
  updatePosition(entity: Entity) {
    const stateComponent = entity.getComponent(RenderingSystemStateComponent)
    const position = entity.getComponent(PositionComponent)
    if (!position || !stateComponent || !stateComponent.element) return

    stateComponent.element.style.left = `${position.x}px`
    stateComponent.element.style.top = `${position.y}px`
  }

  addRenderable(entity: Entity) {
    const position = entity.getComponent(PositionComponent)
    const renderable = entity.getComponent(RenderableComponent)
    if (!position || !renderable) return

    const element = document.createElement('div')
    element.style.position = 'absolute'
    element.style.background = renderable.color
    element.style.width = '40px'
    element.style.height = '40px'
    element.style.borderRadius = '20px'
    element.style.left = `${position.x}px`
    element.style.top = `${position.y}px`
    document.body.appendChild(element)

    entity.addComponent(RenderingSystemStateComponent, { element })
  }  
  
  static queries = {
    uninitialized: {
      components: [
        PositionComponent,
        RenderableComponent,
        Not(RenderingSystemStateComponent),
      ],
      listen: { added: true },
    },

    positions: {
      components: [PositionComponent, RenderingSystemStateComponent],
      listen: { changed: true },
    },

    initialized: {
      components: [RenderableComponent, RenderingSystemStateComponent],
      listen: { removed: true },
    },
  }
}

Entities

Components

Systems

  • Player
  • Player
  • Position
  • Gamepad
  • Renderable
  • RigidBody
  • RenderingSystemState
  • Input
  • Physics
  • PlayerMovement
  • Rendering

Extending our game

ECS in Thermal Runway

Components

  • PositionComponent
  • KeyboardControllerComponent
  • GamepadControllerComponent
  • VelocityComponent
  • ModelComponent
  • ScaleComponent
  • ThreeMeshStateComponent
  • RigidBodyComponent
  • AmmoRigidBodyStateComponent
  • PlayerTagComponent
  • PlayerViewTagComponent
  • ...

Systems

  • GameOverSystem
  • NewGameSystem
  • MainMenuSystem
  • PlatformCreationSystem
  • KeyboardSystem
  • GamepadSystem
  • PlayerMovementSystem
  • PhysicsSystem
  • PlayerViewSystem
  • RenderingSystem
  • UISystem
  • ScoringSystem

Wrapping-up

https://github.com/pirelenito/ecsy-talk

https://github.com/macaco-maluco/thermal-runway

https://thermalrunway.macacomaluco.space

Criteria Rank Score Raw Score
Fun #309 3.722 3.722
Presentation #1075 3.528 3.528
Overall #1207 3.250 3.250
Originality #3784 2.444 2.444

Top 6%

in the fun criteria!

What about Minecraft?

https://youtu.be/DMza7sGJ-Ro

https://youtu.be/-8_xWQgeqvU

Thank you!

@pirelenito

https://slides.com/pirelenito/ecsy-talk

Made with Slides.com