core库demo更新

This commit is contained in:
YHH
2025-10-08 20:52:31 +08:00
parent 69c46f32eb
commit 610232e6b0
21 changed files with 2020 additions and 1536 deletions

View File

@@ -0,0 +1,483 @@
<!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 Core Demos</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #0f0f23;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
}
.app-container {
display: flex;
height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
border-right: 2px solid #0a3d62;
display: flex;
flex-direction: column;
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
}
.sidebar-header {
padding: 30px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text-align: center;
}
.sidebar-header h1 {
font-size: 1.5em;
color: white;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.sidebar-header p {
font-size: 0.85em;
color: rgba(255,255,255,0.9);
font-weight: 300;
}
.demo-list {
flex: 1;
overflow-y: auto;
padding: 15px 0;
}
.demo-category {
margin-bottom: 10px;
}
.category-title {
padding: 12px 20px;
color: #8892b0;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.demo-item {
padding: 14px 20px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
display: flex;
align-items: center;
gap: 12px;
}
.demo-item:hover {
background: rgba(102, 126, 234, 0.1);
border-left-color: #667eea;
}
.demo-item.active {
background: rgba(102, 126, 234, 0.2);
border-left-color: #667eea;
}
.demo-icon {
font-size: 20px;
width: 24px;
text-align: center;
}
.demo-info {
flex: 1;
}
.demo-name {
font-size: 14px;
font-weight: 500;
color: #ccd6f6;
margin-bottom: 2px;
}
.demo-item.active .demo-name {
color: #667eea;
font-weight: 600;
}
.demo-desc {
font-size: 11px;
color: #8892b0;
line-height: 1.3;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center;
}
.github-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: rgba(102, 126, 234, 0.2);
color: #667eea;
text-decoration: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.github-link:hover {
background: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-header {
padding: 25px 40px;
background: #1a1a2e;
border-bottom: 2px solid #0a3d62;
}
.content-header h2 {
font-size: 2em;
color: #ccd6f6;
margin-bottom: 8px;
}
.content-header p {
color: #8892b0;
font-size: 14px;
line-height: 1.6;
}
.demo-canvas-container {
flex: 1;
position: relative;
overflow: hidden;
background: #0a0a15;
}
#demoCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 控制面板 */
.control-panel {
position: absolute;
top: 20px;
right: 20px;
width: 320px;
background: rgba(26, 26, 46, 0.95);
border-radius: 12px;
border: 1px solid rgba(102, 126, 234, 0.3);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
backdrop-filter: blur(10px);
max-height: calc(100% - 40px);
overflow-y: auto;
}
.control-panel-header {
padding: 15px 20px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px 12px 0 0;
}
.control-panel-header h3 {
color: #667eea;
font-size: 16px;
font-weight: 600;
}
.control-panel-content {
padding: 20px;
}
.control-section {
margin-bottom: 20px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section h4 {
color: #8892b0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
font-weight: 600;
}
.button-group {
display: flex;
flex-direction: column;
gap: 8px;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button.secondary {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
}
button.secondary:hover {
background: rgba(102, 126, 234, 0.3);
}
button.danger {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
button.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.input-group {
margin-bottom: 12px;
}
.input-group label {
display: block;
color: #8892b0;
font-size: 12px;
margin-bottom: 6px;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: #ccd6f6;
font-size: 13px;
transition: all 0.2s;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
background: rgba(255,255,255,0.08);
}
/* 统计信息 */
.stats-panel {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 15px;
}
.stat-item {
background: rgba(255,255,255,0.03);
padding: 12px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.stat-label {
color: #8892b0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.stat-value {
color: #667eea;
font-size: 20px;
font-weight: 700;
}
/* Toast通知 */
.toast {
position: fixed;
bottom: 30px;
right: 30px;
background: rgba(26, 26, 46, 0.98);
border: 1px solid #667eea;
border-radius: 8px;
padding: 15px 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
transform: translateY(150%);
transition: transform 0.3s ease;
z-index: 1000;
min-width: 280px;
}
.toast.show {
transform: translateY(0);
}
.toast-content {
display: flex;
align-items: center;
gap: 12px;
}
.toast-icon {
font-size: 20px;
}
.toast-message {
color: #ccd6f6;
font-size: 14px;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
/* 加载动画 */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 15px;
color: #8892b0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<h1>🎮 ECS Core Demos</h1>
<p>交互式演示集合</p>
</div>
<div class="demo-list" id="demoList">
<!-- Demo列表将通过JS动态生成 -->
</div>
<div class="sidebar-footer">
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
<span></span>
<span>GitHub</span>
</a>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="content-header">
<h2 id="demoTitle">选择一个演示开始</h2>
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
</div>
<div class="demo-canvas-container">
<canvas id="demoCanvas"></canvas>
<!-- 控制面板 -->
<div class="control-panel" id="controlPanel" style="display: none;">
<div class="control-panel-header">
<h3>控制面板</h3>
</div>
<div class="control-panel-content" id="controlPanelContent">
<!-- 控制内容将由各个demo动态生成 -->
</div>
</div>
<!-- 加载动画 -->
<div class="loading" id="loading" style="display: none;">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
</div>
<!-- Toast通知 -->
<div class="toast" id="toast">
<div class="toast-content">
<span class="toast-icon"></span>
<span class="toast-message" id="toastMessage"></span>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

630
examples/core-demos/package-lock.json generated Normal file
View File

@@ -0,0 +1,630 @@
{
"name": "ecs-core-demos",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-core-demos",
"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.51",
"license": "MIT",
"dependencies": {
"msgpack-lite": "^0.1.26"
},
"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/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"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.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"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-core-demos",
"version": "1.0.0",
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
"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,99 @@
import { Scene, Core } from '@esengine/ecs-framework';
export interface DemoInfo {
id: string;
name: string;
description: string;
category: string;
icon: string;
}
export abstract class DemoBase {
protected scene: Scene;
protected canvas: HTMLCanvasElement;
protected ctx: CanvasRenderingContext2D;
protected controlPanel: HTMLElement;
protected isRunning: boolean = false;
protected animationFrameId: number | null = null;
protected lastTime: number = 0;
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.controlPanel = controlPanel;
this.scene = new Scene({ name: this.getInfo().name });
// 设置canvas大小
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
abstract getInfo(): DemoInfo;
abstract setup(): void;
abstract createControls(): void;
protected resizeCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
}
public start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
// 设置当前场景到Core
Core.setScene(this.scene);
this.scene.begin();
this.loop();
}
public stop() {
this.isRunning = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
public destroy() {
this.stop();
this.scene.end();
}
protected loop = () => {
if (!this.isRunning) return;
// 计算deltaTime
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
Core.update(deltaTime);
// 渲染
this.render();
// 继续循环
this.animationFrameId = requestAnimationFrame(this.loop);
}
protected abstract render(): void;
protected showToast(message: string, icon: string = '✅') {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = icon;
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
}

View File

@@ -0,0 +1,387 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Serializable,
Serialize,
SerializeAsMap
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('SerDemo_Position')
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('SerDemo_Velocity')
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('SerDemo_Renderable')
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
@ECSComponent('SerDemo_Player')
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
class PlayerComponent extends Component {
@Serialize() name: string = 'Player';
@Serialize() level: number = 1;
@Serialize() health: number = 100;
@SerializeAsMap() inventory: Map<string, number> = new Map();
constructor(name: string = 'Player') {
super();
this.name = name;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
update() {
const entities = this.scene.entities.buffer;
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent);
const vel = entity.getComponent(VelocityComponent);
if (pos && vel) {
pos.x += vel.vx;
pos.y += vel.vy;
// 边界反弹
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super();
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
update() {
// 清空画布
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染所有实体
const entities = this.scene.entities.buffer;
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent);
const render = entity.getComponent(RenderableComponent);
if (pos && render) {
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
// 如果是玩家,显示名字
const player = entity.getComponent(PlayerComponent);
if (player) {
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
}
}
}
}
}
export class SerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private jsonData: string = '';
private binaryData: Buffer | null = null;
getInfo(): DemoInfo {
return {
id: 'serialization',
name: '场景序列化',
description: '演示场景的序列化和反序列化功能支持JSON和二进制格式',
category: '核心功能',
icon: '💾'
};
}
setup() {
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
// ComponentRegistry会被序列化系统自动使用无需手动注册
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建控制面板
this.createControls();
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
const playerComp = new PlayerComponent('Hero');
playerComp.level = 5;
playerComp.health = 100;
playerComp.inventory.set('sword', 1);
playerComp.inventory.set('potion', 5);
player.addComponent(playerComp);
// 创建一些随机实体
for (let i = 0; i < 5; i++) {
this.createRandomEntity();
}
// 设置场景数据
this.scene.sceneData.set('weather', 'sunny');
this.scene.sceneData.set('gameTime', 12.5);
this.scene.sceneData.set('difficulty', 'normal');
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="clearEntities" class="danger">清空所有实体</button>
</div>
</div>
<div class="control-section">
<h4>序列化操作</h4>
<div class="button-group">
<button id="serializeJSON">序列化为JSON</button>
<button id="serializeBinary" class="success">序列化为二进制</button>
<button id="deserialize" class="secondary">反序列化恢复</button>
</div>
</div>
<div class="control-section">
<h4>本地存储</h4>
<div class="button-group">
<button id="saveLocal" class="success">保存到LocalStorage</button>
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
</div>
</div>
<div class="control-section">
<h4>场景数据</h4>
<div class="input-group">
<label>天气</label>
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
</div>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
</div>
<div class="control-section">
<h4>序列化数据预览</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
点击序列化按钮查看数据...
</div>
</div>
`;
// 绑定事件
this.bindEvents();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.updateStats();
this.showToast('添加了一个随机实体');
});
document.getElementById('clearEntities')!.addEventListener('click', () => {
this.scene.destroyAllEntities();
this.createInitialEntities();
this.updateStats();
this.showToast('场景已重置');
});
document.getElementById('serializeJSON')!.addEventListener('click', () => {
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
this.updateDataPreview(this.jsonData, 'json');
this.updateStats();
this.showToast('已序列化为JSON格式');
});
document.getElementById('serializeBinary')!.addEventListener('click', () => {
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
const base64 = this.binaryData.toString('base64');
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
this.updateStats();
this.showToast('已序列化为二进制格式', '🔐');
});
document.getElementById('deserialize')!.addEventListener('click', () => {
const data = this.binaryData || this.jsonData;
if (!data) {
this.showToast('请先执行序列化操作', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('场景已恢复');
});
document.getElementById('saveLocal')!.addEventListener('click', () => {
const jsonData = this.scene.serialize({ format: 'json' }) as string;
localStorage.setItem('ecs_demo_scene', jsonData);
this.showToast('已保存到LocalStorage', '💾');
});
document.getElementById('loadLocal')!.addEventListener('click', () => {
const data = localStorage.getItem('ecs_demo_scene');
if (!data) {
this.showToast('LocalStorage中没有保存的场景', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('已从LocalStorage加载', '📂');
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const weather = (document.getElementById('weather') as HTMLInputElement).value;
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
this.scene.sceneData.set('weather', weather);
this.scene.sceneData.set('gameTime', gameTime);
this.showToast('场景数据已更新');
});
// 初始更新统计
this.updateStats();
}
private updateDataPreview(data: string, format: string) {
const preview = document.getElementById('dataPreview')!;
if (format === 'json') {
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
preview.textContent = truncated;
} else {
preview.textContent = data;
}
}
private updateStats() {
const entityCount = this.scene.entities.count;
document.getElementById('entityCount')!.textContent = entityCount.toString();
// 计算JSON大小
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
}
// 计算二进制大小
if (this.binaryData) {
const binarySize = this.binaryData.length;
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
// 计算压缩率
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
}
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
}

View File

@@ -0,0 +1,835 @@
import { DemoBase, DemoInfo } from './DemoBase';
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
import { BrowserAdapter } from '../platform/BrowserAdapter';
// ============ 组件定义 ============
@ECSComponent('WorkerDemo_Position')
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('WorkerDemo_Velocity')
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;
}
}
@ECSComponent('WorkerDemo_Physics')
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('WorkerDemo_Renderable')
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('WorkerDemo_Lifetime')
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;
}
}
// ============ 系统定义 ============
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')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig;
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
const defaultConfig = {
gravity: 100,
canvasWidth,
canvasHeight,
groundFriction: 0.98
};
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true
}
);
this.physicsConfig = defaultConfig;
}
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
};
}
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || this.physicsConfig;
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);
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 };
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量
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 readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引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]
};
}
protected getSharedArrayBufferProcessFunction(): any {
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;
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 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;
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;
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;
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);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.empty().all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: Entity[]): void {
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const position = entity.getComponent(Position);
const renderable = entity.getComponent(Renderable);
if (!position || !renderable) continue;
this.ctx.fillStyle = renderable.color;
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
}
}
}
@ECSSystem('LifetimeSystem')
class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(Lifetime));
}
protected override process(entities: Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
const lifetime = entity.getComponent(Lifetime);
if (!lifetime) continue;
lifetime.currentAge += deltaTime;
if (lifetime.isDead()) {
entity.destroy();
}
}
}
}
// ============ Demo类 ============
export class WorkerSystemDemo extends DemoBase {
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
private currentFPS = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private elements: { [key: string]: HTMLElement } = {};
getInfo(): DemoInfo {
return {
id: 'worker-system',
name: 'Worker System',
description: '演示 ECS 框架中的多线程物理计算能力',
category: '核心功能',
icon: '⚙️'
};
}
setup(): void {
// 注册浏览器平台适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 初始化系统
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
this.scene.addSystem(this.physicsSystem);
this.scene.addSystem(this.lifetimeSystem);
this.scene.addSystem(this.renderSystem);
// 创建控制面板
this.createControls();
// 初始化UI元素引用
this.initializeUIElements();
this.bindEvents();
// 生成初始实体
this.spawnInitialEntities(1000);
}
createControls(): void {
this.controlPanel.innerHTML = `
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
style="width: 100%; margin-bottom: 5px;">
<span id="entityCountValue" style="color: #fff;">1000</span>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
禁用 Worker
</button>
</div>
<div style="margin-bottom: 15px;">
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
生成粒子爆炸
</button>
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
清空所有实体
</button>
<button id="resetDemo" style="width: 100%; padding: 8px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
重置演示
</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
style="width: 100%; margin-bottom: 5px;">
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5"
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
</div>
</div>
`;
}
protected render(): void {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
this.updateUI();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
}
}
}
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.spawnInitialEntities(count);
});
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
});
}
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.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.updateWorkerConfig({ friction });
});
}
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.spawnParticleExplosion(centerX, centerY, 100);
});
}
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.clearAllEntities();
});
}
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
this.spawnInitialEntities(1000);
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
});
}
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.spawnParticleExplosion(x, y, 30);
});
}
private updateUI(): void {
const workerInfo = this.physicsSystem.getWorkerInfo();
if (this.elements.fps) {
this.elements.fps.textContent = this.currentFPS.toString();
}
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
}
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.style.color = '#4eff4a';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.style.color = '#ff4a4a';
}
}
if (this.elements.workerLoad) {
const entityCount = this.scene.entities.count;
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 spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
private createParticle(): void {
const entity = this.scene.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'
];
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));
}
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.scene.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));
}
}
private clearAllEntities(): void {
const entities = [...this.scene.entities.buffer];
for (const entity of entities) {
entity.destroy();
}
}
private toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 保存当前物理配置
const currentConfig = this.physicsSystem.getPhysicsConfig();
this.scene.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
this.physicsSystem.updateOrder = 1;
// 恢复物理配置
this.physicsSystem.updatePhysicsConfig(currentConfig);
this.scene.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
private 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
});
}
}
}

View File

@@ -0,0 +1,12 @@
import { DemoBase } from './DemoBase';
import { SerializationDemo } from './SerializationDemo';
import { WorkerSystemDemo } from './WorkerSystemDemo';
export { DemoBase, SerializationDemo, WorkerSystemDemo };
// Demo注册表
export const DEMO_REGISTRY: typeof DemoBase[] = [
SerializationDemo,
WorkerSystemDemo,
// 更多demos可以在这里添加
];

View File

@@ -0,0 +1,171 @@
import { DEMO_REGISTRY, DemoBase } from './demos';
import { Core } from '@esengine/ecs-framework';
class DemoManager {
private demos: Map<string, typeof DemoBase> = new Map();
private currentDemo: DemoBase | null = null;
private canvas: HTMLCanvasElement;
private controlPanel: HTMLElement;
constructor() {
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
// 注册所有demos
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
this.demos.set(info.id, DemoClass);
tempInstance.destroy();
}
// 渲染demo列表
this.renderDemoList();
// 自动加载第一个demo
const firstDemo = DEMO_REGISTRY[0];
if (firstDemo) {
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
this.loadDemo(info.id);
}
}
private renderDemoList() {
const demoList = document.getElementById('demoList')!;
// 按分类组织demos
const categories = new Map<string, typeof DemoBase[]>();
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
if (!categories.has(info.category)) {
categories.set(info.category, []);
}
categories.get(info.category)!.push(DemoClass);
}
// 渲染分类和demos
let html = '';
for (const [category, demoClasses] of categories) {
html += `<div class="demo-category">`;
html += `<div class="category-title">${category}</div>`;
for (const DemoClass of demoClasses) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
html += `
<div class="demo-item" data-demo-id="${info.id}">
<div class="demo-icon">${info.icon}</div>
<div class="demo-info">
<div class="demo-name">${info.name}</div>
<div class="demo-desc">${info.description}</div>
</div>
</div>
`;
}
html += `</div>`;
}
demoList.innerHTML = html;
// 绑定点击事件
demoList.querySelectorAll('.demo-item').forEach(item => {
item.addEventListener('click', () => {
const demoId = item.getAttribute('data-demo-id')!;
this.loadDemo(demoId);
});
});
}
private loadDemo(demoId: string) {
// 停止并销毁当前demo
if (this.currentDemo) {
this.currentDemo.destroy();
this.currentDemo = null;
}
// 显示加载动画
const loading = document.getElementById('loading')!;
loading.style.display = 'block';
// 延迟加载,给用户反馈
setTimeout(() => {
const DemoClass = this.demos.get(demoId);
if (!DemoClass) {
console.error(`Demo ${demoId} not found`);
loading.style.display = 'none';
return;
}
try {
// 创建新demo
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
const info = this.currentDemo.getInfo();
// 更新页面标题和描述
document.getElementById('demoTitle')!.textContent = info.name;
document.getElementById('demoDescription')!.textContent = info.description;
// 设置demo
this.currentDemo.setup();
// 显示控制面板
this.controlPanel.style.display = 'block';
// 启动demo
this.currentDemo.start();
// 更新菜单选中状态
document.querySelectorAll('.demo-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-demo-id') === demoId) {
item.classList.add('active');
}
});
loading.style.display = 'none';
console.log(`✅ Demo "${info.name}" loaded successfully`);
} catch (error) {
console.error(`Failed to load demo ${demoId}:`, error);
loading.style.display = 'none';
this.showError('加载演示失败:' + (error as Error).message);
}
}, 300);
}
private showError(message: string) {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = '❌';
toastMessage.textContent = message;
toast.style.borderColor = '#f5576c';
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
toast.style.borderColor = '#667eea';
}, 3000);
}
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
new DemoManager();
});

View File

@@ -0,0 +1,204 @@
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 浏览器平台适配器
*/
export class BrowserAdapter implements IPlatformAdapter {
public readonly name = 'browser';
public readonly version: string;
constructor() {
this.version = this.getBrowserInfo();
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
return typeof Worker !== 'undefined';
}
/**
* 检查是否支持SharedArrayBuffer
*/
public isSharedArrayBufferSupported(): boolean {
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
}
/**
* 获取硬件并发数CPU核心数
*/
public getHardwareConcurrency(): number {
return navigator.hardwareConcurrency || 4;
}
/**
* 创建Worker
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('浏览器不支持Worker');
}
try {
return new BrowserWorker(script, options);
} catch (error) {
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
if (!this.isSharedArrayBufferSupported()) {
return null;
}
try {
return new SharedArrayBuffer(length);
} catch (error) {
console.warn('SharedArrayBuffer创建失败:', error);
return null;
}
}
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
return performance.now();
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: this.getHardwareConcurrency(),
supportsModuleWorker: false,
supportsTransferableObjects: true,
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
workerScriptPrefix: '',
limitations: {
noEval: false,
requiresWorkerInit: false
}
};
}
/**
* 获取浏览器信息
*/
private getBrowserInfo(): string {
const userAgent = navigator.userAgent;
if (userAgent.includes('Chrome')) {
const match = userAgent.match(/Chrome\/([0-9.]+)/);
return match ? `Chrome ${match[1]}` : 'Chrome';
} else if (userAgent.includes('Firefox')) {
const match = userAgent.match(/Firefox\/([0-9.]+)/);
return match ? `Firefox ${match[1]}` : 'Firefox';
} else if (userAgent.includes('Safari')) {
const match = userAgent.match(/Version\/([0-9.]+)/);
return match ? `Safari ${match[1]}` : 'Safari';
}
return 'Unknown Browser';
}
/**
* 检查SharedArrayBuffer是否真正可用
*/
private checkSharedArrayBufferEnabled(): boolean {
try {
new SharedArrayBuffer(8);
return true;
} catch {
return false;
}
}
}
/**
* 浏览器Worker封装
*/
class BrowserWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: Worker;
constructor(script: string, options: WorkerCreationOptions = {}) {
this.worker = this.createBrowserWorker(script, options);
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
if (transfer && transfer.length > 0) {
this.worker.postMessage(message, transfer);
} else {
this.worker.postMessage(message);
}
} catch (error) {
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onmessage = (event: MessageEvent) => {
handler({ data: event.data });
};
}
public onError(handler: (error: ErrorEvent) => void): void {
this.worker.onerror = handler;
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
} catch (error) {
console.error('终止Worker失败:', error);
}
}
}
/**
* 创建浏览器Worker
*/
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
try {
// 创建Blob URL
const blob = new Blob([script], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
// 创建Worker
const worker = new Worker(url, {
type: options.type || 'classic',
credentials: options.credentials,
name: options.name
});
// 清理Blob URL延迟清理确保Worker已加载
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
return worker;
} catch (error) {
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
}
}
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3003,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
}
},
build: {
target: 'es2020',
outDir: 'dist'
}
});