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
Developing games for the web
By pirelenito
Developing games for the web
- 168