新增worker-system文档及源码示例

This commit is contained in:
YHH
2025-09-28 20:03:29 +08:00
parent cf2dc91af6
commit 1dfcd008aa
20 changed files with 2697 additions and 19 deletions

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Worker System Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #4a9eff;
}
.demo-area {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.canvas-container {
flex: 1;
}
#gameCanvas {
border: 2px solid #4a9eff;
background: #000;
display: block;
}
.controls {
width: 300px;
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: #ccc;
}
.control-group input, .control-group button {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #555;
background: #3a3a3a;
color: white;
border-radius: 4px;
}
.control-group button {
background: #4a9eff;
cursor: pointer;
transition: background 0.3s;
}
.control-group button:hover {
background: #3a8eef;
}
.control-group button:disabled {
background: #555;
cursor: not-allowed;
}
.stats {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.stats h3 {
margin-top: 0;
color: #4a9eff;
}
.stat-line {
margin: 5px 0;
}
.worker-enabled {
color: #4eff4a;
}
.worker-disabled {
color: #ff4a4a;
}
.performance-high {
color: #4eff4a;
}
.performance-medium {
color: #ffff4a;
}
.performance-low {
color: #ff4a4a;
}
</style>
</head>
<body>
<div class="container">
<h1>ECS Framework Worker System 演示</h1>
<div class="demo-area">
<div class="canvas-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<div class="controls">
<div class="control-group">
<label>实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
<span id="entityCountValue">1000</span>
</div>
<div class="control-group">
<label>Worker 设置:</label>
<button id="toggleWorker">禁用 Worker</button>
</div>
<div class="control-group">
<button id="spawnParticles">生成粒子系统</button>
<button id="clearEntities">清空所有实体</button>
<button id="resetDemo">重置演示</button>
</div>
<div class="control-group">
<label>物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
<label>重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5">
<label>摩擦力: <span id="frictionValue">95%</span></label>
</div>
</div>
</div>
<div class="stats">
<h3>性能统计</h3>
<div class="stat-line">FPS: <span id="fps">0</span></div>
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,626 @@
{
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
}
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.49",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esengine/ecs-framework": {
"resolved": "../../packages/core",
"link": true
},
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=14.18.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"description": "ECS Framework Worker System Demo",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
},
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
}

View File

@@ -0,0 +1,173 @@
import { Scene } from '@esengine/ecs-framework';
import { PhysicsWorkerSystem, RenderSystem, LifetimeSystem } from './systems';
import { Position, Velocity, Physics, Renderable, Lifetime } from './components';
export class GameScene extends Scene {
private canvas: HTMLCanvasElement;
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
constructor(canvas: HTMLCanvasElement) {
super();
this.canvas = canvas;
}
override initialize(): void {
this.name = "WorkerDemoScene";
// 创建系统
this.physicsSystem = new PhysicsWorkerSystem(true); // 默认启用Worker
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
// 设置系统执行顺序
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
// 添加系统到场景
this.addSystem(this.physicsSystem);
this.addSystem(this.lifetimeSystem);
this.addSystem(this.renderSystem);
}
override onStart(): void {
console.log("Worker演示场景已启动");
this.spawnInitialEntities();
}
override unload(): void {
console.log("Worker演示场景已卸载");
}
/**
* 生成初始实体
*/
public spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
/**
* 创建一个粒子实体
*/
public createParticle(): void {
const entity = this.createEntity(`Particle_${Date.now()}_${Math.random()}`);
// 随机位置
const x = Math.random() * (this.canvas.width - 20) + 10;
const y = Math.random() * (this.canvas.height - 20) + 10;
// 随机速度
const dx = (Math.random() - 0.5) * 200;
const dy = (Math.random() - 0.5) * 200;
const mass = Math.random() * 3 + 2;
const bounce = 0.85 + Math.random() * 0.15;
const friction = 0.998 + Math.random() * 0.002;
// 随机颜色和大小 - 增加更多颜色提高多样性
const colors = [
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44',
'#aaff44', '#44aaff', '#ff44aa', '#aa44ff', '#44ffaa', '#cccccc'
];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 6 + 3;
// 添加组件
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, friction));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(5 + Math.random() * 10)); // 5-15秒生命周期
}
/**
* 生成粒子爆发效果
*/
public spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.createEntity(`Explosion_${Date.now()}_${i}`);
// 在中心点周围随机分布
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const distance = Math.random() * 30;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
// 爆炸速度
const speed = 100 + Math.random() * 150;
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const mass = 0.5 + Math.random() * 1;
const bounce = 0.8 + Math.random() * 0.2;
// 亮色
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 4 + 2;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, 0.999));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(2 + Math.random() * 3)); // 短生命周期
}
}
/**
* 清空所有实体
*/
public clearAllEntities(): void {
const entities = [...this.entities.buffer]; // 复制数组避免修改原数组
for (const entity of entities) {
entity.destroy();
}
}
/**
* 切换Worker启用状态
*/
public toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 重新创建物理系统
this.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled);
this.physicsSystem.updateOrder = 1;
this.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
/**
* 更新Worker配置
*/
public updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
if (config.gravity !== undefined || config.friction !== undefined) {
const physicsConfig = this.physicsSystem.getPhysicsConfig();
this.physicsSystem.updatePhysicsConfig({
gravity: config.gravity ?? physicsConfig.gravity,
groundFriction: config.friction ?? physicsConfig.groundFriction
});
}
}
/**
* 获取系统信息
*/
public getSystemInfo() {
return {
physics: this.physicsSystem.getWorkerInfo(),
entityCount: this.entities.count,
physicsConfig: this.physicsSystem.getPhysicsConfig()
};
}
}

View File

@@ -0,0 +1,89 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
// 位置组件
@ECSComponent('Position')
export class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
// 速度组件
@ECSComponent('Velocity')
export class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
scale(factor: number): void {
this.dx *= factor;
this.dy *= factor;
}
}
// 物理组件
@ECSComponent('Physics')
export class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
// 渲染组件
@ECSComponent('Renderable')
export class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
shape: 'circle' | 'square' = 'circle';
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
super();
this.color = color;
this.size = size;
this.shape = shape;
}
}
// 生命周期组件
@ECSComponent('Lifetime')
export class Lifetime extends Component {
maxAge: number = 5;
currentAge: number = 0;
constructor(maxAge: number = 5) {
super();
this.maxAge = maxAge;
this.currentAge = 0;
}
isDead(): boolean {
return this.currentAge >= this.maxAge;
}
}

View File

@@ -0,0 +1,339 @@
import { Core } from '@esengine/ecs-framework';
import { GameScene } from './GameScene';
// 性能监控
interface PerformanceStats {
fps: number;
frameTime: number;
physicsTime: number;
renderTime: number;
memoryUsage: number;
}
class WorkerDemo {
private gameScene: GameScene;
private canvas: HTMLCanvasElement;
private isRunning = false;
private lastTime = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private currentFPS = 0;
private lastWorkerStatusUpdate = 0;
// UI元素
private elements: { [key: string]: HTMLElement } = {};
constructor() {
// 获取canvas
this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
if (!this.canvas) {
throw new Error('Canvas element not found');
}
// 初始化UI元素引用
this.initializeUIElements();
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
// 创建游戏场景
this.gameScene = new GameScene(this.canvas);
// 设置场景
Core.setScene(this.gameScene);
// 绑定事件
this.bindEvents();
// 启动演示
this.start();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
'physicsTime', 'renderTime', 'frameTime', 'memoryUsage'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
} else {
console.warn(`Element with id '${id}' not found`);
}
}
}
private bindEvents(): void {
// 实体数量滑块
if (this.elements.entityCount && this.elements.entityCountValue) {
const slider = this.elements.entityCount as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.entityCountValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const count = parseInt(slider.value);
this.gameScene.spawnInitialEntities(count);
});
}
// Worker切换按钮
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.gameScene.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
this.updateWorkerStatus();
});
}
// 重力滑块
if (this.elements.gravity && this.elements.gravityValue) {
const slider = this.elements.gravity as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.gravityValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const gravity = parseInt(slider.value);
this.gameScene.updateWorkerConfig({ gravity });
});
}
// 摩擦力滑块
if (this.elements.friction && this.elements.frictionValue) {
const slider = this.elements.friction as HTMLInputElement;
slider.addEventListener('input', () => {
const value = parseInt(slider.value);
this.elements.frictionValue.textContent = `${value}%`;
});
slider.addEventListener('change', () => {
const friction = parseInt(slider.value) / 100;
this.gameScene.updateWorkerConfig({ friction });
});
}
// 生成粒子按钮
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.gameScene.spawnParticleExplosion(centerX, centerY, 100);
});
}
// 清空实体按钮
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.gameScene.clearAllEntities();
});
}
// 重置演示按钮
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
this.resetDemo();
});
}
// Canvas点击事件 - 在点击位置生成粒子爆发
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.gameScene.spawnParticleExplosion(x, y, 30);
});
}
private start(): void {
this.isRunning = true;
this.lastTime = performance.now();
this.gameLoop();
console.log('Worker演示已启动');
}
private gameLoop = (): void => {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
const frameStartTime = performance.now();
Core.update(deltaTime);
const frameEndTime = performance.now();
// 更新性能统计
this.updatePerformanceStats({
fps: this.currentFPS,
frameTime: frameEndTime - frameStartTime,
physicsTime: (window as any).physicsExecutionTime || 0,
renderTime: (window as any).renderExecutionTime || 0,
memoryUsage: this.getMemoryUsage()
});
// 更新FPS计算
this.frameCount++;
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
// 更新UI
this.updateUI();
// 继续循环
requestAnimationFrame(this.gameLoop);
};
private updatePerformanceStats(stats: PerformanceStats): void {
if (this.elements.fps) {
this.elements.fps.textContent = stats.fps.toString();
this.elements.fps.className = stats.fps >= 55 ? 'performance-high' :
stats.fps >= 30 ? 'performance-medium' : 'performance-low';
}
if (this.elements.frameTime) {
this.elements.frameTime.textContent = stats.frameTime.toFixed(2);
this.elements.frameTime.className = stats.frameTime <= 16 ? 'performance-high' :
stats.frameTime <= 33 ? 'performance-medium' : 'performance-low';
}
if (this.elements.physicsTime) {
this.elements.physicsTime.textContent = stats.physicsTime.toFixed(2);
this.elements.physicsTime.className = stats.physicsTime <= 8 ? 'performance-high' :
stats.physicsTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.renderTime) {
this.elements.renderTime.textContent = stats.renderTime.toFixed(2);
this.elements.renderTime.className = stats.renderTime <= 8 ? 'performance-high' :
stats.renderTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.memoryUsage) {
this.elements.memoryUsage.textContent = stats.memoryUsage.toFixed(1);
}
}
private updateUI(): void {
const currentTime = performance.now();
const systemInfo = this.gameScene.getSystemInfo();
// 更新实体数量(每帧更新)
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = systemInfo.entityCount.toString();
}
// 更新Worker状态每500ms更新一次即可
if (currentTime - this.lastWorkerStatusUpdate >= 500) {
this.updateWorkerStatus();
this.lastWorkerStatusUpdate = currentTime;
}
// 更新全局Worker信息供其他系统使用
(window as any).workerInfo = systemInfo.physics;
}
private updateWorkerStatus(): void {
const systemInfo = this.gameScene.getSystemInfo();
const workerInfo = systemInfo.physics;
const entityCount = systemInfo.entityCount;
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.className = 'worker-enabled';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.className = 'worker-disabled';
}
}
if (this.elements.workerLoad) {
if (workerInfo.enabled && entityCount > 0) {
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
} else {
this.elements.workerLoad.textContent = 'N/A';
}
}
}
private getMemoryUsage(): number {
if ('memory' in performance) {
const memory = (performance as any).memory;
return memory.usedJSHeapSize / (1024 * 1024); // MB
}
return 0;
}
private resetDemo(): void {
// 重置所有控件到默认值
if (this.elements.entityCount) {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
}
if (this.elements.gravity) {
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
}
if (this.elements.friction) {
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
}
// 确保Worker被启用
const workerInfo = this.gameScene.getSystemInfo().physics;
if (!workerInfo.enabled) {
this.gameScene.toggleWorker(); // 只有在禁用时才切换
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.textContent = '禁用 Worker';
}
// 重新生成实体
this.gameScene.spawnInitialEntities(1000);
// 重置配置
this.gameScene.updateWorkerConfig({
gravity: 100,
friction: 0.95
});
console.log('演示已重置');
}
public stop(): void {
this.isRunning = false;
}
}
// 启动演示
document.addEventListener('DOMContentLoaded', () => {
try {
new WorkerDemo();
} catch (error) {
console.error('启动演示失败:', error);
document.body.innerHTML = `
<div style="padding: 20px; color: red;">
<h1>启动失败</h1>
<p>错误: ${error}</p>
<p>请确保浏览器支持Web Workers和Canvas API</p>
</div>
`;
}
});

View File

@@ -0,0 +1,30 @@
import { EntitySystem, Matcher, Entity, ECSSystem, Time } from '@esengine/ecs-framework';
import { Lifetime } from '../components';
@ECSSystem('LifetimeSystem')
export class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(Lifetime));
}
protected override process(entities: readonly Entity[]): void {
const entitiesToRemove: Entity[] = [];
for (const entity of entities) {
const lifetime = entity.getComponent(Lifetime)!;
// 更新年龄
lifetime.currentAge += Time.deltaTime;
// 检查是否需要销毁
if (lifetime.isDead()) {
entitiesToRemove.push(entity);
}
}
// 销毁过期的实体
for (const entity of entitiesToRemove) {
entity.destroy();
}
}
}

View File

@@ -0,0 +1,466 @@
import { WorkerEntitySystem, Matcher, Entity, ECSSystem, SharedArrayBufferProcessFunction } from '@esengine/ecs-framework';
import { Position, Velocity, Physics, Renderable } from '../components';
interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
export class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig = {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98 // 减少地面摩擦
};
constructor(enableWorker: boolean = true) {
const defaultConfig = {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: navigator.hardwareConcurrency || 2, // 恢复多Worker
systemConfig: defaultConfig,
useSharedArrayBuffer: true // 使用SharedArrayBuffer进行全局碰撞检测
}
);
}
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable)!;
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable.size
};
}
/**
* Worker处理函数 - 纯函数会被序列化到Worker中执行
* 注意:这个函数内部不能访问外部变量,必须是纯函数
* 非SharedArrayBuffer模式每个Worker只能看到分配给它的实体批次
* 这会导致跨批次的碰撞检测缺失,但单批次内的碰撞是正确的
*/
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
// 创建实体副本以避免修改原始数据
const result = entities.map(e => ({ ...e }));
// 应用重力和基础物理
for (let i = 0; i < result.length; i++) {
const entity = result[i];
// 应用重力
entity.dy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
// 边界碰撞检测和处理
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
// 地面摩擦力
entity.dx *= config.groundFriction;
}
// 空气阻力
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
// 小球间碰撞检测
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
// 计算距离
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
// 检测碰撞
if (distance < minDistance && distance > 0) {
// 碰撞法线
const nx = dx / distance;
const ny = dy / distance;
// 分离小球以避免重叠
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
// 相对速度
const relativeVelocityX = ball2.dx - ball1.dx;
const relativeVelocityY = ball2.dy - ball1.dy;
// 沿碰撞法线的速度分量
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// 如果速度分量为正,小球正在分离,不需要处理
if (velocityAlongNormal > 0) continue;
// 计算弹性系数(两球弹性的平均值)
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
// 计算冲量大小
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
// 应用冲量
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
// 轻微的能量损失,保持活力
const energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
/**
* 应用处理结果
*/
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
// 检查实体是否仍然存在且有效
if (!entity || !entity.enabled) {
return;
}
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
// 检查组件是否仍然存在实体可能在Worker处理期间被修改
if (!position || !velocity) {
return;
}
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
/**
* 更新物理配置
*/
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
/**
* 获取物理配置
*/
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
private startTime: number = 0;
/**
* 性能监控
*/
protected override onEnd(): void {
super.onEnd();
const endTime = performance.now();
const executionTime = endTime - this.startTime;
// 发送性能数据到UI
(window as any).physicsExecutionTime = executionTime;
}
/**
* 获取实体数据大小
*/
protected getDefaultEntityDataSize(): number {
return 9; // id, x, y, dx, dy, mass, bounce, friction, radius
}
/**
* 将实体数据写入SharedArrayBuffer
*/
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量用于Worker函数判断实际有效数据范围
const currentEntityCount = Math.floor(offset / 9) + 1;
sharedArray[0] = currentEntityCount; // 元数据:实际实体数量
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
sharedArray[dataOffset + 0] = entityData.id;
sharedArray[dataOffset + 1] = entityData.x;
sharedArray[dataOffset + 2] = entityData.y;
sharedArray[dataOffset + 3] = entityData.dx;
sharedArray[dataOffset + 4] = entityData.dy;
sharedArray[dataOffset + 5] = entityData.mass;
sharedArray[dataOffset + 6] = entityData.bounce;
sharedArray[dataOffset + 7] = entityData.friction;
sharedArray[dataOffset + 8] = entityData.radius;
}
/**
* 性能监控开始
*/
protected override onBegin(): void {
super.onBegin();
this.startTime = performance.now();
}
/**
* 从SharedArrayBuffer读取实体数据
*/
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
return {
id: sharedArray[dataOffset + 0],
x: sharedArray[dataOffset + 1],
y: sharedArray[dataOffset + 2],
dx: sharedArray[dataOffset + 3],
dy: sharedArray[dataOffset + 4],
mass: sharedArray[dataOffset + 5],
bounce: sharedArray[dataOffset + 6],
friction: sharedArray[dataOffset + 7],
radius: sharedArray[dataOffset + 8]
};
}
/**
* SharedArrayBuffer处理函数
*/
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
// 读取实际实体数量(存储在第一个位置)
const actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset = i * 9 + 9; // 数据从索引9开始加上元数据偏移
// 读取实体数据
const id = sharedFloatArray[offset + 0];
if (id === 0) continue; // 跳过无效实体
let x = sharedFloatArray[offset + 1];
let y = sharedFloatArray[offset + 2];
let dx = sharedFloatArray[offset + 3];
let dy = sharedFloatArray[offset + 4];
const mass = sharedFloatArray[offset + 5];
const bounce = sharedFloatArray[offset + 6];
const friction = sharedFloatArray[offset + 7];
const radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞检测和处理
if (x <= radius) {
x = radius;
dx = -dx * bounce;
} else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
} else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
// 地面摩擦力
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 小球间碰撞检测
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset1 = i * 9 + 9; // 数据从索引9开始加上元数据偏移
const id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0) continue;
let x1 = sharedFloatArray[offset1 + 1];
let y1 = sharedFloatArray[offset1 + 2];
let dx1 = sharedFloatArray[offset1 + 3];
let dy1 = sharedFloatArray[offset1 + 4];
const mass1 = sharedFloatArray[offset1 + 5];
const bounce1 = sharedFloatArray[offset1 + 6];
const radius1 = sharedFloatArray[offset1 + 8];
// 检测与所有其他小球的碰撞(能看到所有实体,实现完整碰撞检测)
for (let j = 0; j < actualEntityCount; j++) {
if (i === j) continue;
const offset2 = j * 9 + 9; // 数据从索引9开始加上元数据偏移
const id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0) continue;
const x2 = sharedFloatArray[offset2 + 1];
const y2 = sharedFloatArray[offset2 + 2];
const dx2 = sharedFloatArray[offset2 + 3];
const dy2 = sharedFloatArray[offset2 + 4];
const mass2 = sharedFloatArray[offset2 + 5];
const bounce2 = sharedFloatArray[offset2 + 6];
const radius2 = sharedFloatArray[offset2 + 8];
// 额外检查:确保位置和半径都是有效值
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
// 计算距离
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const minDistance = radius1 + radius2;
// 检测碰撞
if (distance < minDistance && distance > 0) {
// 碰撞法线
const nx = deltaX / distance;
const ny = deltaY / distance;
// 分离小球 - 只调整当前Worker负责的球
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
// 相对速度
const relativeVelocityX = dx2 - dx1;
const relativeVelocityY = dy2 - dy1;
// 沿碰撞法线的速度分量
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// 如果速度分量为正,小球正在分离
if (velocityAlongNormal > 0) continue;
// 弹性系数
const restitution = (bounce1 + bounce2) * 0.5;
// 冲量计算
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
// 应用冲量到当前小球只更新当前Worker负责的球
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
// 能量损失
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
// 只更新当前Worker负责的实体
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}

View File

@@ -0,0 +1,107 @@
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { Position, Renderable } from '../components';
@ECSSystem('RenderSystem')
export class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private startTime: number = 0;
private batchCount: number = 0;
private drawCallCount: number = 0;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.empty().all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override onBegin(): void {
super.onBegin();
this.startTime = performance.now();
// 清空画布
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
protected override process(entities: readonly Entity[]): void {
// 保持原始绘制顺序,但优化连续相同颜色的绘制
let lastColor = '';
this.drawCallCount = 0;
for (const entity of entities) {
const position = entity.getComponent(Position)!;
const renderable = entity.getComponent(Renderable)!;
// 只在颜色变化时设置fillStyle减少状态切换
if (renderable.color !== lastColor) {
this.ctx.fillStyle = renderable.color;
lastColor = renderable.color;
}
if (renderable.shape === 'circle') {
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
this.drawCallCount++;
} else if (renderable.shape === 'square') {
this.ctx.fillRect(
position.x - renderable.size / 2,
position.y - renderable.size / 2,
renderable.size,
renderable.size
);
this.drawCallCount++;
}
}
// 计算颜色多样性用于显示
const uniqueColors = new Set(entities.map(e => e.getComponent(Renderable)!.color));
this.batchCount = uniqueColors.size;
}
protected override onEnd(): void {
super.onEnd();
const endTime = performance.now();
const executionTime = endTime - this.startTime;
// 发送性能数据到UI
(window as any).renderExecutionTime = executionTime;
// 绘制调试信息
this.drawDebugInfo();
}
private drawDebugInfo(): void {
const entities = this.entities;
this.ctx.fillStyle = '#00ff00';
this.ctx.font = '14px Arial';
this.ctx.fillText(`实体数量: ${entities.length}`, 10, 20);
this.ctx.fillText(`渲染批次: ${this.batchCount}`, 10, 140);
this.ctx.fillText(`绘制调用: ${this.drawCallCount}`, 10, 160);
const workerInfo = (window as any).workerInfo;
if (workerInfo) {
this.ctx.fillStyle = workerInfo.enabled ? '#00ff00' : '#ff0000';
this.ctx.fillText(`Worker: ${workerInfo.enabled ? '启用' : '禁用'}`, 10, 40);
if (workerInfo.enabled) {
this.ctx.fillStyle = '#ffff00';
const entitiesPerWorker = Math.ceil(entities.length / workerInfo.workerCount);
this.ctx.fillText(`每个Worker实体: ${entitiesPerWorker}`, 10, 60);
this.ctx.fillText(`Worker数量: ${workerInfo.workerCount}`, 10, 80);
}
}
// 显示性能信息
const physicsTime = (window as any).physicsExecutionTime || 0;
const renderTime = (window as any).renderExecutionTime || 0;
this.ctx.fillStyle = physicsTime > 16 ? '#ff0000' : physicsTime > 8 ? '#ffff00' : '#00ff00';
this.ctx.fillText(`物理: ${physicsTime.toFixed(2)}ms`, 10, 100);
this.ctx.fillStyle = renderTime > 16 ? '#ff0000' : renderTime > 8 ? '#ffff00' : '#00ff00';
this.ctx.fillText(`渲染: ${renderTime.toFixed(2)}ms`, 10, 120);
}
}

View File

@@ -0,0 +1,3 @@
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
export { RenderSystem } from './RenderSystem';
export { LifetimeSystem } from './LifetimeSystem';

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,26 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
open: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'
}
},
build: {
target: 'es2020',
outDir: 'dist',
minify: false,
rollupOptions: {
output: {
format: 'es',
manualChunks: undefined
}
}
},
esbuild: {
target: 'es2020'
}
});