https://github.com/heypoom/doge
Data Oriented Game Engine in TypeScript with ECS
https://github.com/heypoom/doge
Last synced: 3 months ago
JSON representation
Data Oriented Game Engine in TypeScript with ECS
- Host: GitHub
- URL: https://github.com/heypoom/doge
- Owner: heypoom
- Created: 2020-11-20T02:58:26.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2021-03-27T02:44:07.000Z (almost 5 years ago)
- Last Synced: 2025-10-01T23:40:31.777Z (3 months ago)
- Language: TypeScript
- Homepage: https://cubeland.netlify.app
- Size: 405 KB
- Stars: 4
- Watchers: 2
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# DOGE: Data Oriented Game Engine

## What is DOGE?
DOGE is a data-oriented game engine built with TypeScript.
This engine is built on the concepts of the Entity-Component-System (ECS) architecture; it uses Pixi.js for graphics and Matter.js for physics.
This is still just a **proof-of-concept**. Please feel free to take a look!
# Key Concepts
## Entities
Entities refer to the objects in the game world, such as the player, enemy, walls, dropped items, lights and so on.
Entities in DOGE are TypeScript types that describe which piece of data (data components) are required by this entity. For example, the `Actor` entity might have the `position`, `movement`, `texture` and `collider` components.
> Therefore, entities are just a "data container" that contains the "data components"
To reduce boilerplate, we use the `Schema` function to compose the data components together.
```ts
// src/core/@types/entities/IActor.ts
const ActorSchema = Schema('position', 'movement', 'texture', 'collider')
type IActor = typeof ActorSchema
```
> We store the entities in `src/core/@types/entities`.
## Components
Components refer to the pieces of data that can be used in the entities in the game world, and can be used by systems to perform computation.
For example: `Position`, `Movement`, `Inventory`, `Shape`, `Texture`, `Timer`, `Collider`, etc.
Components in DOGE are plain TypeScript types that describes which data they contain. Here is how `Position` and `Texture` looks like:
```ts
// src/core/@types/components/IPosition.ts
interface IPosition {
x: number
y: number
}
// src/core/@types/components/ITexture.ts
interface ITexture {
src: string
width: number
height: number
}
```
> We store the data components in `src/core/@types/components`.
## Systems
Systems uses the data components in entities to process the game world, using the data as a source of truth. Each system handles the different domains and concerns.
For example: `TextureRendererSystem`, `ColliderSystem`, `MovementSystem`, etc.
Systems in DOGE are an object containing the lifecycle functions (on tick, on setup), and the dependencies aka the data components the system needs.
We use the `createSystem` helper to type-check the data components based on the dependencies.
Here is the entire code for the `TextureRendererSystem`.
```ts
// src/core/systems/renderer/TextureRenderer.ts
const TextureRendererSystem = createSystem({
deps: ['position', 'texture'],
async onSetup(es) {
for (const entity of es) {
const { position, texture } = entity.data
const tex = await addTexture(texture.src)
const sprite = new Sprite(tex)
sprite.x = position.x
sprite.y = position.y
sprite.width = texture.width
sprite.height = texture.height
sprite.name = entity.id
pixi.stage.addChild(sprite)
}
},
onTick(es) {
es.forEach((entity) => {
const { position } = entity.data
const sprite = pixi.stage.getChildByName(entity.id)
if (!sprite) return
sprite.x = position.x
sprite.y = position.y
})
},
})
```
> We store the systems in `src/core/systems`.
## Actions
Actions refer to the action that can be performed by the entities in the game world, or triggered from the interaction between systems or entities.
For example, we can use the `@actor/move` action to move the player to the left:
```ts
const player = world.get('player')
world.act('@actor/move', { direction: 'left' }, player)
```
This action is used in the `MovementSystem` to make the player move according to arrow or WASD navigation.
> Therefore, we can extract the logic out of our systems and into the actions to improve code maintainability and ease of debugging.
To implement actions for an entity, we first declare a type that maps the action name to the action input data.
```ts
// src/core/actions/@types/IActorAction.ts
interface IActorAction {
'@actor/move': { direction: IDirection }
'@actor/use': { item: IItem }
'@actor/paint': { team: ITeam; color: string }
}
```
Then, we can implement actions for the actor. We can refactor the action logic into a separate file as well:
```ts
// src/core/actions/actor/index.ts
const ActorActions: IActionGroup = {
'@actor/move': (a, e) => MoveAction[a.direction]?.(e),
}
// src/core/actions/actor/movement.ts
type IHandler = (e: IEntity<'actor'>) => void
export const MoveAction: Record = {
up(e) {
const { position, movement } = e.data
if (position.y > 0) position.y -= movement.speed
},
}
```
> We store the actions in `src/core/actions`.