Skip to content

Commit 8252ce2

Browse files
🎉 initial commit
0 parents  commit 8252ce2

11 files changed

+844
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Data oriented entity component system (ecs) implementation inspired by [simondev](https://www.youtube.com/channel/UCEwhtpXrg5MmwlH04ANpL8A)
2+
3+
See src/snapshots and src/tests for api demonstration or even src/benchmark.mjs.
4+
See src/ecs.mjs for the source code (very lightweight, 76 LoC).
5+
Load benchmark.html through a server to see a sample benchmark and do profiles.
6+
7+
At higher entity counts rendering takes all the time with my current setup. You can disable rendering by commenting out
8+
9+
```
10+
requestAnimationFrame(render);
11+
```

package-lock.json

+76
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "spatial-grid",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "node src/utils/testRunner.mjs"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"devDependencies": {
13+
"async": "^3.2.0",
14+
"console-table-printer": "^2.8.2",
15+
"diff": "^5.0.0"
16+
}
17+
}

src/benchmark.html

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<title>ecs benchmark</title>
4+
<style>
5+
html, body {
6+
margin: 0;
7+
height: 100%;
8+
}
9+
#canvas {
10+
width: 100%;
11+
height: 100%;
12+
display: block;
13+
}
14+
</style>
15+
</head>
16+
<body>
17+
<canvas id="canvas" style="width: 100%; height: 100%; display: block;"></canvas>
18+
</body>
19+
<script src="./benchmark.mjs" type="module"></script>

src/benchmark.mjs

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import ecs from "./ecs.mjs";
2+
import * as THREE from "https://unpkg.com/three@0.127.0/build/three.module.js";
3+
4+
const renderer = new THREE.WebGLRenderer({
5+
canvas: document.querySelector("#canvas"),
6+
});
7+
8+
const fov = 75;
9+
const aspect = 2;
10+
const near = 0.1;
11+
const far = 100;
12+
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
13+
camera.position.z = 30;
14+
15+
const scene = new THREE.Scene();
16+
17+
const color = 0xffffff;
18+
const intensity = 1;
19+
const light = new THREE.DirectionalLight(color, intensity);
20+
light.position.set(-1, 2, 4);
21+
scene.add(light);
22+
23+
function resizeRendererToDisplaySize(renderer) {
24+
const canvas = renderer.domElement;
25+
const pixelRatio = window.devicePixelRatio;
26+
const width = (canvas.clientWidth * pixelRatio) | 0;
27+
const height = (canvas.clientHeight * pixelRatio) | 0;
28+
const needResize = canvas.width !== width || canvas.height !== height;
29+
if (needResize) {
30+
renderer.setSize(width, height, false);
31+
}
32+
return needResize;
33+
}
34+
35+
function render() {
36+
renderer.render(scene, camera);
37+
38+
if (resizeRendererToDisplaySize(renderer)) {
39+
const canvas = renderer.domElement;
40+
camera.aspect = canvas.clientWidth / canvas.clientHeight;
41+
camera.updateProjectionMatrix();
42+
}
43+
44+
requestAnimationFrame(render);
45+
}
46+
requestAnimationFrame(render);
47+
48+
/**
49+
* Run ecs with "entityCount" entities, give all of them random position and velocity components, move them on each iteration according to moveSystem.
50+
* Also give them age, kill them when age runs out and on each update spawn entities until there are "entityCount" entities;
51+
*/
52+
const entityCount = 3_000;
53+
const entityMaxAge = 10 * 1000;
54+
const components = ["mesh", "velocity", "age"];
55+
const moveSystem = {
56+
name: "moveSystem",
57+
execute: (world, deltaTime, time) => {
58+
const moveEntity = (eid) => {
59+
const velocity = world.components.velocity.get(eid);
60+
const mesh = world.components.mesh.get(eid);
61+
mesh.position.x += velocity.x * deltaTime;
62+
mesh.position.y += velocity.y * deltaTime;
63+
mesh.position.z += velocity.z * deltaTime;
64+
65+
mesh.rotation.x = time * velocity.x;
66+
mesh.rotation.y = time * velocity.y;
67+
// TODO maybe move to its own system?
68+
// add age for entities too far away
69+
if (
70+
(Math.abs(mesh.position.x) > 5 ||
71+
Math.abs(mesh.position.y) > 5 ||
72+
Math.abs(mesh.position.z) > 5) &&
73+
!world.components.age.has(eid)
74+
) {
75+
ecs.addComponents(world, eid, {
76+
age: { value: Math.random() * entityMaxAge },
77+
});
78+
}
79+
// remove velocity if even farther
80+
if (
81+
(Math.abs(mesh.position.x) > 12 ||
82+
Math.abs(mesh.position.y) > 12 ||
83+
Math.abs(mesh.position.z) > 12) &&
84+
world.components.velocity.has(eid)
85+
) {
86+
ecs.removeComponents(world, eid, ["velocity"]);
87+
}
88+
};
89+
ecs.innerJoin(moveEntity, world.components.mesh, world.components.velocity);
90+
},
91+
};
92+
const agingSystem = {
93+
name: "agingSystem",
94+
execute: (world, deltaTime) => {
95+
for (const eid of world.components.age.keys()) {
96+
const currentAge = (world.components.age.get(eid).value -= deltaTime);
97+
if (currentAge < 0) {
98+
world.resources.scene.remove(world.components.mesh.get(eid));
99+
ecs.removeEntity(world, eid);
100+
}
101+
}
102+
},
103+
};
104+
const spawningSystem = {
105+
name: "spawningSystem",
106+
execute: (world) => {
107+
const currentEntityCount = world.entities.count;
108+
if (currentEntityCount < entityCount) {
109+
const delta = entityCount - currentEntityCount;
110+
for (let i = 0; i < delta; i++) {
111+
world.resources.createEntity(
112+
world,
113+
world.resources.createRandomEntityComponents(world)
114+
);
115+
}
116+
}
117+
},
118+
};
119+
120+
let updateCounter = 0;
121+
let frameTime1 = performance.now();
122+
123+
const debugSystem = {
124+
name: "debugSystem",
125+
execute: () => {
126+
updateCounter++;
127+
if (updateCounter === 60) {
128+
const frameTime2 = performance.now();
129+
const frameDelta = frameTime2 - frameTime1;
130+
frameTime1 = frameTime2;
131+
const fps = 60 / (frameDelta * 0.001);
132+
console.log("fps", Math.round(fps));
133+
updateCounter = 0;
134+
}
135+
},
136+
};
137+
138+
const systems = [moveSystem, agingSystem, spawningSystem, debugSystem];
139+
140+
console.time("init");
141+
const world = ecs.createWorld();
142+
143+
const boxWidth = 1;
144+
const boxHeight = 1;
145+
const boxDepth = 1;
146+
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
147+
148+
const material = new THREE.MeshPhongMaterial({ color: 0x44aa88 });
149+
150+
world.resources.scene = scene;
151+
world.resources.materials = {};
152+
world.resources.materials.cube = material;
153+
world.resources.geometries = {};
154+
world.resources.geometries.cube = geometry;
155+
world.resources.camera = camera;
156+
157+
components.forEach((component) => {
158+
ecs.registerComponent(world, component);
159+
});
160+
161+
const createEntity = ecs.initializeCreateEntity();
162+
163+
world.resources.createEntity = createEntity;
164+
165+
const createRandomEntityComponents = (world) => {
166+
const mesh = new THREE.Mesh(
167+
world.resources.geometries.cube,
168+
world.resources.materials.cube
169+
);
170+
mesh.position.x = (Math.random() - 0.5 + Number.EPSILON) * 10;
171+
mesh.position.y = (Math.random() - 0.5 + Number.EPSILON) * 10;
172+
mesh.position.z = (Math.random() - 0.5 + Number.EPSILON) * 10;
173+
world.resources.scene.add(mesh);
174+
return {
175+
mesh,
176+
velocity: {
177+
x: (Math.random() - 0.5) * 0.001 + Number.EPSILON,
178+
y: (Math.random() - 0.5) * 0.001 + Number.EPSILON,
179+
z: (Math.random() - 0.5) * 0.001 + Number.EPSILON,
180+
},
181+
};
182+
};
183+
184+
world.resources.createRandomEntityComponents = createRandomEntityComponents;
185+
186+
for (let i = 0; i < entityCount; i++) {
187+
createEntity(world, createRandomEntityComponents(world));
188+
}
189+
190+
ecs.registerSystem(world, moveSystem);
191+
192+
systems.forEach((system) => {
193+
ecs.registerSystem(world, system);
194+
});
195+
196+
console.timeEnd("init");
197+
console.log({ entityCount });
198+
199+
ecs.run(world, performance.now());

0 commit comments

Comments
 (0)